Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/opencode/src/dag/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/dag/dag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions packages/opencode/test/dag/blocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/test/dag/dag-wake-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}),
Expand Down
Loading