fix: seed rejected PROMPT.md on replan so Plan Review can converge
Plan Review REVISE previously fed feedback without the rejected plan body, so triage rewrote from title/description and looped. Seed the draft for surgical revision, use reviewType spec for the pre-execution gate, and tighten planner/reviewer prompts toward blocking-only REVISE with concrete edits.
This commit is contained in:
@@ -37,6 +37,9 @@ describe("resolveAgentPrompt", () => {
|
||||
const result = resolveAgentPrompt("reviewer");
|
||||
expect(result).toBeTruthy();
|
||||
expect(result).toContain("independent code and plan reviewer");
|
||||
// FNXC:PlanReviewReplan 2026-07-15-11:15: convergence guidance for Plan Review REVISE thrash.
|
||||
expect(result).toContain("Spec / Plan Review Convergence");
|
||||
expect(result).toContain("concrete PROMPT.md edit");
|
||||
});
|
||||
|
||||
it("returns the correct built-in prompt for merger when no config provided", () => {
|
||||
|
||||
@@ -657,6 +657,12 @@ If the task targets a different task ID (audit, forensic walk, historical reconc
|
||||
<!-- Frontend UX criteria are applied deterministically by packages/core/src/frontend-ux-policy.ts and mirror the "frontend-ux-design" reviewer persona in packages/core/src/types.ts. -->`;;
|
||||
|
||||
// FN-6235: single source for the built-in reviewer policy; the engine REVIEWER_SYSTEM_PROMPT duplicate was removed.
|
||||
/*
|
||||
FNXC:PlanReviewReplan 2026-07-15-11:15:
|
||||
Built-in reviewer prompt includes Spec/Plan Review Convergence rules so REVISE stays
|
||||
blocking-only with surgical fix lists, reducing planner↔Plan-Review thrash (paired with
|
||||
triage seeding existing PROMPT.md on needs-replan and reviewType "spec" for the triage gate).
|
||||
*/
|
||||
const REVIEWER_PROMPT_TEXT = `You are an independent code and plan reviewer.
|
||||
|
||||
## Your Role
|
||||
@@ -786,6 +792,15 @@ Concrete examples:
|
||||
- [Optional improvements, not blocking]
|
||||
\`\`\`
|
||||
|
||||
## Spec / Plan Review Convergence
|
||||
|
||||
Specs and pre-execution Plan Review share this gate. Prefer **APPROVE** / **APPROVE_WITH_NOTES** when the plan is executable enough for an agent to implement. Put optional polish only under **Suggestions**.
|
||||
|
||||
When you must **REVISE**:
|
||||
- List each blocking issue as a concrete PROMPT.md edit (which section, what to add/change/remove).
|
||||
- Do not demand a full rewrite unless the approach is fundamentally wrong (**RETHINK**).
|
||||
- Prefer fixing local PROMPT.md defects in-session when you have write tools, then **APPROVE**, instead of bouncing the task through another full replan cycle.
|
||||
|
||||
## Spec Review — Undersplit Task Detection
|
||||
|
||||
When reviewing specs, assess whether the task should have been broken into subtasks. The bar for splitting is high — most tasks should remain whole. Coordination overhead (worktrees, dependency wiring, merge sequencing) is real, so splitting must clearly pay for itself.
|
||||
|
||||
@@ -134,7 +134,7 @@ describe("Plan Review unavailable retry", () => {
|
||||
task.id,
|
||||
0,
|
||||
"PROMPT.md",
|
||||
"plan",
|
||||
"spec",
|
||||
prompt,
|
||||
undefined,
|
||||
expect.objectContaining({ taskId: task.id }),
|
||||
@@ -255,7 +255,7 @@ describe("Plan Review unavailable retry", () => {
|
||||
task.id,
|
||||
0,
|
||||
"PROMPT.md",
|
||||
"plan",
|
||||
"spec",
|
||||
prompt,
|
||||
undefined,
|
||||
expect.objectContaining({ taskId: task.id }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Settings, Task, TaskDetail, TaskStore } from "@fusion/core";
|
||||
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
@@ -11,6 +11,11 @@ import { TriageProcessor } from "../triage.js";
|
||||
* the planner prompt must be seeded from the most recent Plan Review REVISE output
|
||||
* stored in workflowStepResults — otherwise the planner re-plans with
|
||||
* `feedback: undefined` and regenerates the same rejected plan.
|
||||
*
|
||||
* FNXC:PlanReviewReplan 2026-07-15-11:15:
|
||||
* Also seed the rejected PROMPT.md body so buildSpecificationPrompt uses surgical
|
||||
* revision mode (Existing Specification + Revision Feedback) instead of a full
|
||||
* rewrite from title/description — the main non-convergence failure mode.
|
||||
*/
|
||||
|
||||
const { mockCreateResolvedAgentSession, mockPromptWithFallback } = vi.hoisted(() => ({
|
||||
@@ -155,6 +160,7 @@ describe("triage replan feedback falls back to Plan Review REVISE output", () =>
|
||||
|
||||
it("seeds the planner prompt from the latest plan-review REVISE output when no comment feedback exists", async () => {
|
||||
const reviseOutput = "PLAN-REVIEW-REVISE-MARKER: the plan omits the required migration step and must add it.";
|
||||
const rejectedDraft = "# Existing rejected plan\n\n## Mission\nDo not lose this body during replan.\n";
|
||||
const task = createTask({
|
||||
id: "FN-REPLAN-FEEDBACK-WSR",
|
||||
// No user comments and no "AI spec revision requested" log entry — the only
|
||||
@@ -173,6 +179,7 @@ describe("triage replan feedback falls back to Plan Review REVISE output", () =>
|
||||
],
|
||||
});
|
||||
rootDir = await createRoot(task.id);
|
||||
await writeFile(join(rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), rejectedDraft, "utf-8");
|
||||
const harness = createMutableStore(task);
|
||||
const processor = new TriageProcessor(harness.store, rootDir);
|
||||
|
||||
@@ -188,11 +195,18 @@ describe("triage replan feedback falls back to Plan Review REVISE output", () =>
|
||||
expect(mockPromptWithFallback).toHaveBeenCalled();
|
||||
expect(capturedPrompt).toBeDefined();
|
||||
expect(capturedPrompt).toContain(reviseOutput);
|
||||
// Surgical revision: rejected PROMPT body + feedback, not a fresh respec from title alone.
|
||||
expect(capturedPrompt).toContain("Revise this task");
|
||||
expect(capturedPrompt).toContain("Existing Specification");
|
||||
expect(capturedPrompt).toContain("Do not lose this body during replan");
|
||||
expect(capturedPrompt).toContain("Converge — do not rewrite from scratch");
|
||||
expect(capturedPrompt).not.toContain("Re-specify this task");
|
||||
});
|
||||
|
||||
it("prefers an explicit AI spec revision comment over the workflowStepResults fallback", async () => {
|
||||
const reviseOutput = "PLAN-REVIEW-REVISE-MARKER: stale fallback that must not win.";
|
||||
const explicitFeedback = "EXPLICIT-COMMENT-FEEDBACK: address the auth edge case first.";
|
||||
const rejectedDraft = "# Rejected plan body\n\nKeep this under surgical revision.\n";
|
||||
const task = createTask({
|
||||
id: "FN-REPLAN-FEEDBACK-PRECEDENCE",
|
||||
log: [
|
||||
@@ -214,6 +228,7 @@ describe("triage replan feedback falls back to Plan Review REVISE output", () =>
|
||||
],
|
||||
});
|
||||
rootDir = await createRoot(task.id);
|
||||
await writeFile(join(rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), rejectedDraft, "utf-8");
|
||||
const harness = createMutableStore(task);
|
||||
const processor = new TriageProcessor(harness.store, rootDir);
|
||||
|
||||
@@ -228,5 +243,7 @@ describe("triage replan feedback falls back to Plan Review REVISE output", () =>
|
||||
expect(capturedPrompt).toBeDefined();
|
||||
expect(capturedPrompt).toContain(explicitFeedback);
|
||||
expect(capturedPrompt).not.toContain(reviseOutput);
|
||||
expect(capturedPrompt).toContain("Existing Specification");
|
||||
expect(capturedPrompt).toContain("Keep this under surgical revision");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -313,7 +313,9 @@ describe("buildSpecificationPrompt", () => {
|
||||
expect(prompt).toContain("Revise this task");
|
||||
expect(prompt).toContain("Revision Instructions");
|
||||
expect(prompt).toContain("Existing Specification");
|
||||
expect(prompt).toContain("User Feedback");
|
||||
expect(prompt).toContain("Revision Feedback");
|
||||
expect(prompt).toContain("Converge — do not rewrite from scratch");
|
||||
expect(prompt).toContain("surgical");
|
||||
expect(prompt).toContain(existingPrompt);
|
||||
expect(prompt).toContain(feedback);
|
||||
expect(prompt).toContain("revising an existing task specification");
|
||||
@@ -334,9 +336,10 @@ describe("buildSpecificationPrompt", () => {
|
||||
expect(prompt).toContain("Re-specify this task");
|
||||
expect(prompt).toContain("Re-specification Instructions");
|
||||
expect(prompt).toContain("fresh replacement specification");
|
||||
expect(prompt).toContain("Revision Feedback");
|
||||
expect(prompt).toContain(feedback);
|
||||
expect(prompt).not.toContain("Existing Specification");
|
||||
expect(prompt).toContain("without carrying forward stale assumptions");
|
||||
expect(prompt).toContain("no usable prior PROMPT.md draft");
|
||||
expect(prompt).toContain("Treat the current task title and description as required primary inputs");
|
||||
});
|
||||
|
||||
@@ -1496,7 +1499,7 @@ Planner rewrote mission without the raw request.
|
||||
"FN-PLAN-APPROVE",
|
||||
0,
|
||||
"PROMPT.md",
|
||||
"plan",
|
||||
"spec",
|
||||
expect.any(String),
|
||||
undefined,
|
||||
expect.objectContaining({ taskId: "FN-PLAN-APPROVE" }),
|
||||
@@ -1664,7 +1667,7 @@ Planner rewrote mission without the raw request.
|
||||
taskId,
|
||||
0,
|
||||
"PROMPT.md",
|
||||
"plan",
|
||||
"spec",
|
||||
prompt,
|
||||
undefined,
|
||||
expect.objectContaining({ taskId }),
|
||||
@@ -1742,7 +1745,7 @@ Planner rewrote mission without the raw request.
|
||||
taskId,
|
||||
0,
|
||||
"PROMPT.md",
|
||||
"plan",
|
||||
"spec",
|
||||
prompt,
|
||||
undefined,
|
||||
expect.objectContaining({ taskId }),
|
||||
@@ -1945,7 +1948,7 @@ Planner rewrote mission without the raw request.
|
||||
taskId,
|
||||
0,
|
||||
"PROMPT.md",
|
||||
"plan",
|
||||
"spec",
|
||||
prompt,
|
||||
undefined,
|
||||
expect.objectContaining({ taskId }),
|
||||
@@ -2117,7 +2120,7 @@ Planner rewrote mission without the raw request.
|
||||
taskId,
|
||||
0,
|
||||
"PROMPT.md",
|
||||
"plan",
|
||||
"spec",
|
||||
prompt,
|
||||
undefined,
|
||||
expect.objectContaining({ taskId }),
|
||||
@@ -2438,7 +2441,7 @@ Planner rewrote mission without the raw request.
|
||||
elapsedTaskId,
|
||||
0,
|
||||
"PROMPT.md",
|
||||
"plan",
|
||||
"spec",
|
||||
prompt,
|
||||
undefined,
|
||||
expect.objectContaining({ taskId: elapsedTaskId }),
|
||||
|
||||
@@ -795,6 +795,13 @@ function buildReviewRequest(
|
||||
];
|
||||
|
||||
if (reviewType === "spec") {
|
||||
/*
|
||||
FNXC:PlanReviewReplan 2026-07-15-11:15:
|
||||
Spec/Plan Review REVISE loops burn planner+reviewer turns when feedback is vague or
|
||||
when polish is treated as blocking. Prefer fix-and-APPROVE / Suggestions for non-
|
||||
blocking nits; when REVISE is required, list concrete PROMPT.md edits the planner can
|
||||
apply surgically so the next cycle converges.
|
||||
*/
|
||||
parts.push(
|
||||
"## What to review",
|
||||
"Evaluate this PROMPT.md specification for completeness and quality.",
|
||||
@@ -806,6 +813,12 @@ function buildReviewRequest(
|
||||
"Read relevant source files to verify the spec references real files, functions, and patterns.",
|
||||
"Check that steps have concrete, verifiable outcomes — not vague instructions.",
|
||||
"Ensure testing requirements demand real automated tests with assertions.",
|
||||
"",
|
||||
"## Convergence rules (blocking REVISE budget)",
|
||||
"- Prefer APPROVE or APPROVE_WITH_NOTES when the plan is executable; put polish and optional improvements under **Suggestions** only.",
|
||||
"- Issue REVISE only for blocking defects that would cause the implementor to redo work or violate a hard gate (missing Surface Enumeration / Symptom Verification for bug-class tasks, dangling task-document refs, untestable steps, missing mission, user comments ignored, external-integration evidence gaps when required).",
|
||||
"- When you REVISE, list each blocking fix as a concrete edit the planner can apply to this PROMPT.md (section + what to add/change). Do not request a full rewrite unless the approach is fundamentally wrong (RETHINK).",
|
||||
"- If same-session PROMPT.md repair is available and a fix is local, apply it and APPROVE rather than bouncing to another replan cycle.",
|
||||
);
|
||||
|
||||
// Add user comment coverage check for spec reviews
|
||||
|
||||
@@ -1305,16 +1305,28 @@ export class TriageProcessor {
|
||||
);
|
||||
feedback = feedbackLogEntry?.outcome;
|
||||
|
||||
if (feedbackLogEntry?.action === TRIAGE_STUCK_RESUME_LOG_ACTION) {
|
||||
/*
|
||||
FNXC:Triage 2026-06-27-16:18:
|
||||
Stuck-resume replans must load the existing PROMPT.md draft, or the saved plan task document when PROMPT.md is absent, into buildSpecificationPrompt so `isRevision` is reachable for either persisted planning surface.
|
||||
*/
|
||||
const planningDraft = await this.readNonEmptyPlanningDraft(task.id, "stuck-resume replan seed");
|
||||
existingPrompt = planningDraft?.content;
|
||||
if (!existingPrompt) {
|
||||
feedback = undefined;
|
||||
}
|
||||
/*
|
||||
FNXC:Triage 2026-06-27-16:18:
|
||||
Stuck-resume replans must load the existing PROMPT.md draft, or the saved plan task document when PROMPT.md is absent, into buildSpecificationPrompt so `isRevision` is reachable for either persisted planning surface.
|
||||
|
||||
FNXC:PlanReviewReplan 2026-07-15-11:15:
|
||||
Load the rejected plan on EVERY needs-replan path, not only stuck-resume.
|
||||
Plan Review REVISE previously set feedback but left existingPrompt undefined, so
|
||||
buildSpecificationPrompt took the fresh-respecification branch ("Do not reuse
|
||||
stale PROMPT.md") and rewrote from title/description. That is the main
|
||||
non-convergence loop: surgical REVISE feedback without the rejected plan body
|
||||
causes the planner to invent a new spec, the reviewer finds new gaps, and the
|
||||
cycle repeats until the replan cap. Seed the draft whenever it exists so
|
||||
isRevision mode applies surgical edits against the actual PROMPT.md.
|
||||
*/
|
||||
const replanSeedReason =
|
||||
feedbackLogEntry?.action === TRIAGE_STUCK_RESUME_LOG_ACTION
|
||||
? "stuck-resume replan seed"
|
||||
: "needs-replan revision seed";
|
||||
const planningDraft = await this.readNonEmptyPlanningDraft(task.id, replanSeedReason);
|
||||
existingPrompt = planningDraft?.content;
|
||||
if (feedbackLogEntry?.action === TRIAGE_STUCK_RESUME_LOG_ACTION && !existingPrompt) {
|
||||
feedback = undefined;
|
||||
}
|
||||
|
||||
// Ensure the latest user feedback is always actionable for re-plans.
|
||||
@@ -1348,7 +1360,8 @@ export class TriageProcessor {
|
||||
}
|
||||
|
||||
planLog.log(
|
||||
`${task.id} re-planning with feedback: ${feedback?.slice(0, 100)}...`,
|
||||
`${task.id} re-planning with feedback: ${feedback?.slice(0, 100)}...`
|
||||
+ (existingPrompt ? " (seeded existing PROMPT.md for surgical revision)" : " (no existing draft — fresh respec)"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2243,12 +2256,20 @@ export class TriageProcessor {
|
||||
FNXC:AgentSteering 2026-06-30-13:19:
|
||||
Plan Review receives the uncapped reviewer context so older task comments cannot be silently dropped before the mandatory execution gate evaluates operator requirements.
|
||||
*/
|
||||
/*
|
||||
FNXC:PlanReviewReplan 2026-07-15-11:15:
|
||||
Triage Plan Review gates the full PROMPT.md before execution. Use reviewType "spec"
|
||||
(not per-step "plan") so buildReviewRequest injects the full spec quality checklist
|
||||
(mission, steps, surface enumeration, symptom verification, dangling task docs, …)
|
||||
instead of step-checkbox language that does not match this gate. Inline PROMPT.md
|
||||
repair remains allowed so the reviewer can fix-and-APPROVE instead of REVISE-looping.
|
||||
*/
|
||||
const review = await reviewStep(
|
||||
this.rootDir,
|
||||
task.id,
|
||||
0,
|
||||
"PROMPT.md",
|
||||
"plan",
|
||||
"spec",
|
||||
promptContent,
|
||||
undefined,
|
||||
{
|
||||
@@ -3065,31 +3086,44 @@ When writing PROMPT.md, add this as an explicit requirement under completion doc
|
||||
|
||||
let revisionSection = "";
|
||||
if (isRevision) {
|
||||
/*
|
||||
FNXC:PlanReviewReplan 2026-07-15-11:15:
|
||||
Plan Review REVISE and user re-spec feedback share this path. Label feedback generically
|
||||
(not "User Feedback" only) and force surgical edits: wholesale rewrites from title alone
|
||||
were the non-convergence failure mode (new plan → new reviewer findings → another REVISE).
|
||||
RETHINK-class feedback may still require structural change; REVISE must fix listed issues
|
||||
without inventing new scope.
|
||||
*/
|
||||
revisionSection = `
|
||||
|
||||
## Revision Instructions
|
||||
You are revising an existing task specification based on user feedback.
|
||||
You are revising an existing task specification based on Plan Review or user feedback.
|
||||
|
||||
**Important:** Keep the same overall PROMPT.md structure (headings, sections, format) but improve the content to address the feedback below. Do not drastically change the file structure unless necessary.
|
||||
**Converge — do not rewrite from scratch.**
|
||||
- Keep the same overall PROMPT.md structure (headings, sections, format) unless the feedback explicitly requires a fundamental rethink (RETHINK).
|
||||
- Apply **surgical** edits that fully resolve every blocking issue in the revision feedback below.
|
||||
- Preserve wording, steps, file scope, and acceptance criteria the feedback does not criticize.
|
||||
- Do not expand scope, invent new deliverables, or churn File Scope to "improve" an otherwise approved plan.
|
||||
- After editing, re-check each blocking item so a subsequent Plan Review can APPROVE without a new round of objections.
|
||||
|
||||
## Existing Specification
|
||||
\`\`\`markdown
|
||||
${existingPrompt}
|
||||
\`\`\`
|
||||
|
||||
## User Feedback
|
||||
## Revision Feedback
|
||||
${feedback}
|
||||
|
||||
Please revise the specification above to address this feedback. Write the complete revised PROMPT.md to \`${promptPath}\`.`;
|
||||
Revise the specification above to address this feedback. Write the complete revised PROMPT.md to \`${promptPath}\`.`;
|
||||
} else if (isFreshRespecification) {
|
||||
revisionSection = `
|
||||
|
||||
## Re-specification Instructions
|
||||
You are creating a fresh replacement specification based on user feedback.
|
||||
You are creating a fresh replacement specification based on Plan Review or user feedback (no usable prior PROMPT.md draft is available).
|
||||
|
||||
**Important:** Do not reuse stale PROMPT.md content. Treat the current task title and description as required primary inputs, inspect the codebase, and write a complete new specification that addresses the feedback below.
|
||||
**Important:** Treat the current task title and description as required primary inputs, inspect the codebase, and write a complete new specification that addresses the feedback below. Do not invent requirements beyond the feedback and task description.
|
||||
|
||||
## User Feedback
|
||||
## Revision Feedback
|
||||
${feedback}
|
||||
|
||||
Please write the complete fresh PROMPT.md to \`${promptPath}\`.`;
|
||||
@@ -3163,7 +3197,7 @@ ${task.breakIntoSubtasks ? "- **Break into subtasks:** Yes (user requested)" : "
|
||||
${task.dependencies.length > 0 ? `- **Dependencies:** ${task.dependencies.join(", ")}` : ""}${revisionSection}${subtaskSection}
|
||||
|
||||
## Instructions
|
||||
${isRevision ? "1. Review the existing specification and user feedback carefully\n2. Revise the PROMPT.md to address the feedback while maintaining the structure\n3. Keep `## Original Description` at the top (after title/metadata) with the operator description **verbatim**\n4. Ensure the specification is detailed enough for an AI agent to execute" : isFreshRespecification ? "1. Read the project structure to understand context (package.json, source files, etc.)\n2. Treat the current task title and description as mandatory primary inputs for a new spec\n3. Write a fresh complete PROMPT.md specification to the given path following the format in your system prompt\n4. Include `## Original Description` near the top with the exact Description text above (verbatim)\n5. Address the user feedback without carrying forward stale assumptions from the old spec\n6. Name actual files, functions, and patterns from the codebase — be specific" : "1. Read the project structure to understand context (package.json, source files, etc.)\n2. Write a complete PROMPT.md specification to the given path following the format in your system prompt\n3. Include `## Original Description` immediately after title/`Created`/`Size` with the exact Description text above (verbatim — do not paraphrase)\n4. The specification must be detailed enough for an autonomous AI agent to implement without asking questions\n5. Name actual files, functions, and patterns from the codebase — be specific"}
|
||||
${isRevision ? "1. Read the existing specification and revision feedback carefully\n2. Apply surgical PROMPT.md edits that fully resolve every blocking feedback item — do not rewrite from title/description alone\n3. Keep structure stable unless feedback requires rethink; preserve uncriticized content\n4. Keep `## Original Description` at the top (after title/metadata) with the operator description **verbatim**\n5. Ensure the revised specification is still detailed enough for an AI agent to execute" : isFreshRespecification ? "1. Read the project structure to understand context (package.json, source files, etc.)\n2. Treat the current task title and description as mandatory primary inputs for a new spec\n3. Write a fresh complete PROMPT.md specification to the given path following the format in your system prompt\n4. Include `## Original Description` near the top with the exact Description text above (verbatim)\n5. Address the revision feedback without inventing extra scope\n6. Name actual files, functions, and patterns from the codebase — be specific" : "1. Read the project structure to understand context (package.json, source files, etc.)\n2. Write a complete PROMPT.md specification to the given path following the format in your system prompt\n3. Include `## Original Description` immediately after title/`Created`/`Size` with the exact Description text above (verbatim — do not paraphrase)\n4. The specification must be detailed enough for an autonomous AI agent to implement without asking questions\n5. Name actual files, functions, and patterns from the codebase — be specific"}
|
||||
|
||||
Use the write tool to write the specification file.${commandsSection}${completionDocumentationSection}${memorySection}${attachmentsSection}${userCommentsSection}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user