From a8ae58df199c28ba3337237a47118d04cc6077a7 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Tue, 7 Apr 2026 05:02:06 -0400 Subject: [PATCH] fix(core): make follow-up execution aware of the saved plan file Users launching a follow-up implementation session now get an explicit plan-file path and instruction to read it first, so execution stays aligned with the canonical plan even when copied plan text drifts. --- .../opencode/src/kilocode/plan-followup.ts | 26 +++++++++-------- .../test/kilocode/plan-followup.test.ts | 28 ++++++++++++++++--- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/kilocode/plan-followup.ts b/packages/opencode/src/kilocode/plan-followup.ts index 70cdab89fd6..869fcc3377e 100644 --- a/packages/opencode/src/kilocode/plan-followup.ts +++ b/packages/opencode/src/kilocode/plan-followup.ts @@ -273,20 +273,24 @@ export namespace PlanFollowup { Todo.get(input.sessionID), ]) - const sections = [`Implement the following plan:\n\n${input.plan}`] - - if (handover) { - sections.push(`## Handover from Planning Session\n\n${handover}`) - } - - const todoList = formatTodos(todos) - if (todoList) { - sections.push(`## Todo List\n\n${todoList}`) - } - await Instance.provide({ directory: session.directory, fn: async () => { + const file = Session.plan(session) + const sections = [ + `Plan file: ${file}\nRead this file first and treat it as the source of truth for implementation.`, + `Implement the following plan:\n\n${input.plan}`, + ] + + if (handover) { + sections.push(`## Handover from Planning Session\n\n${handover}`) + } + + const todoList = formatTodos(todos) + if (todoList) { + sections.push(`## Todo List\n\n${todoList}`) + } + const next = await Session.create({}) await inject({ sessionID: next.id, diff --git a/packages/opencode/test/kilocode/plan-followup.test.ts b/packages/opencode/test/kilocode/plan-followup.test.ts index 5cb75675bc8..70b4bc73748 100644 --- a/packages/opencode/test/kilocode/plan-followup.test.ts +++ b/packages/opencode/test/kilocode/plan-followup.test.ts @@ -449,6 +449,7 @@ describe("plan follow-up", () => { if (!newSessionID || !next) throw new Error("expected follow-up session") expect(next.id).toBe(newSessionID) expect(next.parentID).toBeUndefined() + const planPath = Session.plan(await Session.get(seeded.sessionID)) const messages = await Session.messages({ sessionID: newSessionID }) const user = messages.find((item) => item.info.role === "user") expect(user?.info.role).toBe("user") @@ -461,6 +462,7 @@ describe("plan follow-up", () => { expect(part?.type).toBe("text") if (!part || part.type !== "text") throw new Error("expected text part") expect(part.text).toContain("Implement the following plan:") + expect(part.text).toContain(`Plan file: ${planPath}`) expect(part.text).toContain("1. Add API\n2. Add tests") expect(part.text).toContain("## Handover from Planning Session") expect(part.text).toContain("Found REST endpoints in src/api.ts") @@ -479,6 +481,7 @@ describe("plan follow-up", () => { test("ask - creates a new session in the planning session directory when the current instance differs", () => withInstance(async () => { + await using other = await tmpdir({ git: true }) const get = spyOn(Agent, "get").mockImplementation(async () => undefined as any) const modelSpy = spyOn(Provider, "getModel").mockResolvedValue(fakeModel) const llmSpy = spyOn(LLM, "stream").mockResolvedValue({ @@ -492,15 +495,17 @@ describe("plan follow-up", () => { }, } - const dir = path.join(Instance.directory, "worktrees", "feature") - await fs.mkdir(dir, { recursive: true }) + const dir = other.path const seeded = await Instance.provide({ directory: dir, fn: async () => seed({ text: "1. Add API\n2. Add tests" }), }) - const before = await sessions() + const before = await Instance.provide({ + directory: dir, + fn: async () => sessions(), + }) const pending = PlanFollowup.ask({ sessionID: seeded.sessionID, messages: seeded.messages, @@ -517,16 +522,31 @@ describe("plan follow-up", () => { }) await expect(pending).resolves.toBe("break") + const after = await Instance.provide({ + directory: dir, + fn: async () => sessions(), + }) - const after = await sessions() const prev = new Set(before.map((item) => item.id)) const added = after.filter((item) => !prev.has(item.id)) expect(added).toHaveLength(1) const next = added[0] + if (!next) throw new Error("expected follow-up session") expect(next?.directory).toBe(dir) expect(next?.parentID).toBeUndefined() if (next) { + const planPath = await Instance.provide({ + directory: dir, + fn: async () => Session.plan(await Session.get(seeded.sessionID)), + }) + const messages = await Session.messages({ sessionID: next.id }) + const user = messages.find((item) => item.info.role === "user") + if (!user || user.info.role !== "user") throw new Error("expected user message") + const part = user.parts.find((item) => item.type === "text") + if (!part || part.type !== "text") throw new Error("expected text part") + expect(part.text).toContain(`Plan file: ${planPath}`) + SessionPrompt.cancel(next.id) } }))