From 00d7de8522083f7bf809fdcae89759032ce926d6 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Sun, 23 Aug 2026 09:04:42 -0700 Subject: [PATCH] fix(pi): append-only phase framing so plan transitions keep the prompt cache The context filter stripped delivered framing from mid-history at phase transitions, shifting every later message and invalidating the provider's cached prefix (88 of 119 messages re-billed in the reporter's session). History is now append-only: delivered framing stays, and stale instructions are neutralized by superseding language in the phase templates plus the existing plan-mode-off countermand. Fixes #1380 --- apps/pi-extension/index.ts | 118 +++--------- apps/pi-extension/phase-prompts.test.ts | 244 +++++++++--------------- apps/pi-extension/plannotator.json | 4 +- 3 files changed, 120 insertions(+), 246 deletions(-) diff --git a/apps/pi-extension/index.ts b/apps/pi-extension/index.ts index c09908e8b..6b36649e7 100644 --- a/apps/pi-extension/index.ts +++ b/apps/pi-extension/index.ts @@ -143,10 +143,12 @@ type PersistedPlannotatorState = { /** * One-shot countermand delivered on the first prompt after a planning or - * executing phase returns to idle (#1320). The idle context filter silently - * strips the phase framing, but the model's own plan-mode turns — and any - * blocked-write tool results — stay in history and keep steering it, so the - * end of plan mode must be stated, not just implied by removal. + * executing phase returns to idle (#1320). It is the SOLE mechanism ending + * plan mode in the conversation: delivered framing stays in history untouched + * (#1380 — removing it from mid-history shifted every later message and + * invalidated the provider's cached prefix), so the model's plan-mode steering + * — its own turns, blocked-write tool results, and the framing itself — is + * neutralized by this explicit notice, never by silent removal. */ const PLAN_MODE_OFF_NOTICE = `[PLANNOTATOR - PLAN MODE OFF] Plannotator plan mode has ended. Disregard all earlier Plannotator planning or execution instructions from this session: the planning restrictions (markdown-only writes, plan submission for review) and the execution checklist protocol ([DONE:n] markers) no longer apply, and the plan-submission tool is no longer available. Full tool access is restored — respond and use tools normally. If the user wants planning again, they will re-enable plan mode.`; @@ -1388,14 +1390,13 @@ export default function plannotator(pi: ExtensionAPI): void { if (phase !== "planning" && phase !== "executing") { // Idle injects nothing (#1269) — with one exception: the first // prompt after a planning/executing → idle transition delivers a - // one-shot plan-mode-off countermand (#1320). The idle filter - // strips the phase framing silently, but the model's own plan-mode - // turns and blocked-write tool results remain in history and keep - // steering it, so the end of plan mode must be said out loud. - // Cache-wise this is free: the notice is a conversation-suffix - // append at a boundary where stripping the framing has already - // invalidated the cached prefix. Fresh idle sessions never arm the - // latch and keep their byte-stable prefix. + // one-shot plan-mode-off countermand (#1320). Delivered framing + // stays in history (#1380), so this notice is what ends plan mode: + // the model's plan-mode turns, blocked-write tool results, and the + // framing itself keep steering it until the end is said out loud. + // Cache-wise the notice is free unconditionally — a pure + // conversation-suffix append on a prefix nothing else perturbs. + // Fresh idle sessions never arm the latch and inject nothing. if (phase !== "idle" || !idleNoticePending) return; idleNoticePending = false; persistState(); @@ -1513,79 +1514,20 @@ Mark completed steps with [DONE:n] in your response.` }; }); - // Keep plannotator conversation messages coherent with the current phase. - // While idle, everything plannotator injected is filtered out — except the - // newest plan-mode-off notice (details.phase === "idle"), which is the - // countermand for the framing this very filter removes (#1320); stripping - // it too would re-create the silent-removal bug it exists to fix. - // During a phase, only the newest framing for the CURRENT phase survives: - // framing from other phases or earlier cycles is dropped (stale planning - // rules cannot leak into execution), along with todo-status messages that - // predate the current cycle's framing. The filter is deterministic within a - // phase, so it never perturbs the provider's cached prefix mid-phase; the - // only mid-history changes happen at phase transitions. - pi.on("context", async (event) => { - if (phase === "idle") { - // Anchor search mirrors the per-phase logic below: the newest idle - // framing (the plan-mode-off notice) survives, every other injected - // message is stripped. Deterministic across idle turns, so the - // filter itself never perturbs the provider's cached prefix while - // idle — sessions that never entered plan mode filter nothing. - let idleAnchor = -1; - for (let i = event.messages.length - 1; i >= 0; i--) { - const msg = event.messages[i] as { customType?: string; details?: unknown }; - if ( - msg.customType === "plannotator-framing" && - (msg.details as { phase?: string } | undefined)?.phase === "idle" - ) { - idleAnchor = i; - break; - } - } - return { - messages: event.messages.filter((m, index) => { - const msg = m as { customType?: string; role?: string; content?: unknown }; - if (msg.customType === "plannotator-framing") return index === idleAnchor; - if (msg.customType === "plannotator-context") return false; - if (msg.role !== "user") return true; - - const content = msg.content; - if (typeof content === "string") { - return !content.includes("[PLANNOTATOR -"); - } - if (Array.isArray(content)) { - return !content.some( - (c) => - c.type === "text" && - (c as { text?: string }).text?.includes("[PLANNOTATOR -"), - ); - } - return true; - }), - }; - } - - let anchor = -1; - for (let i = event.messages.length - 1; i >= 0; i--) { - const msg = event.messages[i] as { customType?: string; details?: unknown }; - if ( - msg.customType === "plannotator-framing" && - (msg.details as { phase?: string } | undefined)?.phase === phase - ) { - anchor = i; - break; - } - } - - return { - messages: event.messages.filter((m, index) => { - const msg = m as { customType?: string }; - if (msg.customType === "plannotator-framing") return index === anchor; - if (msg.customType === "plannotator-context") return anchor === -1 || index > anchor; - return true; - }), - }; - }); + // There is deliberately NO "context" handler (#1380). One existed here and + // stripped plannotator-injected messages at phase transitions; Pi applies a + // context handler's result only to the outgoing LLM request (the runner + // structuredClones history and transformContext shapes the request in + // streamAssistantResponse), but the provider's prompt cache keys on the + // exact request prefix, so removing an already-sent mid-history message + // shifted every later message and re-billed the whole tail as uncached + // input (the reporter measured 88 of 119 messages invalidated on one plan + // completion). The conversation is append-only instead: delivered framing + // and todo snapshots stay in history for the life of the session, and + // stale instructions are neutralized by countermands — the executing + // framing supersedes planning, and PLAN_MODE_OFF_NOTICE supersedes both — + // which models follow by recency. Compaction remains the one boundary that + // rewrites history, and it invalidates the provider cache by itself. // Track execution progress pi.on("turn_end", async (event, ctx) => { @@ -1810,8 +1752,10 @@ Mark completed steps with [DONE:n] in your response.` // Compaction summarizes conversation history and can swallow the delivered // framing message (custom messages are ordinary compactable messages), so // reopen the latch: the next prompt re-delivers the phase framing. If the - // framing survived in the kept tail, the context filter keeps only the - // newest copy, so re-delivery never duplicates. + // framing survived in the kept tail, re-delivery duplicates it — accepted + // (#1380): the copies are identical instructions, the newest governs, and + // compaction already invalidated the cached prefix, so appending a fresh + // copy costs nothing while removing the survivor would cost the cache. pi.on("session_compact", async () => { if (phase !== "planning" && phase !== "executing") return; framingDelivered = false; diff --git a/apps/pi-extension/phase-prompts.test.ts b/apps/pi-extension/phase-prompts.test.ts index 8aef0e3c4..e96b6eab3 100644 --- a/apps/pi-extension/phase-prompts.test.ts +++ b/apps/pi-extension/phase-prompts.test.ts @@ -154,16 +154,6 @@ async function startAgent( return results[0] as PromptResult; } -async function filterContext( - runtime: ReturnType, - context: ReturnType, - messages: ContextMessage[], -): Promise { - const results = await runtime.run("context", context, { messages }); - const result = results[0] as { messages?: ContextMessage[] } | undefined; - return result?.messages; -} - function executingContext( cwd: string, options: { framingDelivered?: boolean } = {}, @@ -189,19 +179,6 @@ function templateWarnings(context: ReturnType): Array<{ me return context.notifications.filter((n) => n.level === "warning" && n.message.includes("unknown template variables")); } -const framingMessage = (phase: string, content = `framing-${phase}`): ContextMessage => ({ - role: "custom", - customType: "plannotator-framing", - content, - details: { phase }, -}); - -const todoMessage = (content = "todo"): ContextMessage => ({ - role: "custom", - customType: "plannotator-context", - content, -}); - describe("Plannotator phase framing messages", () => { test("before_agent_start never returns a systemPrompt in any phase", async () => { const cwd = makeWorkspace(); @@ -791,155 +768,108 @@ describe("Plannotator plan-mode-off countermand (#1320)", () => { }); }); -describe("Plannotator context filtering", () => { - test("idle filters out all plannotator-injected messages", async () => { - const cwd = makeWorkspace(); - const runtime = createRuntime(); - const context = createContext({ cwd }); - await runtime.run("session_start", context); - - const kept = await filterContext(runtime, context, [ - { role: "user", content: "real question" }, - framingMessage("planning"), - todoMessage(), - { role: "user", content: "[PLANNOTATOR - PLANNING PHASE] legacy injected" }, - { role: "assistant", content: "answer" }, - ]); +describe("Plannotator append-only conversation (#1380)", () => { + // Pi applies "context" handler results only to the outgoing LLM request, + // but the provider prompt cache keys on the exact request prefix, so a + // handler that changes its verdict on an already-sent message re-bills the + // whole tail as uncached input. requestView models Pi's transformContext: + // handlers shape the request when present, otherwise it IS the history. + async function requestView( + runtime: ReturnType, + context: ReturnType, + history: ContextMessage[], + ): Promise { + const results = await runtime.run("context", context, { messages: history }); + for (const result of results) { + const shaped = (result as { messages?: ContextMessage[] } | undefined)?.messages; + if (shaped) return shaped; + } + return history; + } - expect(kept?.map((m) => m.content)).toEqual(["real question", "answer"]); - }); + function toInjected(result: PromptResult): ContextMessage { + if (!result?.message) throw new Error("expected an injected message"); + return { + role: "custom", + customType: result.message.customType, + content: result.message.content, + details: result.message.details, + }; + } - test("executing drops stale planning framing and keeps only the current framing", async () => { + test("the outgoing request stays prefix-stable across planning, executing, and back to idle", async () => { + // The #1380 regression: the old context filter stripped delivered + // framing from mid-history at phase transitions, shifting every later + // message and invalidating the provider's cached prefix (88 of 119 + // messages re-billed in the reporter's session). Any reintroduced + // handler that reshapes already-sent history fails the prefix + // comparisons below. const cwd = makeWorkspace(); writeFileSync(join(cwd, "PLAN.md"), "# Plan\n\n- [ ] Step one\n", "utf-8"); const runtime = createRuntime(); - const context = executingContext(cwd); - await runtime.run("session_start", context); - - const kept = await filterContext(runtime, context, [ - framingMessage("planning"), - { role: "user", content: "please plan" }, - todoMessage("stale todo from an earlier cycle"), - framingMessage("executing", "current executing framing"), - todoMessage("current todo"), - { role: "assistant", content: "working" }, - ]); - - expect(kept?.map((m) => m.content)).toEqual([ - "please plan", - "current executing framing", - "current todo", - "working", - ]); - }); - - test("a planning re-entry keeps only the newest planning framing", async () => { - const cwd = makeWorkspace(); - const runtime = createRuntime(); const context = createContext({ cwd }); await runtime.run("session_start", context); - await runtime.commands.get("plannotator-plan-mode")?.handler("", context); - - const kept = await filterContext(runtime, context, [ - framingMessage("planning", "old cycle framing"), - { role: "user", content: "first cycle" }, - framingMessage("planning", "new cycle framing"), - { role: "user", content: "second cycle" }, - ]); - expect(kept?.map((m) => m.content)).toEqual(["first cycle", "new cycle framing", "second cycle"]); - }); - - test("idle keeps only the newest plan-mode-off notice while stripping the rest", async () => { - const cwd = makeWorkspace(); - const runtime = createRuntime(); - const context = createContext({ cwd }); - await runtime.run("session_start", context); - await runtime.commands.get("plannotator-plan-mode")?.handler("", context); - await runtime.commands.get("plannotator-plan-mode")?.handler("", context); // toggle off → idle - - const kept = await filterContext(runtime, context, [ - framingMessage("planning"), - { role: "user", content: "please plan" }, - todoMessage(), - framingMessage("idle", "old off notice from an earlier cycle"), - { role: "user", content: "[PLANNOTATOR - PLANNING PHASE] legacy injected" }, - framingMessage("idle", "current off notice"), - { role: "assistant", content: "understood" }, - ]); - - // The countermand for the framing this filter strips must itself - // survive — dropping it re-creates the silent-removal bug (#1320). - expect(kept?.map((m) => m.content)).toEqual([ - "please plan", - "current off notice", - "understood", - ]); - }); - - test("the idle filter is deterministic and passes untouched sessions through unchanged", async () => { - const cwd = makeWorkspace(); - const runtime = createRuntime(); - const context = createContext({ cwd }); - await runtime.run("session_start", context); + const history: ContextMessage[] = []; + const assertExtends = (next: ContextMessage[], prev: ContextMessage[]): void => { + expect(next.length).toBeGreaterThanOrEqual(prev.length); + for (let i = 0; i < prev.length; i++) { + expect(JSON.stringify(next[i])).toBe(JSON.stringify(prev[i])); + } + }; - // Fresh idle session, no plannotator content: the filter must not - // perturb the message list at all — a byte-stable prefix is what keeps - // the provider prompt cache warm (#922/#1269). - const untouched: ContextMessage[] = [ - { role: "user", content: "hello" }, - { role: "assistant", content: "hi" }, - { role: "toolResult", content: "ls output" }, - ]; - expect(await filterContext(runtime, context, untouched)).toEqual(untouched); - - // With a delivered notice in history, consecutive idle calls yield - // identical output (the anchor is stable), so the filter never flips - // the notice in and out of context between LLM calls. - await runtime.commands.get("plannotator-plan-mode")?.handler("", context); + // Planning turn: framing is delivered and appended like the host would. await runtime.commands.get("plannotator-plan-mode")?.handler("", context); - const withNotice: ContextMessage[] = [ - { role: "user", content: "question" }, - framingMessage("idle", "off notice"), - { role: "assistant", content: "answer" }, - ]; - const first = await filterContext(runtime, context, withNotice); - const second = await filterContext(runtime, context, withNotice); - expect(first).toEqual(second); - expect(first?.map((m) => m.content)).toEqual(["question", "off notice", "answer"]); + history.push({ role: "user", content: "plan this" }); + history.push(toInjected(await startAgent(runtime, context))); + history.push({ role: "assistant", content: "drafted the plan" }); + const planningRequest = await requestView(runtime, context, history); + assertExtends(planningRequest, []); + + // Executing turn (same runtime, phase flipped through the session-tree + // resync): the planning framing already sent upstream must survive. + const executingPath = executingContext(cwd); + await runtime.run("session_tree", executingPath, { newLeafId: "n1", oldLeafId: null }); + history.push({ role: "user", content: "approved, go" }); + history.push(toInjected(await startAgent(runtime, executingPath))); + history.push({ role: "assistant", content: "working [DONE:1]" }); + const executingRequest = await requestView(runtime, executingPath, history); + assertExtends(executingRequest, planningRequest); + + // Back to idle: everything sent during both phases must survive, and + // the plan-mode-off countermand arrives as a pure suffix append. + await runtime.commands.get("plannotator-plan-mode")?.handler("", executingPath); + const notice = await startAgent(runtime, executingPath); + expect(notice?.message?.content).toContain("[PLANNOTATOR - PLAN MODE OFF]"); + history.push(toInjected(notice)); + const idleRequest = await requestView(runtime, executingPath, history); + assertExtends(idleRequest, executingRequest); + expect(idleRequest[idleRequest.length - 1]?.content).toContain("[PLANNOTATOR - PLAN MODE OFF]"); }); - test("entering planning drops the delivered plan-mode-off notice", async () => { + test("phase framing carries the superseding language that replaced removal", async () => { + // With history append-only, stale instructions are neutralized by + // countermand text instead of deletion. Deliberate protocol copy pins: + // trimming these sentences silently reopens the stale-steering hole the + // removed filter used to cover, so they must not drift. const cwd = makeWorkspace(); - const runtime = createRuntime(); - const context = createContext({ cwd }); - await runtime.run("session_start", context); - await runtime.commands.get("plannotator-plan-mode")?.handler("", context); // planning - - // The new planning framing supersedes the old countermand; keeping - // both would tell the model plan mode is simultaneously on and off. - const kept = await filterContext(runtime, context, [ - framingMessage("idle", "off notice"), - { role: "user", content: "plan this" }, - framingMessage("planning", "current planning framing"), - ]); - - expect(kept?.map((m) => m.content)).toEqual(["plan this", "current planning framing"]); - }); - - test("an active phase without its own framing still drops other-phase framing", async () => { - const cwd = makeWorkspace({ phases: { executing: { instructions: null } } }); writeFileSync(join(cwd, "PLAN.md"), "# Plan\n\n- [ ] Step one\n", "utf-8"); - const runtime = createRuntime(); - const context = executingContext(cwd); - await runtime.run("session_start", context); - const kept = await filterContext(runtime, context, [ - framingMessage("planning"), - { role: "user", content: "prompt" }, - todoMessage("current todo"), - ]); + const planningRuntime = createRuntime(); + const planningCtx = createContext({ cwd }); + await planningRuntime.run("session_start", planningCtx); + await planningRuntime.commands.get("plannotator-plan-mode")?.handler("", planningCtx); + const planning = await startAgent(planningRuntime, planningCtx); + expect(planning?.message?.content).toContain( + "supersedes every earlier Plannotator instruction", + ); - expect(kept?.map((m) => m.content)).toEqual(["prompt", "current todo"]); + const executingRuntime = createRuntime(); + const executing = executingContext(cwd); + await executingRuntime.run("session_start", executing); + const framing = await startAgent(executingRuntime, executing); + expect(framing?.message?.content).toContain( + "supersedes every earlier Plannotator instruction", + ); }); }); diff --git a/apps/pi-extension/plannotator.json b/apps/pi-extension/plannotator.json index fe8f662b4..a010b2093 100644 --- a/apps/pi-extension/plannotator.json +++ b/apps/pi-extension/plannotator.json @@ -9,10 +9,10 @@ "plannotator_submit_plan" ], "statusLabel": "⏸ plan", - "instructions": "[PLANNOTATOR - PLANNING PHASE]\nYou are in plan mode. You MUST NOT make any changes to the codebase — no edits, no commits, no installs, no destructive commands. During planning you may only write or edit markdown files (.md, .mdx) inside the working directory.\n\nDo not run destructive commands (rm, git push, npm install, etc.) — focus on reading and exploring the codebase. Web fetching is fine.\n\n## Iterative Planning Workflow\n\nYou are pair-planning with the user. Explore the code to build context, then write your findings into a markdown plan file as you go. The plan starts as a rough skeleton and gradually becomes the final plan.\n\n### Picking a plan file\n\nChoose a descriptive filename for your plan. Convention: `PLAN.md` at the repo root for a single focused plan, or `plans/.md` for projects that keep multiple plans. Reuse the same filename across revisions of the same plan so version history links up.\n\n### The Loop\n\nRepeat this cycle until the plan is complete:\n\n1. **Explore** — Use the available reading, searching, and command tools to understand the codebase. Actively search for existing functions, utilities, and patterns that can be reused — avoid proposing new code when suitable implementations already exist.\n2. **Update the plan file** — After each discovery, immediately capture what you learned in the plan. Don't wait until the end. Use the available file tools to create the initial draft and make targeted updates.\n3. **Ask the user** — When you hit an ambiguity or decision you can't resolve from code alone, ask. Then go back to step 1.\n\n### First Turn\n\nStart by quickly scanning key files to form an initial understanding of the task scope. Then write a skeleton plan (headers and rough notes) and ask the user your first round of questions. Don't explore exhaustively before engaging the user.\n\n### Asking Good Questions\n\n- Never ask what you could find out by reading the code.\n- Batch related questions together.\n- Focus on things only the user can answer: requirements, preferences, tradeoffs, edge-case priorities.\n- Scale depth to the task — a vague feature request needs many rounds; a focused bug fix may need one or none.\n\n### Plan File Structure\n\nYour plan file should use markdown with clear sections:\n- **Context** — Why this change is being made: the problem, what prompted it, the intended outcome.\n- **Approach** — Your recommended approach only, not all alternatives considered.\n- **Files to modify** — List the critical file paths that will be changed.\n- **Reuse** — Reference existing functions and utilities you found, with their file paths.\n- **Steps** — Implementation checklist:\n - [ ] Step 1 description\n - [ ] Step 2 description\n- **Verification** — How to test the changes end-to-end (run the code, run tests, manual checks).\n\nKeep the plan concise enough to scan quickly, but detailed enough to execute effectively.\n\n### When to Submit\n\nYour plan is ready when you've addressed all ambiguities and it covers: what to change, which files to modify, what existing code to reuse, and how to verify. Call plannotator_submit_plan with the path to your plan file to submit for review.\n\n### Revising After Feedback\n\nWhen the user denies a plan with feedback:\n1. Read the plan file to see the current plan.\n2. Make targeted changes addressing the feedback — do NOT rewrite the entire file.\n3. Call plannotator_submit_plan again with the same filePath to resubmit.\n\n### Ending Your Turn\n\nYour turn should only end by either:\n- Asking the user a question to gather more information.\n- Calling plannotator_submit_plan when the plan is ready for review.\n\nDo not end your turn without doing one of these two things." + "instructions": "[PLANNOTATOR - PLANNING PHASE]\nYou are in plan mode. This supersedes every earlier Plannotator instruction in this conversation, including any earlier notice that plan mode was off. You MUST NOT make any changes to the codebase — no edits, no commits, no installs, no destructive commands. During planning you may only write or edit markdown files (.md, .mdx) inside the working directory.\n\nDo not run destructive commands (rm, git push, npm install, etc.) — focus on reading and exploring the codebase. Web fetching is fine.\n\n## Iterative Planning Workflow\n\nYou are pair-planning with the user. Explore the code to build context, then write your findings into a markdown plan file as you go. The plan starts as a rough skeleton and gradually becomes the final plan.\n\n### Picking a plan file\n\nChoose a descriptive filename for your plan. Convention: `PLAN.md` at the repo root for a single focused plan, or `plans/.md` for projects that keep multiple plans. Reuse the same filename across revisions of the same plan so version history links up.\n\n### The Loop\n\nRepeat this cycle until the plan is complete:\n\n1. **Explore** — Use the available reading, searching, and command tools to understand the codebase. Actively search for existing functions, utilities, and patterns that can be reused — avoid proposing new code when suitable implementations already exist.\n2. **Update the plan file** — After each discovery, immediately capture what you learned in the plan. Don't wait until the end. Use the available file tools to create the initial draft and make targeted updates.\n3. **Ask the user** — When you hit an ambiguity or decision you can't resolve from code alone, ask. Then go back to step 1.\n\n### First Turn\n\nStart by quickly scanning key files to form an initial understanding of the task scope. Then write a skeleton plan (headers and rough notes) and ask the user your first round of questions. Don't explore exhaustively before engaging the user.\n\n### Asking Good Questions\n\n- Never ask what you could find out by reading the code.\n- Batch related questions together.\n- Focus on things only the user can answer: requirements, preferences, tradeoffs, edge-case priorities.\n- Scale depth to the task — a vague feature request needs many rounds; a focused bug fix may need one or none.\n\n### Plan File Structure\n\nYour plan file should use markdown with clear sections:\n- **Context** — Why this change is being made: the problem, what prompted it, the intended outcome.\n- **Approach** — Your recommended approach only, not all alternatives considered.\n- **Files to modify** — List the critical file paths that will be changed.\n- **Reuse** — Reference existing functions and utilities you found, with their file paths.\n- **Steps** — Implementation checklist:\n - [ ] Step 1 description\n - [ ] Step 2 description\n- **Verification** — How to test the changes end-to-end (run the code, run tests, manual checks).\n\nKeep the plan concise enough to scan quickly, but detailed enough to execute effectively.\n\n### When to Submit\n\nYour plan is ready when you've addressed all ambiguities and it covers: what to change, which files to modify, what existing code to reuse, and how to verify. Call plannotator_submit_plan with the path to your plan file to submit for review.\n\n### Revising After Feedback\n\nWhen the user denies a plan with feedback:\n1. Read the plan file to see the current plan.\n2. Make targeted changes addressing the feedback — do NOT rewrite the entire file.\n3. Call plannotator_submit_plan again with the same filePath to resubmit.\n\n### Ending Your Turn\n\nYour turn should only end by either:\n- Asking the user a question to gather more information.\n- Calling plannotator_submit_plan when the plan is ready for review.\n\nDo not end your turn without doing one of these two things." }, "executing": { - "instructions": "[PLANNOTATOR - EXECUTING PLAN]\nThe planning phase is over: the plan has been approved and planning-phase restrictions no longer apply. Full tool access is enabled. Execute the plan from ${planFilePath}.\n\nRemaining steps:\n${todoList}\n\nExecute each remaining step in order. After completing a step, include [DONE:n] in your response where n is the step number. Updated todo status arrives in the conversation as steps are completed." + "instructions": "[PLANNOTATOR - EXECUTING PLAN]\nThe planning phase is over: the plan has been approved and planning-phase restrictions no longer apply. This supersedes every earlier Plannotator instruction in this conversation, including the planning-phase rules above. Full tool access is enabled. Execute the plan from ${planFilePath}.\n\nRemaining steps:\n${todoList}\n\nExecute each remaining step in order. After completing a step, include [DONE:n] in your response where n is the step number. Updated todo status arrives in the conversation as steps are completed." } } }