diff --git a/packages/engine/src/__tests__/ce-workflow-step-executor.test.ts b/packages/engine/src/__tests__/ce-workflow-step-executor.test.ts index cceb274e6b..d013f8faeb 100644 --- a/packages/engine/src/__tests__/ce-workflow-step-executor.test.ts +++ b/packages/engine/src/__tests__/ce-workflow-step-executor.test.ts @@ -857,7 +857,13 @@ describe("CE workflow-step executor integration", () => { // ── Item 5: FUSION_HEADLESS gating on stepEnv ─────────────────────────────── describe("executeWorkflowStep FUSION_HEADLESS (U3)", () => { - it("makes the approved PROMPT.md contract authoritative during Code Review", async () => { + it.each([ + ["code-review group", { id: "custom-check", name: "Implementation Check", optionalGroupId: "code-review" }], + ["browser-verification group", { id: "custom-check", name: "Implementation Check", optionalGroupId: "browser-verification" }], + ["review name", { id: "custom-check", name: "Custom Review" }], + ["verification name", { id: "custom-check", name: "Custom Verification" }], + ["inline-fix metadata", { id: "custom-check", name: "Implementation Check", reviewCanFixInline: true }], + ])("makes the approved PROMPT.md contract authoritative for the %s classifier", async (_label, classifier) => { const store = createMockStore(); const { executor } = makeExecutor(store); const cap = captureSession(); @@ -870,9 +876,7 @@ Ship FIVE kinds. Do NOT add roadmap-item in this task. await (executor as any).executeWorkflowStep( baseStepTask({ description: "Original request: ship SIX kinds including roadmap-item." }), makeStep({ - id: "graph:code-review-step", - name: "Code Review", - optionalGroupId: "code-review", + ...classifier, prompt: "Review the implementation against the approved task contract.", gateMode: "gate", }), @@ -886,6 +890,24 @@ Ship FIVE kinds. Do NOT add roadmap-item in this task. expect(cap.last?.systemPrompt).toContain("Do not enforce superseded requirements from the original Task Description"); }); + it("does not restore the historical task description when PROMPT.md is unavailable", async () => { + const store = createMockStore(); + const { executor } = makeExecutor(store); + const cap = captureSession(); + vi.spyOn(executor as any, "readTaskArtifact").mockResolvedValue(undefined); + + await (executor as any).executeWorkflowStep( + baseStepTask({ description: "Original request: ship SIX kinds including roadmap-item." }), + makeStep({ name: "Code Review", optionalGroupId: "code-review", gateMode: "gate" }), + "/tmp/wt", + {}, + ); + + expect(cap.last?.systemPrompt).toContain("Approved Task Contract Unavailable"); + expect(cap.last?.systemPrompt).toContain("Task Description is historical input only and is not a substitute contract"); + expect(cap.last?.systemPrompt).toContain("Return REVISE with the single reason that the approved contract could not be loaded"); + }); + it("sets FUSION_HEADLESS=1 only when unattended=true; always sets FUSION_WORKFLOW_STEP", async () => { const store = createMockStore(); const { executor } = makeExecutor(store); diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 6a794ae711..c0aea2e51b 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -16433,8 +16433,8 @@ ${scopeGuard} workflowStepMetadata.requireExternalIntegrationEvidence === true; /* - * FNXC:PlanReviewSpecInjection 2026-07-05-17:20: - * FN-7561: the Plan Review reviewer runs readonly with cwd=worktree, but the spec artifact lives at the project root under `.fusion/tasks//PROMPT.md` — OUTSIDE the task worktree. Instructing the agent to "Read PROMPT.md" therefore had it search the worktree, fail to find the file, and emit "no PROMPT.md file was found / task data lives in a DB" prose instead of a parseable verdict. That malformed/hard-failed output fed the unbounded triage↔plan-review replan loop (FN-7525 looped 13+ times overnight; FN-7575 too). Load the spec text from the store (document layer → on-disk PROMPT.md) ONCE and inject it directly into the reviewer prompt so the verdict never depends on the agent locating the file. Read from the store, not fs, so it is correct regardless of worktree vs project-root layout. + * FNXC:WorkflowReviewSpecInjection 2026-07-18-18:15: + * FN-7561 established that review agents cannot reliably locate the project-root PROMPT.md from a task worktree. Load it once through the store and embed it for every review-type node. FN-8288 extends that invariant beyond Plan Review: approved planning revisions are authoritative, the original task description is historical, and a failed artifact read must stay visible instead of silently restoring superseded scope. */ const workflowReviewSpecArtifact = isReviewTypeWorkflowStep ? await this.readTaskArtifact(task.id, "PROMPT.md") @@ -16501,17 +16501,25 @@ ${scopeGuard} * unrelated local commits can make a plan-only gate reject implementation * state and loop back to triage after the planner already approved the spec. */ - const approvedContractBlock = !isPlanReviewStep && workflowReviewSpecText - ? ` + const approvedContractBlock = !isPlanReviewStep + ? workflowReviewSpecText + ? ` Approved Task Contract: - PROMPT.md is the authoritative current contract for this review. It includes any approved planning revisions and scope decisions. - The Task Description is historical input only. Do not enforce superseded requirements from the original Task Description when they conflict with PROMPT.md. - Do not request behavior that PROMPT.md explicitly defers, excludes, or forbids. Review the implementation against the approved contract reproduced below. +- Scope exclusions do not waive security, correctness, or data-integrity defects in the approved implementation. --- BEGIN APPROVED PROMPT.md --- ${workflowReviewSpecText} --- END APPROVED PROMPT.md ---` + : ` + +Approved Task Contract Unavailable: +- PROMPT.md could not be loaded for this review. The Task Description is historical input only and is not a substitute contract. +- Do not infer, reinstate, approve, or reject requirements from the Task Description. +- Return REVISE with the single reason that the approved contract could not be loaded so the workflow can retry with canonical scope.` : ""; const scopeBlock = isPlanReviewStep ? `Plan Review Scope: