From 818aa0f1d6e0bca7f6c388810a5a7258817a6ed3 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Fri, 24 Apr 2026 20:41:01 +0000 Subject: [PATCH] feat: log bundled skill names and add skills to onMeta commandNotes (FAR-36) Adds a diagnostic log line after skill resolution so operators can see exactly which skills were bundled into each run, making it straightforward to diagnose skill availability issues. Also surfaces the skill list in the onMeta commandNotes for run metadata visibility. Co-Authored-By: Claude Sonnet 4.6 --- src/server/execute.test.ts | 56 ++++++++++++++++++++++++++++++++++++++ src/server/execute.ts | 3 ++ 2 files changed, 59 insertions(+) diff --git a/src/server/execute.test.ts b/src/server/execute.test.ts index 4970481..1aefa30 100644 --- a/src/server/execute.test.ts +++ b/src/server/execute.test.ts @@ -1167,6 +1167,62 @@ describe("execute: happy path", () => { expect(result.exitCode).toBe(0); }); + + it("logs bundled skill names and count (FAR-36 diagnostic)", async () => { + const skills = [ + { key: "safety--abc123", runtimeName: "safety--abc123", desired: true, managed: true, required: true, state: "configured" as const }, + { key: "sdlc--def456", runtimeName: "sdlc--def456", desired: true, managed: true, required: true, state: "configured" as const }, + ]; + mockReadSkillEntries.mockResolvedValue(skills); + + const logs: Array<{ stream: string; msg: string }> = []; + const onLog = vi.fn().mockImplementation(async (stream: string, msg: string) => { logs.push({ stream, msg }); }); + + const executePromise = execute(makeCtx({ onLog } as Partial)); + await vi.advanceTimersByTimeAsync(3_100); + await executePromise; + + const skillLine = logs.find((l) => l.msg.includes("Skills bundled")); + expect(skillLine).toBeDefined(); + expect(skillLine?.stream).toBe("stdout"); + expect(skillLine?.msg).toContain("(2):"); + expect(skillLine?.msg).toContain("safety--abc123"); + expect(skillLine?.msg).toContain("sdlc--def456"); + }); + + it("logs Skills bundled (0): none when no skills are configured (FAR-36 diagnostic)", async () => { + mockReadSkillEntries.mockResolvedValue([]); + + const logs: Array<{ stream: string; msg: string }> = []; + const onLog = vi.fn().mockImplementation(async (stream: string, msg: string) => { logs.push({ stream, msg }); }); + + const executePromise = execute(makeCtx({ onLog } as Partial)); + await vi.advanceTimersByTimeAsync(3_100); + await executePromise; + + const skillLine = logs.find((l) => l.msg.includes("Skills bundled")); + expect(skillLine).toBeDefined(); + expect(skillLine?.msg).toContain("(0): none"); + }); + + it("includes skill count in onMeta commandNotes (FAR-36 diagnostic)", async () => { + const skills = [ + { key: "safety--abc123", runtimeName: "safety--abc123", desired: true, managed: true, required: true, state: "configured" as const }, + ]; + mockReadSkillEntries.mockResolvedValue(skills); + + const onMeta = vi.fn().mockResolvedValue(undefined); + const executePromise = execute(makeCtx({ onMeta } as Partial)); + await vi.advanceTimersByTimeAsync(3_100); + await executePromise; + + expect(onMeta).toHaveBeenCalled(); + const notes: string[] = onMeta.mock.calls[0][0].commandNotes; + const skillsNote = notes.find((n: string) => n.startsWith("Skills")); + expect(skillsNote).toBeDefined(); + expect(skillsNote).toContain("(1):"); + expect(skillsNote).toContain("safety--abc123"); + }); }); // ─── execute: waitForPod edge cases ────────────────────────────────────────── diff --git a/src/server/execute.ts b/src/server/execute.ts index 1e5c8b6..4920109 100644 --- a/src/server/execute.ts +++ b/src/server/execute.ts @@ -796,6 +796,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise desiredSkillNames.has(e.key)); + const skillSummary = desiredSkills.length > 0 ? desiredSkills.map((s) => s.runtimeName ?? s.key).join(", ") : "none"; + await onLog("stdout", `[paperclip] Skills bundled (${desiredSkills.length}): ${skillSummary}\n`); const instructionsFilePath = asString(config.instructionsFilePath, "").trim(); const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : ""; let instructionsContents: string | null = null; @@ -892,6 +894,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise