diff --git a/packages/opencode/src/dag/blocks.ts b/packages/opencode/src/dag/blocks.ts index db36f4cd5..c63cd33f7 100644 --- a/packages/opencode/src/dag/blocks.ts +++ b/packages/opencode/src/dag/blocks.ts @@ -276,6 +276,14 @@ function compileBlock( const verifyAggregatorIDs = verifyAggregators.get(block.id) const verifyAggregator = verifyAggregatorIDs && verifyAggregatorIDs.length > 0 ? verifyAggregatorIDs[0] : undefined + // A synthesize that follows a review is the route's final gate: it must map + // the review output so unresolvedReviewOutcomes/finalReviewGates recognize + // an ACCEPTed review as resolved (issue #304) — the same binding contract + // the verify aggregation path already honors. + const synthesizeGateBinding = + block.kind === "synthesize" && reviewDependency + ? { [`${reviewDependency.replace(/-/g, "_")}_review`]: `${reviewDependency}.output` } + : undefined return [ node({ id: block.id, @@ -293,7 +301,7 @@ function compileBlock( implementation_changed_files: `${verifyAggregator}.output.changed_files`, implementation_fingerprint: `${verifyAggregator}.output.fingerprint`, } - : undefined, + : synthesizeGateBinding, outputSchema: WRITER_KINDS.has(block.kind) ? IMPLEMENTATION_SCHEMA : block.kind === "verify" diff --git a/packages/opencode/src/dag/dag.ts b/packages/opencode/src/dag/dag.ts index 1c83a4509..28185c5c3 100644 --- a/packages/opencode/src/dag/dag.ts +++ b/packages/opencode/src/dag/dag.ts @@ -112,7 +112,7 @@ export class ReviewGateError extends Error { readonly reviewIDs: string[] constructor(dagID: string, reviewIDs: string[]) { - super(`Cannot complete deep workflow ${dagID}: unresolved review outcome(s): ${reviewIDs.join(", ")}`) + super(`Cannot complete workflow ${dagID}: unresolved review outcome(s): ${reviewIDs.join(", ")}`) this.name = "ReviewGateError" this.dagID = dagID this.reviewIDs = reviewIDs diff --git a/packages/opencode/test/dag/blocks.test.ts b/packages/opencode/test/dag/blocks.test.ts index 9404fa842..ba51154e5 100644 --- a/packages/opencode/test/dag/blocks.test.ts +++ b/packages/opencode/test/dag/blocks.test.ts @@ -161,6 +161,27 @@ describe("workflow blocks", () => { expect(nodes.find((node) => node.id === "report")?.condition).toBe('decision.output.verdict == "ACCEPT"') }) + // Issue #304: a synthesize following a review is the route's final gate — + // it must map the review output, otherwise an ACCEPTed review stays listed + // as an unresolved review outcome forever. + it("binds the review output into a following synthesize as the final gate", () => { + const nodes = DagBlocks.compileWorkflowBlocks({ + objective: "Deliver a reviewed change with a bound final report", + blocks: [ + { id: "implementation", kind: "coding" }, + { id: "verification", kind: "verify", depends_on: ["implementation"] }, + { id: "decision", kind: "review", depends_on: ["verification"] }, + { id: "report", kind: "synthesize", depends_on: ["decision"] }, + { id: "side-note", kind: "synthesize", depends_on: ["verification"] }, + ], + }) + + expect(nodes.find((node) => node.id === "report")?.input_mapping).toEqual({ + decision_review: "decision.output", + }) + expect(nodes.find((node) => node.id === "side-note")?.input_mapping).toBeUndefined() + }) + it("keeps every downstream branch behind a reporting scope gate", () => { const nodes = DagBlocks.compileWorkflowBlocks({ objective: "Deliver only while the bounded route remains valid", diff --git a/packages/opencode/test/dag/dag-wake-integration.test.ts b/packages/opencode/test/dag/dag-wake-integration.test.ts index fe72bf4a7..76039be7d 100644 --- a/packages/opencode/test/dag/dag-wake-integration.test.ts +++ b/packages/opencode/test/dag/dag-wake-integration.test.ts @@ -788,6 +788,8 @@ describe("DagLoop atomic wake integration", () => { expect(completion).toBeInstanceOf(Error) if (!(completion instanceof Error)) throw new Error("deep completion unexpectedly succeeded") expect(completion.message).toContain("unresolved review outcome") + // Issue #305: the gate message must not claim a mode it does not check. + expect(completion.message).not.toContain("deep workflow") expect((yield* store.getWorkflow(dagID))?.status).toBe("running") expect((yield* store.getNode(dagID, "review-diff"))?.status).toBe("pending") }),