77ed2004f8
K8s Job pods were starting without the Paperclip skill loaded, so agents could not find their heartbeat procedure and reported "no issue content in my workspace" on every wake. Root cause: claude_local materialises skills into a PVC-backed prompt-bundle directory and passes --add-dir to Claude, but claude_k8s did neither. Changes: - Add src/server/prompt-cache.ts with prepareClaudePromptBundle (ported from adapter-claude-local). Writes skill symlinks and the agent's instructions file into a content-addressed bundle directory under the shared PVC (/paperclip/instances/.../claude-prompt-cache/<hash>/). - execute.ts: read desired skills and instructions file before building the Job manifest, then call prepareClaudePromptBundle and pass the resulting bundle to buildJobManifest. - job-manifest.ts: accept optional promptBundle in JobBuildInput; when present, pass --add-dir <bundle.addDir> and use bundle.instructionsFilePath for --append-system-prompt-file. Also fix: skip --append-system-prompt-file on session resumes to avoid wasting tokens on re-injection. - skills.ts: correct the detail string to reflect actual materialisation. - job-manifest.test.ts: add 5 new tests covering --add-dir injection, bundle path preference, session-resume skipping, and fallback behaviour. Co-Authored-By: Paperclip <noreply@paperclip.ing>
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
import type {
|
|
AdapterSkillContext,
|
|
AdapterSkillSnapshot,
|
|
AdapterSkillEntry,
|
|
} from "@paperclipai/adapter-utils";
|
|
import {
|
|
readPaperclipRuntimeSkillEntries,
|
|
resolvePaperclipDesiredSkillNames,
|
|
readInstalledSkillTargets,
|
|
} from "@paperclipai/adapter-utils/server-utils";
|
|
import path from "node:path";
|
|
|
|
const SKILLS_HOME = "/paperclip/.claude/skills";
|
|
|
|
async function buildK8sSkillSnapshot(
|
|
config: Record<string, unknown>,
|
|
): Promise<AdapterSkillSnapshot> {
|
|
const availableEntries = await readPaperclipRuntimeSkillEntries(config, import.meta.dirname ?? __dirname);
|
|
const availableByKey = new Map(availableEntries.map((e) => [e.key, e]));
|
|
const desiredSkills = resolvePaperclipDesiredSkillNames(config, availableEntries);
|
|
const desiredSet = new Set(desiredSkills);
|
|
const installed = await readInstalledSkillTargets(SKILLS_HOME);
|
|
|
|
const entries: AdapterSkillEntry[] = availableEntries.map((entry) => ({
|
|
key: entry.key,
|
|
runtimeName: entry.runtimeName,
|
|
desired: desiredSet.has(entry.key),
|
|
managed: true,
|
|
state: desiredSet.has(entry.key) ? "configured" : "available",
|
|
origin: entry.required ? "paperclip_required" : "company_managed",
|
|
originLabel: entry.required ? "Required by Paperclip" : "Managed by Paperclip",
|
|
readOnly: false,
|
|
sourcePath: entry.source,
|
|
targetPath: null,
|
|
detail: desiredSet.has(entry.key)
|
|
? "Materialized into the PVC-backed Claude prompt bundle before each K8s Job run."
|
|
: null,
|
|
required: Boolean(entry.required),
|
|
requiredReason: entry.requiredReason ?? null,
|
|
}));
|
|
|
|
const warnings: string[] = [];
|
|
|
|
for (const desiredSkill of desiredSkills) {
|
|
if (availableByKey.has(desiredSkill)) continue;
|
|
warnings.push(`Desired skill "${desiredSkill}" is not available from the Paperclip skills directory.`);
|
|
entries.push({
|
|
key: desiredSkill,
|
|
runtimeName: null,
|
|
desired: true,
|
|
managed: true,
|
|
state: "missing",
|
|
origin: "external_unknown",
|
|
originLabel: "External or unavailable",
|
|
readOnly: false,
|
|
sourcePath: undefined,
|
|
targetPath: undefined,
|
|
detail: "Paperclip cannot find this skill in the runtime skills directory.",
|
|
});
|
|
}
|
|
|
|
for (const [name, installedEntry] of installed.entries()) {
|
|
if (availableEntries.some((e) => e.runtimeName === name)) continue;
|
|
entries.push({
|
|
key: name,
|
|
runtimeName: name,
|
|
desired: false,
|
|
managed: false,
|
|
state: "external",
|
|
origin: "user_installed",
|
|
originLabel: "User-installed",
|
|
locationLabel: "~/.claude/skills",
|
|
readOnly: true,
|
|
sourcePath: null,
|
|
targetPath: installedEntry.targetPath ?? path.join(SKILLS_HOME, name),
|
|
detail: "Installed outside Paperclip management in the Claude skills home.",
|
|
});
|
|
}
|
|
|
|
entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
|
|
return {
|
|
adapterType: "claude_k8s",
|
|
supported: true,
|
|
mode: "ephemeral",
|
|
desiredSkills,
|
|
entries,
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
export async function listK8sSkills(ctx: AdapterSkillContext): Promise<AdapterSkillSnapshot> {
|
|
return buildK8sSkillSnapshot(ctx.config);
|
|
}
|
|
|
|
export async function syncK8sSkills(
|
|
ctx: AdapterSkillContext,
|
|
_desiredSkills: string[],
|
|
): Promise<AdapterSkillSnapshot> {
|
|
return buildK8sSkillSnapshot(ctx.config);
|
|
}
|