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
This commit is contained in:
roboomp
2026-07-15 12:35:29 +00:00
parent 2134526b7d
commit faf4890b08
3 changed files with 49 additions and 4 deletions
+1
View File
@@ -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
@@ -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/);
});
});
@@ -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.
<procedure>
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://<slug>-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.
</procedure>
{{/if}}