From faf4890b088ccb93c5f28477713250b68be480c7 Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 15 Jul 2026 12:35:29 +0000 Subject: [PATCH] fix(plan-mode): anchored re-entry on the new request The re-entry system prompt led with "Read the existing plan" and its "different task -> overwrite it" step contradicted the planExists guidance ("leave that plan in place and start a fresh file"). Weak models fixated on reconciling the incomplete old plan and dropped the new request entirely. - Rewrote the Re-entry procedure in plan-mode-active.md to treat the new request as the primary input and the old plan as reference only, and to fold corrections for unfinished old work INTO the new plan rather than substituting them for the new request. - Aligned the "different task" branch with the planExists section (fresh file, no overwrite), removing the contradiction. - Added reentry-prompt.test.ts asserting the anchoring contract and the absence of the contradiction. Fixes #5576 --- packages/coding-agent/CHANGELOG.md | 1 + .../src/plan-mode/reentry-prompt.test.ts | 41 +++++++++++++++++++ .../src/prompts/system/plan-mode-active.md | 11 +++-- 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 packages/coding-agent/src/plan-mode/reentry-prompt.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 71e86facb..287b92158 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -16,6 +16,7 @@ - Fixed Bash internal URLs remaining unresolved when used as unquoted arguments inside command substitutions ([#5535](https://github.com/can1357/oh-my-pi/issues/5535)). - Fixed the built-in `fd` printing `fd: Broken pipe (os error 32)` when a downstream pipeline reader exited early (e.g. `fd … | head`); it now exits silently with 141 (128+SIGPIPE), matching real fd. - Fixed prewalk repeatedly continuing after a bash-only task such as `commit` had already completed ([#5551](https://github.com/can1357/oh-my-pi/issues/5551)). +- Fixed plan-mode re-entry dropping a new plan request when a prior plan artifact existed: the re-entry prompt led with the old plan and contradicted the plan-file guidance, so weak models only reconciled the incomplete previous plan. Re-entry now anchors on the new request and folds any old-plan corrections into it ([#5576](https://github.com/can1357/oh-my-pi/issues/5576)). ## [16.5.2] - 2026-07-14 diff --git a/packages/coding-agent/src/plan-mode/reentry-prompt.test.ts b/packages/coding-agent/src/plan-mode/reentry-prompt.test.ts new file mode 100644 index 000000000..2566c799e --- /dev/null +++ b/packages/coding-agent/src/plan-mode/reentry-prompt.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "bun:test"; +import { prompt } from "@oh-my-pi/pi-utils"; +import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" }; + +const BASE = { + planFilePath: "local://old-feature-plan.md", + askToolName: "ask", + writeToolName: "write", + editToolName: "edit", + isHashlineEditMode: false, + iterative: false, +} as const; + +function render(overrides: { reentry: boolean; planExists: boolean }): string { + return prompt.render(planModeActivePrompt, { ...BASE, ...overrides }); +} + +describe("plan-mode re-entry prompt", () => { + it("only emits the Re-entry section when re-entering", () => { + expect(render({ reentry: false, planExists: true })).not.toContain("## Re-entry"); + expect(render({ reentry: true, planExists: true })).toContain("## Re-entry"); + }); + + it("anchors the turn on the new request, not the old plan", () => { + const rendered = render({ reentry: true, planExists: true }); + const reentry = rendered.slice(rendered.indexOf("## Re-entry")); + // The new request is the primary input; the old plan is reference only. + expect(reentry).toMatch(/NEW request[\s\S]*primary input/); + // Corrections to an incomplete old plan must be folded into the new plan, + // never substituted for it (the reported failure: dropping the new request). + expect(reentry).toMatch(/combine, never substitute/); + }); + + it("does not contradict the planExists guidance on a different task", () => { + const rendered = render({ reentry: true, planExists: true }); + // planExists branch: different task -> leave old plan, write a fresh file. + expect(rendered).toContain("leave that plan in place and start a fresh"); + // Re-entry must agree; the old "Different task -> overwrite it" directive is gone. + expect(rendered).not.toMatch(/[Dd]ifferent task → overwrite/); + }); +}); diff --git a/packages/coding-agent/src/prompts/system/plan-mode-active.md b/packages/coding-agent/src/prompts/system/plan-mode-active.md index 5056ff0be..0d25a8f94 100644 --- a/packages/coding-agent/src/prompts/system/plan-mode-active.md +++ b/packages/coding-agent/src/prompts/system/plan-mode-active.md @@ -48,11 +48,14 @@ Every question MUST change the plan or settle a load-bearing choice. Batch them. {{#if reentry}} ## Re-entry +You are re-entering plan mode with a NEW request. That new request is the primary input and MUST be planned; the existing plan is only reference. You NEVER narrow the turn to reconciling the old plan and drop the new request. + -1. Read the existing plan. -2. Compare the new request against it. -3. Different task → overwrite it. Same task continuing → update it and delete outdated sections. -4. Call `resolve` with `action: "apply"` and `extra: { title }` when complete. +1. Read the new request and make it the plan you build this turn. +2. Read the existing plan as reference only. +3. Same task continuing → update that plan with `{{editToolName}}` and delete outdated sections. Different task → leave that plan in place and write a fresh `local://-plan.md` for the new request. +4. If the old plan has unfinished or broken work the new request depends on, fold those corrections INTO the new plan — combine, never substitute the old fix for the new request. +5. Call `resolve` with `action: "apply"` and `extra: { title }` when the new request is decision-complete. {{/if}}