@deksden-com/dd-flow-cli 0.9.0-beta.120 → 0.9.0-beta.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/build-info.json +5 -5
  3. package/dist/cli/hook-ingress.js +4 -3
  4. package/dist/cli/native-hook-ingress.js +34 -2
  5. package/dist/harness-runtime/bin/dd-agy.mjs +6 -3
  6. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +16 -0
  7. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +12 -2
  8. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +60 -8
  9. package/dist/harness-runtime/lib/dd-zcode.mjs +28 -13
  10. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
  11. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  12. package/dist/harness-runtime/lib/lifecycle-command.mjs +218 -0
  13. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  14. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  15. package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
  16. package/dist/harness-runtime/lib/native-hook-command.mjs +36 -5
  17. package/dist/runtime/context.js +1 -1
  18. package/dist/schemas/flow-run.schema.json +1 -0
  19. package/dist/services/code-checks.js +40 -0
  20. package/dist/services/codex-hook-delivery.js +6 -2
  21. package/dist/services/hooks.js +51 -59
  22. package/dist/services/lifecycle-command.js +1 -383
  23. package/dist/services/lifecycle-contract.js +1 -30
  24. package/dist/services/lifecycle-invocations.js +34 -13
  25. package/dist/services/managed-processes.js +42 -6
  26. package/dist/services/run-control.js +7 -1
  27. package/dist/services/runs.js +60 -47
  28. package/dist/services/runtime-scope-stop.js +23 -2
  29. package/dist/services/stage-report-renderer.js +19 -0
  30. package/dist/services/verification-matrix.js +325 -0
  31. package/dist/services/vnext-code-review.js +25 -8
  32. package/dist/services/vnext-code.js +38 -11
  33. package/dist/services/vnext-merge.js +30 -7
  34. package/dist/services/vnext-plan-review.js +22 -6
  35. package/dist/services/vnext-plan.js +45 -17
  36. package/dist/services/work-registry.js +13 -1
  37. package/dist/storage/database.js +43 -9
  38. package/package.json +4 -1
@@ -25,6 +25,7 @@ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-
25
25
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
26
26
  import { assertRunMutationAllowed } from "./run-recovery.js";
27
27
  import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
28
+ import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, publishVerificationMatrix, latestRunVerificationMatrix } from "./verification-matrix.js";
28
29
  const stage = "code";
29
30
  const stageDir = vnextStageDirectory("code");
30
31
  /** A controller prepares slow, deterministic project tooling before it asks an
@@ -63,8 +64,7 @@ export function prepareVnextCodeStart(context, input) {
63
64
  if (existingStage && fs.existsSync(existingPrompt)) {
64
65
  return { ...common, resumed: true, existingStage, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(existingRoot) };
65
66
  }
66
- if (!sourceRepairAttempt)
67
- prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
67
+ const attachment = sourceRepairAttempt ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
68
68
  const batch = path.join(home, "03-plan", "code-work-batch.json");
69
69
  const handoffFailures = validateVnextCodeHandoff(context, {
70
70
  projectRoot,
@@ -86,23 +86,27 @@ export function prepareVnextCodeStart(context, input) {
86
86
  validateWorkBatchFile(batch);
87
87
  const template = fs.readFileSync(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
88
88
  const variables = getFlowRunVariables(context, { projectRoot, runId: run.id });
89
- return { ...common, resumed: false, template, capacity: variables.variables?.["runtime.subagent.available_slots"] };
89
+ const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", completeness: "declared", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
90
+ return { ...common, resumed: false, template, capacity: variables.variables?.["runtime.subagent.available_slots"], matrix };
90
91
  }
91
92
  export async function startVnextCode(context, input, prepared = prepareVnextCodeStart(context, input)) {
92
93
  const { projectRoot, project, run, root, promptPath, rootWork, sourceRepairAttempt } = prepared;
93
94
  assertRunMutationAllowed(context, project.id, run.id);
94
95
  assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
95
96
  if (prepared.resumed) {
97
+ resumeVerificationMatrix(context, project.id, run.id, stage);
96
98
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
97
99
  bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
98
100
  const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
99
101
  return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) } };
100
102
  }
103
+ if (!sourceRepairAttempt)
104
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" }, true);
101
105
  fs.mkdirSync(root, { recursive: true });
102
106
  const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
103
107
  // Graph rendering may issue managed child commands; it belongs to execution.
104
108
  const graph = codeWorkGraph(context, project.id, run.id);
105
- const prompt = `${coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph })}\n${stageLifecycleInstruction}\n`;
109
+ const prompt = `${coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph })}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, prepared.home, "implementation and code-verification.json")}\n`;
106
110
  fs.writeFileSync(promptPath, prompt);
107
111
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
108
112
  const binding = bindStageCoordinatorWork(context, {
@@ -114,15 +118,19 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
114
118
  ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
115
119
  });
116
120
  if (!sourceRepairAttempt) {
117
- attachFlowRunStage(context, {
121
+ const attachmentInput = {
118
122
  projectRoot,
119
123
  runId: input.runId,
120
124
  stage,
121
125
  dir: stageDir,
122
126
  status: "running",
123
127
  dataSchemaId: "dd-flow/stage-report@1"
124
- });
128
+ };
129
+ const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
130
+ commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
125
131
  }
132
+ else
133
+ commitVerificationMatrixStart(context, prepared.matrix, () => undefined);
126
134
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
127
135
  appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
128
136
  return {
@@ -180,8 +188,10 @@ export async function finishVnextCode(context, input) {
180
188
  // Reject a malformed semantic receipt before running the expensive gate.
181
189
  const verification = input.verification ?? verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
182
190
  validateSchema({ schemaName: "code-verification", file: input.verificationFile, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
191
+ const retainedMatrix = latestRunVerificationMatrix(context, project.id, run.id, stage);
192
+ const retainedMatrixInstruction = verificationMatrixInstruction(retainedMatrix?.metadata ?? null, home, "the current semantic verification and repair");
183
193
  if (verification.verdict === "blocked") {
184
- return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, instruction: "CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again." };
194
+ return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again.\n${retainedMatrixInstruction}` };
185
195
  }
186
196
  if (verification.verdict === "passed" && verification.unresolved.length > 0) {
187
197
  throw new AppError("verification_contradictory", "A passed CODE verification cannot contain unresolved obligations", 2, { unresolved: verification.unresolved });
@@ -196,7 +206,7 @@ export async function finishVnextCode(context, input) {
196
206
  verification,
197
207
  objective: verification.summary
198
208
  });
199
- return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke this same stage finish command again." };
209
+ return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke this same stage finish command again.\n${retainedMatrixInstruction}` };
200
210
  }
201
211
  const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
202
212
  if (input.retryCheckId) {
@@ -209,7 +219,11 @@ export async function finishVnextCode(context, input) {
209
219
  ...(input.progress ? { progress: input.progress } : {})
210
220
  });
211
221
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
222
+ const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: input.verificationFile });
223
+ const matrixInstruction = matrix ? verificationMatrixInstruction(matrix.metadata, home, "semantic verification in code-verification.json") : "";
212
224
  if (failed.length) {
225
+ context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
226
+ publishVerificationMatrix(context, matrix);
213
227
  const continuation = registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks, generation });
214
228
  throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
215
229
  failures: failed,
@@ -217,15 +231,19 @@ export async function finishVnextCode(context, input) {
217
231
  retry_after_workspace_change: true,
218
232
  workspace_fingerprint: failed[0].workspace_fingerprint,
219
233
  continuation,
220
- instruction: "Repair Work is already registered. End this coordinator Turn; the controller will launch a fresh child. After it completes, update semantic verification and finish the Stage."
234
+ ...(matrix ? { verification_matrix: matrix.metadata, verification_matrix_instruction: matrixInstruction } : {}),
235
+ instruction: `Repair Work is already registered. End this coordinator Turn; the controller will launch a fresh child. After it completes, update semantic verification and finish the Stage.\n${matrixInstruction}`
221
236
  });
222
237
  }
223
238
  const finalReceipts = receipts;
224
239
  if (verification.verdict === "needs_checks") {
240
+ context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
241
+ publishVerificationMatrix(context, matrix);
225
242
  return { ok: true, run_id: run.id, stage, outcome: "verification_required", verification,
243
+ ...(matrix ? { verification_matrix: matrix.metadata, verification_matrix_instruction: matrixInstruction } : {}),
226
244
  receipts: finalReceipts.map(receipt => ({ id: receipt.id, receipt_path: receipt.receipt_path, input_hash: receipt.input_hash, status: receipt.status })),
227
245
  next: { kind: "verification_required", finish_command: finishCommand(context, run.id, projectRoot, input.verificationFile) },
228
- instruction: "Aggregate receipts are available. Keep CODE running. Inspect them, update code-verification.json to passed or needs_repair, then invoke only the issued successor finish_command. A material document change belongs to the registered fresh repair Work, not this coordinator Session." };
246
+ instruction: `Aggregate receipts are available. Keep CODE running. Inspect them, update code-verification.json to passed or needs_repair, then invoke only the issued successor finish_command. A material document change belongs to the registered fresh repair Work, not this coordinator Session.\n${matrixInstruction}` };
229
247
  }
230
248
  const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: finalReceipts, runId: run.id, runHome: home });
231
249
  const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
@@ -278,6 +296,8 @@ export async function finishVnextCode(context, input) {
278
296
  let mergeRequest = null;
279
297
  assertRunMutationAllowed(context, project.id, run.id, generation);
280
298
  const prepareMerge = next === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
299
+ if (matrix)
300
+ Object.assign(report.semantic, { verification_matrix: matrix.metadata });
281
301
  const preparedReport = prepareStageReport(report);
282
302
  const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: report.mechanical.git };
283
303
  withStageSettlement(context, () => {
@@ -288,6 +308,7 @@ export async function finishVnextCode(context, input) {
288
308
  }
289
309
  mergeRequest = prepareMerge?.() ?? null;
290
310
  const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
311
+ acceptVerificationMatrix(context, matrix);
291
312
  acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
292
313
  completeFlowRunStage(context, completionInput, preparedCompletion);
293
314
  const rootWork = requireRootWork(context, project.id, run.id);
@@ -582,7 +603,13 @@ export function verificationProjection(works, receipts, options = {}) {
582
603
  if (item.obligation_ref)
583
604
  rawEvidence.set(item.obligation_ref, unique([...(rawEvidence.get(item.obligation_ref) ?? []), ...(item.refs ?? [])]));
584
605
  }
585
- const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
606
+ const byCriterionGate = new Map();
607
+ for (const entry of packets.flatMap((value) => value.acceptance)) {
608
+ const key = JSON.stringify([entry.criterion_id, entry.gate]);
609
+ const previous = byCriterionGate.get(key);
610
+ byCriterionGate.set(key, { criterion_id: entry.criterion_id, gate: entry.gate, check_refs: unique([...(previous?.check_refs ?? []), ...entry.check_refs]) });
611
+ }
612
+ const acceptance = [...byCriterionGate.values()];
586
613
  const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
587
614
  const acceptanceEvidence = new Map(acceptance.map((item) => {
588
615
  const reported = rawEvidence.get(item.criterion_id) ?? [];
@@ -23,6 +23,7 @@ import { ensureLaneWorkspace, prepareLaneWorkspace, heartbeatLaneLock, releaseLa
23
23
  import { assertRunMutationAllowed } from "./run-recovery.js";
24
24
  import { assertStageStartHookEvent } from "./hooks.js";
25
25
  import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
26
+ import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix, publishVerificationMatrix } from "./verification-matrix.js";
26
27
  const stage = "merge";
27
28
  const stageDir = vnextStageDirectory(stage);
28
29
  export function isVnextMergeRun(context, input) {
@@ -133,8 +134,7 @@ export function prepareVnextMergeStart(context, input) {
133
134
  if (!work || work.project_id !== project.id || work.run_id !== run.id || !["created", "running"].includes(work.status))
134
135
  throw new AppError("runtime_artifact_invalid", "MERGE executor Work is missing or not startable", 1, { work_id: request.executor_work_id });
135
136
  const index = JSON.parse(run.index_json);
136
- if (!index.stage_runs?.some(entry => entry.stage === stage))
137
- prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
137
+ const attachment = ["active", "action_required"].includes(request.status) && index.stage_runs?.some(entry => entry.stage === stage) ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
138
138
  prepareLaneWorkspace(context, { projectRoot, lane: "merge", workspacePath: request.target_workspace, branch: request.target_branch });
139
139
  const checks = effectiveMergeChecks(run, request), settings = executionSettings(run);
140
140
  const existingPrompt = ["active", "action_required"].includes(request.status) && fs.existsSync(promptPath) ? fs.readFileSync(promptPath, "utf8") : null;
@@ -142,15 +142,21 @@ export function prepareVnextMergeStart(context, input) {
142
142
  prepareOutputFile(promptPath);
143
143
  if (input.externalContext)
144
144
  prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
145
- return { ...common, terminal: false, root, workRoot, resultPath, promptPath, checks, settings, existingPrompt };
145
+ const matrix = existingPrompt !== null ? latestRunVerificationMatrix(context, project.id, run.id, stage, "input") : prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
146
+ return { ...common, terminal: false, root, workRoot, resultPath, promptPath, checks, settings, existingPrompt, matrix };
146
147
  }
147
148
  export async function startVnextMerge(context, input, prepared = prepareVnextMergeStart(context, input)) {
148
149
  const { projectRoot, project, run, request, generation } = prepared;
149
150
  if (prepared.terminal)
150
151
  return requestView(context, request);
151
152
  const { root, workRoot, resultPath, promptPath, checks, settings, existingPrompt } = prepared;
153
+ if (existingPrompt !== null)
154
+ resumeVerificationMatrix(context, project.id, run.id, stage);
152
155
  assertRunMutationAllowed(context, project.id, run.id, generation);
153
156
  assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
157
+ const alreadyAttached = ["active", "action_required"].includes(request.status) && (JSON.parse(requireRun(context, project.id, run.id).index_json).stage_runs ?? []).some((item) => item.stage === stage);
158
+ if (!alreadyAttached)
159
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" }, true);
154
160
  fs.mkdirSync(workRoot, { recursive: true });
155
161
  ensureLaneWorkspace(context, { projectRoot, lane: "merge", workspacePath: request.target_workspace, branch: request.target_branch });
156
162
  const lane = await waitAcquireLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, timeoutSeconds: 3600, pollIntervalSeconds: 15, ttlSeconds: 7200, reason: `MERGE ${request.merge_request_id}`, ...(input.progress ? { progress: input.progress } : {}) });
@@ -163,11 +169,13 @@ export async function startVnextMerge(context, input, prepared = prepareVnextMer
163
169
  const binding = pendingWork?.status === "created"
164
170
  ? startStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) })
165
171
  : bindStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
166
- const prompt = existingPrompt ?? `${mergePrompt(context, { run, request, resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n`;
172
+ const prompt = existingPrompt ?? `${mergePrompt(context, { run, request, resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, run.run_root, "MERGE source/target verification and residual-risk assessment")}\n`;
167
173
  if (existingPrompt === null)
168
174
  fs.writeFileSync(promptPath, prompt);
169
175
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
170
- ensureMergeStageAttached(context, run, projectRoot);
176
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" };
177
+ const attachment = alreadyAttached ? null : prepareFlowRunStageAttachment(context, attachmentInput);
178
+ commitVerificationMatrixStart(context, prepared.matrix, () => attachment ? attachFlowRunStage(context, attachmentInput, attachment) : undefined);
171
179
  return startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
172
180
  }
173
181
  const current = requireRequest(context, request.merge_request_id);
@@ -182,11 +190,13 @@ export async function startVnextMerge(context, input, prepared = prepareVnextMer
182
190
  if (claimed.changes !== 1 && requireRequest(context, current.merge_request_id).status !== "active")
183
191
  throw new AppError("merge_claim_conflict", "MERGE request could not acquire the integration lane", 1, { merge_request_id: current.merge_request_id });
184
192
  const started = startStageCoordinatorWork(context, { workId: current.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
185
- const prompt = `${mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n`;
193
+ const prompt = `${mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, run.run_root, "MERGE source/target verification and residual-risk assessment")}\n`;
186
194
  fs.writeFileSync(promptPath, prompt);
187
195
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
188
196
  context.db.run("UPDATE work_sessions SET prompt_path = ?, result_path = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [promptPath, resultPath, context.now(), current.executor_work_id]);
189
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
197
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" };
198
+ const attachment = alreadyAttached ? null : prepareFlowRunStageAttachment(context, attachmentInput);
199
+ commitVerificationMatrixStart(context, prepared.matrix, () => attachment ? attachFlowRunStage(context, attachmentInput, attachment) : undefined);
190
200
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_started", merge_request_id: current.merge_request_id, work_id: current.executor_work_id, target_head: targetHead });
191
201
  return startPacket(context, run, current, started, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
192
202
  }
@@ -323,6 +333,11 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
323
333
  verifyLocalDelivery(request);
324
334
  context.db.run("UPDATE merge_requests SET checkpoint = 'delivered', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
325
335
  request = requireRequest(context, request.merge_request_id);
336
+ const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: prepared.resultPath });
337
+ // Retain exact bounded sources before deleting the feature checkout. A
338
+ // crash here recovers presentation from packets, never repeats the checks.
339
+ context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
340
+ publishVerificationMatrix(context, matrix);
326
341
  performConfiguredCleanup(run, request);
327
342
  const acceptedResult = prepared.result.text;
328
343
  const executor = context.db.get("SELECT status, result FROM works WHERE work_id = ?", [request.executor_work_id]);
@@ -338,6 +353,8 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
338
353
  const root = path.join(requireHome(run), stageDir);
339
354
  const retainedReport = completedMergeReport(context, run, root);
340
355
  const report = retainedReport ?? mergeReport(context, run, requireRequest(context, request.merge_request_id), semantic, receipts);
356
+ if (matrix)
357
+ Object.assign(report.semantic, { verification_matrix: matrix.metadata });
341
358
  if (!retainedReport)
342
359
  commitMergeStageReport(context, run, projectRoot, root, report);
343
360
  const rootWork = requireRootWork(context, project.id, run.id);
@@ -409,10 +426,16 @@ function completedMergeReport(context, run, root) {
409
426
  return JSON.parse(fs.readFileSync(file, "utf8"));
410
427
  }
411
428
  function commitMergeStageReport(context, run, projectRoot, root, report) {
429
+ const matrix = latestRunVerificationMatrix(context, run.project_id, run.id, stage, "output");
430
+ if (matrix)
431
+ Object.assign(report.semantic, { verification_matrix: matrix.metadata });
432
+ if (JSON.parse(run.index_json).verification_matrix_contract && !matrix)
433
+ throw new AppError("verification_matrix_packet_missing", "Completed marked MERGE lacks its retained final matrix", 1, { run_id: run.id });
412
434
  const preparedReport = prepareStageReport(report);
413
435
  const completion = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@2", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256 };
414
436
  const preparedCompletion = prepareFlowRunStageCompletion(context, completion);
415
437
  withStageSettlement(context, () => {
438
+ acceptVerificationMatrix(context, matrix);
416
439
  acceptStageReport(context, { projectId: run.project_id, runId: run.id, stage, root, prepared: preparedReport });
417
440
  completeFlowRunStage(context, completion, preparedCompletion);
418
441
  });
@@ -22,6 +22,7 @@ import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.j
22
22
  import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
23
23
  import { stageGraphScope } from "./stage-work-graph.js";
24
24
  import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
25
+ import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
25
26
  const stage = "plan-review";
26
27
  const stageDir = vnextStageDirectory(stage);
27
28
  const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
@@ -59,14 +60,15 @@ export function prepareVnextPlanReviewStart(context, input) {
59
60
  if (input.externalContext)
60
61
  prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
61
62
  prepareOutputFile(path.join(root, "stage-prompt.md"));
62
- const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum };
63
+ const attachment = existing?.status === "running" ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
64
+ const matrix = existing?.status === "running" ? latestRunVerificationMatrix(context, project.id, run.id, stage, "input") : prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
65
+ const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum, matrix };
63
66
  if (existing?.status === "running") {
64
67
  const orchestration = readFanoutDescriptor(root);
65
68
  if (!orchestration)
66
69
  prepareOutputFile(path.join(root, "fanout.json"));
67
70
  return { ...active, state: "resumed", workId: existing.work_id, prompt: fs.readFileSync(path.join(root, "stage-prompt.md"), "utf8"), orchestration };
68
71
  }
69
- prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
70
72
  const rootWork = rootWorkId(context, project.id, run.id);
71
73
  if (effective === "off") {
72
74
  const failures = validateVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIds });
@@ -85,7 +87,8 @@ export function prepareVnextPlanReviewStart(context, input) {
85
87
  }
86
88
  for (const name of ["stage-report.json", "stage-report.md", "stage-report.html"])
87
89
  prepareOutputFile(path.join(root, name));
88
- return { ...active, state: "off", rootWork, codeItems };
90
+ const outputMatrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
91
+ return { ...active, state: "off", rootWork, codeItems, outputMatrix };
89
92
  }
90
93
  const template = read(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "plan-review.md"));
91
94
  const revision = currentPlanRevision(home, run.workspace_root);
@@ -103,11 +106,13 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
103
106
  }
104
107
  const { requested, effective, groups, batchChecksum, planChecksum, workspaceRoute } = prepared;
105
108
  if (prepared.state === "resumed") {
109
+ resumeVerificationMatrix(context, project.id, run.id, stage);
106
110
  const promptPath = path.join(root, "stage-prompt.md");
107
111
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
108
112
  return { ...preparedExisting(context, { projectRoot, projectId: project.id, run, root, workId: prepared.workId, hookEventId: input.hookEventId, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}), requested, effective, groups, batchChecksum, planChecksum, orchestration: prepared.orchestration }), ...(externalContext ? { external_context: externalContext } : {}) };
109
113
  }
110
114
  ensureWorkRegistry(context);
115
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" }, true);
111
116
  fs.mkdirSync(root, { recursive: true });
112
117
  const now = context.now(), rootWork = prepared.rootWork;
113
118
  if (prepared.state === "off") {
@@ -115,12 +120,17 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
115
120
  fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
116
121
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
117
122
  bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
118
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
123
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" };
124
+ const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
125
+ commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
119
126
  const reportGit = gitFacts(run.workspace_root);
120
127
  const nextCommand = codeCommand(context, run.id, projectRoot);
121
128
  return withStageSettlement(context, () => {
122
129
  const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
123
130
  const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, nextCommand });
131
+ acceptVerificationMatrix(context, prepared.outputMatrix);
132
+ if (prepared.outputMatrix)
133
+ Object.assign(report.semantic, { verification_matrix: prepared.outputMatrix.metadata });
124
134
  const preparedReport = prepareStageReport(report);
125
135
  acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
126
136
  const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
@@ -133,7 +143,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
133
143
  const workId = nextWorkId(context, project.id, "plan-review");
134
144
  context.db.run("INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, depends_on_json, status, result, started_at, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, 'reuse_allowed', NULL, '[]', 'running', NULL, ?, ?, ?, NULL)", [workId, project.id, run.id, rootWork, reviewTask, now, now, now]);
135
145
  const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId, projectRoot });
136
- const prompt = `${orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), template: prepared.template, revision: prepared.revision, capacity: prepared.capacity })}\n${stageLifecycleInstruction}\n`;
146
+ const prompt = `${orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), template: prepared.template, revision: prepared.revision, capacity: prepared.capacity })}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, home, "reviewing PLAN coverage, fixtures and proof limits")}\n`;
137
147
  const promptPath = path.join(root, "stage-prompt.md");
138
148
  fs.writeFileSync(promptPath, prompt);
139
149
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
@@ -146,7 +156,9 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
146
156
  fs.rmSync(promptPath, { force: true });
147
157
  throw error;
148
158
  }
149
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
159
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" };
160
+ const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
161
+ commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
150
162
  markReviewPending(home, groups);
151
163
  const workSessionId = String(binding.work_session_id);
152
164
  const sessionId = String(binding.session_id);
@@ -307,13 +319,17 @@ function finishPlanReviewOwned(context, input) {
307
319
  const codeItems = prepareWorkBatch(context, { parentWorkId: codeParentId, file: batch, stage: "code" });
308
320
  const reportGit = gitFacts(run.workspace_root);
309
321
  const nextCommand = codeCommand(context, run.id, projectRoot);
322
+ const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", semanticFile: decisionPath });
310
323
  return withStageSettlement(context, () => {
311
324
  const registered = addWorkBatch(context, { parentWorkId: codeParentId, prepared: codeItems, stage: "code" });
312
325
  const now = context.now();
313
326
  settleReviewParent(context, { projectRoot, projectId: project.id, runId: run.id, parentWorkId: parent.work_id, status: "completed", result: decision.summary, reason: "plan_review_completed", resultPath: path.join(root, "stage-report.json") });
314
327
  const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context), git: reportGit, nextCommand });
315
328
  applyReviewResults(home, latestChildren, decision);
329
+ if (matrix)
330
+ Object.assign(report.semantic, { verification_matrix: matrix.metadata });
316
331
  const preparedReport = prepareStageReport(report);
332
+ acceptVerificationMatrix(context, matrix);
317
333
  acceptStagePacket(context, { projectId: project.id, runId: run.id, stage, prepared: decisionReceipt });
318
334
  acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
319
335
  const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
@@ -21,6 +21,7 @@ import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.j
21
21
  import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
22
22
  import { subagentCapacityKey } from "./vnext-fanout.js";
23
23
  import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
24
+ import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, verificationMatrixInstruction } from "./verification-matrix.js";
24
25
  export function isVnextPlanRun(context, input) {
25
26
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
26
27
  return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
@@ -41,7 +42,7 @@ export function prepareVnextPlanStart(context, input) {
41
42
  const { profile: codeCheckProfile } = readCodeCheckProfile(run.workspace_root);
42
43
  if (context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running'", [project.id, run.id, planTask]))
43
44
  throw new AppError("invalid_work_state", "PLAN already has a running Work", 1, { run_id: run.id });
44
- prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" });
45
+ const attachment = prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" });
45
46
  const root = path.join(home, vnextStageDirectory("plan"));
46
47
  if (input.externalContext)
47
48
  prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
@@ -58,12 +59,16 @@ export function prepareVnextPlanStart(context, input) {
58
59
  const git = gitFacts(run.workspace_root);
59
60
  for (const file of [...planPaths, ...mapPaths, ...["stage-prompt.md", "work-context.json", "fixture-root.prompt.md"].map(name => path.join(root, name))])
60
61
  prepareOutputFile(file);
61
- return { projectRoot, project, run, home, root, rootWork, workspaceRoute, protocols, template, codeCheckProfile, planPaths, mapPaths, owned, identities, aspects, runVariables, mergeRequired, git };
62
+ const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage: "plan", role: "input", stageAttempt: attachment.attempt });
63
+ return { projectRoot, project, run, home, root, rootWork, workspaceRoute, protocols, template, codeCheckProfile, planPaths, mapPaths, owned, identities, aspects, runVariables, mergeRequired, git, matrix };
62
64
  }
63
65
  export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(context, input)) {
64
66
  const { projectRoot, project, run, root, rootWork, workspaceRoute, protocols, template, codeCheckProfile, planPaths, mapPaths, owned, identities, aspects, runVariables, mergeRequired, git, home } = prepared;
65
67
  ensureWorkRegistry(context);
66
68
  assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage: "plan", projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
69
+ if (context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running'", [project.id, run.id, planTask]))
70
+ throw new AppError("invalid_work_state", "PLAN already has a running Work", 1, { run_id: run.id });
71
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" }, true);
67
72
  fs.mkdirSync(root, { recursive: true });
68
73
  const now = context.now();
69
74
  // Imported stage fixtures have a synthetic running root with no prior agent.
@@ -113,9 +118,13 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
113
118
  : `PLAN-REVIEW mode is ${planReviewMode}: include review groups only for genuinely applicable aspects; an empty set is allowed.`;
114
119
  fs.writeFileSync(promptPath, `${prompt}\n<review_mode_rule>${reviewModeRule}</review_mode_rule>\n<verification_rule>Every flow check must be an autonomous executable command. Do not declare external/manual proof or a human review as a check, evidence substitute, DEF, or gate. The only user interaction supported by this flow is an explicit stage pause for a material unanswered question.</verification_rule>\n${stageLifecycleInstruction}\n`);
115
120
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
121
+ if (prepared.matrix)
122
+ fs.appendFileSync(promptPath, verificationMatrixInstruction(prepared.matrix.metadata, home, "designing the source PLAN and its tests"));
116
123
  fs.writeFileSync(path.join(root, "work-context.json"), JSON.stringify({ schema_id: "dd-flow/work-context@1", system: { run_id: run.id, work_id: planWorkId, stage: "plan" }, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root }, artifacts: artifactMaterialization, input: { protocols, owned_obligations: Object.fromEntries(owned) } }, null, 2));
117
124
  const binding = bindStageCoordinatorWork(context, { workId: planWorkId, hookEventId: input.hookEventId, stage: "plan", promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
118
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" });
125
+ const attachmentInput = { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" };
126
+ const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
127
+ commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
119
128
  const workSessionId = String(binding.work_session_id);
120
129
  const sessionId = String(binding.session_id);
121
130
  refreshRunWorkProjection(context, project.id, run.id);
@@ -169,6 +178,9 @@ function finishVnextPlanOwned(context, input) {
169
178
  plan_checksums: planFiles.map((file) => ({ path: path.relative(run.workspace_root, file).split(path.sep).join("/"), sha256: checksum(file) })),
170
179
  aspect_map_checksums: mapFiles.map((file) => ({ path: path.relative(home, file).split(path.sep).join("/"), sha256: checksum(file) }))
171
180
  });
181
+ const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage: "plan", role: "output" });
182
+ if (matrix)
183
+ Object.assign(report.semantic, { verification_matrix: matrix.metadata });
172
184
  const preparedReport = prepareStageReport(report);
173
185
  validateSchema({ schemaName: "stage-report", file: path.join(root, "stage-report.json"), data: preparedReport.normalized, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
174
186
  const completionInput = { projectRoot, runId: run.id, stage: "plan", status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: report.mechanical.git };
@@ -179,6 +191,7 @@ function finishVnextPlanOwned(context, input) {
179
191
  throw new AppError("invalid_work_state", "PLAN Work changed during finish preparation", 1);
180
192
  context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", ["PLAN accepted; awaiting PLAN-REVIEW", now, now, work.work_id]);
181
193
  context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [path.join(root, "stage-report.json"), now, now, workSession.id]);
194
+ acceptVerificationMatrix(context, matrix);
182
195
  acceptStageReport(context, { projectId: project.id, runId: run.id, stage: "plan", root, prepared: preparedReport });
183
196
  completeFlowRunStage(context, completionInput, preparedCompletion);
184
197
  refreshRunWorkProjection(context, project.id, run.id);
@@ -201,6 +214,7 @@ export function prepareVnextPlanArtifacts(context, input) {
201
214
  const batch = path.join(input.home, "03-plan", "code-work-batch.json");
202
215
  const ownership = protocolOwnership(input.home, input.protocols);
203
216
  const obligations = acceptedObligations(input.home);
217
+ const mergeEnding = runEndsAtMerge(context, input.projectRoot, input.runId);
204
218
  const frozenDocumentBaselines = input.publishBatch === false && fs.existsSync(batch)
205
219
  ? new Map(readJson(batch).works?.flatMap((work) => typeof work.key === "string" ? (work.document_updates ?? []).flatMap((update) => typeof update.path === "string" && (typeof update.baseline_sha256 === "string" || update.baseline_sha256 === null)
206
220
  ? [[`${work.key}\u0000${update.path}`, update.baseline_sha256]]
@@ -214,7 +228,7 @@ export function prepareVnextPlanArtifacts(context, input) {
214
228
  try {
215
229
  const { value } = prepareJsonSchema(file, "PLAN", { schemaName: "vnext-protocol-plan", projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
216
230
  assertPlanIdentity(value, planIdentity(input.home, input.runId, protocolId, ownership.get(protocolId) ?? []), file);
217
- validatePlanSemantics(file, value, new Set(ownership.get(protocolId) ?? []), obligations);
231
+ validatePlanSemantics(file, value, new Set(ownership.get(protocolId) ?? []), obligations, mergeEnding);
218
232
  validateCodeCheckCommands(workspaceRoot, value.checks.filter((check) => check.availability === "available").map((check) => check.command));
219
233
  for (const check of value.checks.filter((item) => item.availability === "available"))
220
234
  validateCheckDeclaration(workspaceRoot, check);
@@ -481,7 +495,7 @@ function normalizedAspectMapRefs(file, bytes, projectRoot, runHome, runId) {
481
495
  aspect.planned_artifact_refs = aspect.planned_artifact_refs.map(normalize);
482
496
  return changed ? `${JSON.stringify(map, null, 2)}\n` : bytes;
483
497
  }
484
- function projectCodeWorkBatch(input) {
498
+ export function projectCodeWorkBatch(input) {
485
499
  const obligationMap = acceptedObligationMap(input.home);
486
500
  const orientation = projectOrientation(input.workspaceRoot);
487
501
  const works = input.plans.flatMap(({ protocolId, file, value }) => value.items.map((item) => {
@@ -516,10 +530,9 @@ function projectCodeWorkBatch(input) {
516
530
  ])],
517
531
  discovery_boundary: item.execution_context.discovery_boundary,
518
532
  planned_write_areas: [...new Set([...item.execution_context.planned_write_areas, ...documentPaths])],
519
- // A criterion belongs to every listed Work. Its proof must travel with
520
- // those Work packets as well as item-local verification; otherwise CODE
521
- // silently loses readiness/code checks declared only by acceptance.
522
- checks: value.checks.filter((check) => itemCheckRefs(item, value.acceptance).includes(check.id)),
533
+ // CODE still needs acceptance-only aggregate declarations in its packets;
534
+ // work checks belong only to the item that explicitly verifies them.
535
+ checks: value.checks.filter((check) => itemCheckRefs(item, value.acceptance, value.checks).includes(check.id)),
523
536
  provides_checks: value.checks.filter((check) => check.availability === "planned" && check.provided_by === item.id),
524
537
  stop_conditions: item.execution_context.stop_conditions,
525
538
  depends_on: item.depends_on.map((dependency) => `${protocolId}:${dependency}`),
@@ -548,10 +561,15 @@ function projectCodeWorkBatch(input) {
548
561
  const ordered = [...works].sort((a, b) => a.key.localeCompare(b.key));
549
562
  return { schema_id: "dd-flow/code-work-batch@5", sources: input.plans.map(({ protocolId, file, value }) => ({ plan_id: value.plan_id, protocol_id: protocolId, revision: value.revision, sha256: checksum(file) })), works: ordered };
550
563
  }
551
- function itemCheckRefs(item, acceptance) {
552
- return [...new Set([...item.verification.check_refs, ...acceptance.filter((entry) => entry.plan_item_ids.includes(item.id)).flatMap((entry) => entry.check_refs)])];
564
+ function itemCheckRefs(item, acceptance, catalog) {
565
+ const aggregate = new Set(catalog.filter((check) => check.run_at !== "work").map((check) => check.id));
566
+ return [...new Set([...item.verification.check_refs, ...acceptance.filter((entry) => entry.plan_item_ids.includes(item.id)).flatMap((entry) => entry.check_refs.filter((ref) => aggregate.has(ref)))])];
567
+ }
568
+ function renderCodeTask(item, catalog, acceptance = []) {
569
+ const local = catalog.filter((check) => check.run_at === "work" && item.verification.check_refs.includes(check.id));
570
+ const aggregate = catalog.filter((check) => check.run_at !== "work" && itemCheckRefs(item, acceptance, catalog).includes(check.id));
571
+ return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Work checks: ${local.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Later aggregate checks (CODE/MERGE; do not run in this Work): ${aggregate.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n");
553
572
  }
554
- function renderCodeTask(item, catalog, acceptance = []) { const checks = catalog.filter((check) => itemCheckRefs(item, acceptance).includes(check.id)); return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Checks: ${checks.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n"); }
555
573
  function validateProjectedPaths(batch, workspaceRoot, home, runId) {
556
574
  for (const work of batch.works) {
557
575
  for (const write of work.planned_write_areas)
@@ -595,7 +613,7 @@ function resolvePortablePath(value, workspaceRoot, home, runId) {
595
613
  }
596
614
  function writeJson(file, value) { const temporary = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
597
615
  function runRef(runId, runHome, file) { return `run://${runId}/${path.relative(runHome, file).split(path.sep).join("/")}`; }
598
- function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
616
+ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs, mergeEnding) {
599
617
  const items = plan.items ?? [];
600
618
  const ids = new Set(items.map((item) => item.id).filter((id) => Boolean(id)));
601
619
  if (ids.size !== items.length)
@@ -608,6 +626,8 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
608
626
  for (const check of plan.checks ?? []) {
609
627
  if (checks.has(check.id))
610
628
  throw new AppError("validation", "PLAN check ids must be unique", 2, { file, check_id: check.id });
629
+ if (mergeEnding && check.run_at === "release")
630
+ throw new AppError("check_gate_unreachable", "A RUN ending at MERGE cannot execute RELEASE checks", 2, { file, check_id: check.id });
611
631
  if (check.availability === "planned" && (!check.provided_by || !ids.has(check.provided_by)))
612
632
  throw new AppError("planned_check_provider_missing", "A planned check must name a PLAN item that provides it", 2, { file, check_id: check.id, provided_by: check.provided_by ?? null });
613
633
  if (check.availability === "planned" && !check.command.startsWith("@check/"))
@@ -616,6 +636,8 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
616
636
  throw new AppError("planned_check_definition_missing", "A planned check must declare its exact alias definition", 2, { file, check_id: check.id });
617
637
  checks.set(check.id, check);
618
638
  }
639
+ const localChecks = new Set(items.flatMap((item) => item.verification.check_refs));
640
+ const acceptanceIds = new Set();
619
641
  const ancestors = (itemId, seen = new Set()) => {
620
642
  for (const dependency of items.find((item) => item.id === itemId)?.depends_on ?? [])
621
643
  if (!seen.has(dependency)) {
@@ -639,7 +661,7 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
639
661
  const check = checks.get(checkId);
640
662
  if (!check)
641
663
  throw new AppError("check_reference_unknown", "PLAN item references an unknown check", 2, { file, item: item.id, check_id: checkId });
642
- if (check.availability === "planned" && check.provided_by !== item.id && !ancestors(item.id).has(check.provided_by))
664
+ if (check.run_at === "work" && check.availability === "planned" && check.provided_by !== item.id && !ancestors(item.id).has(check.provided_by))
643
665
  throw new AppError("check_consumer_not_ordered_after_provider", "A Work may consume a planned check only after its provider", 2, { file, item: item.id, check_id: checkId, provider: check.provided_by });
644
666
  }
645
667
  }
@@ -655,6 +677,11 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
655
677
  for (const acceptance of plan.acceptance ?? []) {
656
678
  if (!acceptance.criterion_id || !acceptance.criterion_id.startsWith("AC-") || !ownedRefs.has(acceptance.criterion_id))
657
679
  throw new AppError("validation", "PLAN acceptance references an AC-* owned by this protocol", 2, { file, criterion_id: acceptance.criterion_id });
680
+ if (acceptanceIds.has(acceptance.criterion_id))
681
+ throw new AppError("duplicate_plan_criterion", "PLAN acceptance criterion ids must be unique within a protocol", 2, { file, criterion_id: acceptance.criterion_id });
682
+ acceptanceIds.add(acceptance.criterion_id);
683
+ if (mergeEnding && acceptance.gate === "release")
684
+ throw new AppError("check_gate_unreachable", "A RUN ending at MERGE cannot prove RELEASE acceptance", 2, { file, criterion_id: acceptance.criterion_id });
658
685
  for (const id of acceptance.plan_item_ids ?? [])
659
686
  if (!ids.has(id))
660
687
  throw new AppError("validation", "PLAN acceptance references an unknown item", 2, { file, criterion_id: acceptance.criterion_id, plan_item_id: id });
@@ -662,9 +689,10 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
662
689
  const check = checks.get(id);
663
690
  if (!check)
664
691
  throw new AppError("check_reference_unknown", "PLAN acceptance references an unknown check", 2, { file, criterion_id: acceptance.criterion_id, check_id: id });
665
- for (const itemId of acceptance.plan_item_ids ?? [])
666
- if (check.availability === "planned" && check.provided_by !== itemId && !ancestors(itemId).has(check.provided_by))
667
- throw new AppError("check_consumer_not_ordered_after_provider", "Acceptance may consume a planned check only after its provider", 2, { file, criterion_id: acceptance.criterion_id, item: itemId, check_id: id, provider: check.provided_by });
692
+ if ((acceptance.gate !== "release" && check.run_at === "release") || (!["merge", "release"].includes(acceptance.gate) && check.run_at === "merge"))
693
+ throw new AppError("check_gate_unreachable", "PLAN acceptance references a check after its gate", 2, { file, criterion_id: acceptance.criterion_id, check_id: id, gate: acceptance.gate, run_at: check.run_at });
694
+ if (check.run_at === "work" && !localChecks.has(id))
695
+ throw new AppError("work_check_unassigned", "A work check referenced by acceptance needs an explicit verifying PLAN item", 2, { file, criterion_id: acceptance.criterion_id, check_id: id });
668
696
  }
669
697
  }
670
698
  for (const obligation of ownedRefs)