From 771242ea3d3cb67400366a89546d8ef3db7360dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A4cker=2C=20Henning?= Date: Fri, 24 Apr 2026 16:16:22 +0200 Subject: [PATCH] fix(start-work): prefer nested session plan refs --- src/hooks/start-work/index.test.ts | 49 +++++++++++++++++++ src/hooks/start-work/session-plan-affinity.ts | 26 ++++++++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/hooks/start-work/index.test.ts b/src/hooks/start-work/index.test.ts index ff715e868..70b25596e 100644 --- a/src/hooks/start-work/index.test.ts +++ b/src/hooks/start-work/index.test.ts @@ -281,6 +281,55 @@ You are starting a Sisyphus work session. expect(state?.active_plan).toBe(newPlanPath) }) + test("should still find nested plan references when direct input fields contain a different plan path", async () => { + // given - direct path points to plan-a but nested serialized input also references newer plan-b + const plansDir = join(testDir, ".sisyphus", "plans") + mkdirSync(plansDir, { recursive: true }) + + const planAPath = join(plansDir, "plan-a.md") + const planBPath = join(plansDir, "plan-b.md") + writeFileSync(planAPath, "# Plan A\n- [ ] Task A") + writeFileSync(planBPath, "# Plan B\n- [ ] Task B") + + const hook = createStartWorkHook({ + directory: testDir, + client: { + session: { + messages: async () => ({ + data: [ + { + parts: [ + { + input: { + path: `Legacy reference ${planAPath}`, + metadata: { + selectedPlan: `Current reference ${planBPath}`, + }, + }, + }, + ], + }, + ], + }), + }, + }, + } as Parameters[0]) + const output = { + parts: [{ type: "text", text: createStartWorkPrompt() }], + } + + // when + await hook["chat.message"]( + { sessionID: "session-123" }, + output, + ) + + // then - latest nested reference should still be discoverable and selected + const state = readBoulderState(testDir) + expect(state?.active_plan).toBe(planBPath) + expect(output.parts[0].text).toContain("plan-b") + }) + test("should replace $SESSION_ID placeholder", async () => { // given - hook and message with placeholder const hook = createStartWorkHook(createMockPluginInput()) diff --git a/src/hooks/start-work/session-plan-affinity.ts b/src/hooks/start-work/session-plan-affinity.ts index 9ba14c291..5de2e1162 100644 --- a/src/hooks/start-work/session-plan-affinity.ts +++ b/src/hooks/start-work/session-plan-affinity.ts @@ -29,20 +29,36 @@ function extractPlanPathsFromText(directory: string, text: string): string[] { return matches.map((match) => normalizePlanPath(directory, match)) } +function extractPlanPathsFromValue(directory: string, value: unknown): string[] { + if (typeof value === "string") { + return extractPlanPathsFromText(directory, value) + } + + if (Array.isArray(value)) { + return value.flatMap((item) => extractPlanPathsFromValue(directory, item)) + } + + if (value && typeof value === "object") { + return Object.values(value).flatMap((item) => extractPlanPathsFromValue(directory, item)) + } + + return [] +} + function extractPlanPathsFromInput(directory: string, input: Record | undefined): string[] { if (!input) { return [] } + const nestedCandidates = Object.entries(input) + .filter(([key]) => key !== "filePath" && key !== "path" && key !== "file") + .flatMap(([, value]) => extractPlanPathsFromValue(directory, value)) + const directCandidates = [input.filePath, input.path, input.file] .filter((value): value is string => typeof value === "string") .flatMap((value) => extractPlanPathsFromText(directory, value)) - if (directCandidates.length > 0) { - return directCandidates - } - - return extractPlanPathsFromText(directory, JSON.stringify(input)) + return [...new Set([...nestedCandidates, ...directCandidates])] } export async function findRecentSessionPlanPath(input: {