diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 3b79636f..ba35807a 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -391,9 +391,32 @@ export function readPaperclipSkillSyncPreference(config: Record }; } +function canonicalizeDesiredPaperclipSkillReference( + reference: string, + availableEntries: Array<{ key: string; runtimeName?: string | null }>, +): string { + const normalizedReference = reference.trim().toLowerCase(); + if (!normalizedReference) return ""; + + const exactKey = availableEntries.find((entry) => entry.key.trim().toLowerCase() === normalizedReference); + if (exactKey) return exactKey.key; + + const byRuntimeName = availableEntries.filter((entry) => + typeof entry.runtimeName === "string" && entry.runtimeName.trim().toLowerCase() === normalizedReference, + ); + if (byRuntimeName.length === 1) return byRuntimeName[0]!.key; + + const slugMatches = availableEntries.filter((entry) => + entry.key.trim().toLowerCase().split("/").pop() === normalizedReference, + ); + if (slugMatches.length === 1) return slugMatches[0]!.key; + + return normalizedReference; +} + export function resolvePaperclipDesiredSkillNames( config: Record, - availableEntries: Array<{ key: string; required?: boolean }>, + availableEntries: Array<{ key: string; runtimeName?: string | null; required?: boolean }>, ): string[] { const preference = readPaperclipSkillSyncPreference(config); const requiredSkills = availableEntries @@ -402,7 +425,10 @@ export function resolvePaperclipDesiredSkillNames( if (!preference.explicit) { return Array.from(new Set(requiredSkills)); } - return Array.from(new Set([...requiredSkills, ...preference.desiredSkills])); + const desiredSkills = preference.desiredSkills + .map((reference) => canonicalizeDesiredPaperclipSkillReference(reference, availableEntries)) + .filter(Boolean); + return Array.from(new Set([...requiredSkills, ...desiredSkills])); } export function writePaperclipSkillSyncPreference( diff --git a/server/src/__tests__/claude-local-skill-sync.test.ts b/server/src/__tests__/claude-local-skill-sync.test.ts index a103aa7c..b8b761b2 100644 --- a/server/src/__tests__/claude-local-skill-sync.test.ts +++ b/server/src/__tests__/claude-local-skill-sync.test.ts @@ -39,4 +39,23 @@ describe("claude local skill sync", () => { expect(snapshot.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured"); expect(snapshot.entries.find((entry) => entry.key === createAgentKey)?.state).toBe("configured"); }); + + it("normalizes legacy flat Paperclip skill refs to canonical keys", async () => { + const snapshot = await listClaudeSkills({ + agentId: "agent-3", + companyId: "company-1", + adapterType: "claude_local", + config: { + paperclipSkillSync: { + desiredSkills: ["paperclip"], + }, + }, + }); + + expect(snapshot.warnings).toEqual([]); + expect(snapshot.desiredSkills).toContain(paperclipKey); + expect(snapshot.desiredSkills).not.toContain("paperclip"); + expect(snapshot.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured"); + expect(snapshot.entries.find((entry) => entry.key === "paperclip")).toBeUndefined(); + }); }); diff --git a/server/src/__tests__/codex-local-skill-sync.test.ts b/server/src/__tests__/codex-local-skill-sync.test.ts index 7068e43f..a78d37e7 100644 --- a/server/src/__tests__/codex-local-skill-sync.test.ts +++ b/server/src/__tests__/codex-local-skill-sync.test.ts @@ -86,4 +86,29 @@ describe("codex local skill sync", () => { expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed"); expect((await fs.lstat(path.join(codexHome, "skills", "paperclip"))).isSymbolicLink()).toBe(true); }); + + it("normalizes legacy flat Paperclip skill refs before reporting persistent state", async () => { + const codexHome = await makeTempDir("paperclip-codex-legacy-skill-sync-"); + cleanupDirs.add(codexHome); + + const snapshot = await listCodexSkills({ + agentId: "agent-3", + companyId: "company-1", + adapterType: "codex_local", + config: { + env: { + CODEX_HOME: codexHome, + }, + paperclipSkillSync: { + desiredSkills: ["paperclip"], + }, + }, + }); + + expect(snapshot.warnings).toEqual([]); + expect(snapshot.desiredSkills).toContain(paperclipKey); + expect(snapshot.desiredSkills).not.toContain("paperclip"); + expect(snapshot.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("missing"); + expect(snapshot.entries.find((entry) => entry.key === "paperclip")).toBeUndefined(); + }); });