@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127

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 (122) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +33 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/domain/stage-catalog.js +1 -1
  14. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  17. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  18. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  19. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  20. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  21. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  22. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  23. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  24. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  25. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  26. package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
  27. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
  28. package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
  29. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
  30. package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
  31. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
  32. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  33. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
  34. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  35. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
  36. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  37. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
  38. package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
  39. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  40. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  41. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  42. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  43. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  44. package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
  45. package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
  46. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  47. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  48. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  49. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  50. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  51. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  52. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  53. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  54. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  55. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  56. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  57. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  58. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  59. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  60. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  61. package/dist/runtime/context.js +8 -4
  62. package/dist/schemas/flow-run.schema.json +1 -0
  63. package/dist/schemas/plan-review-decision.schema.json +3 -3
  64. package/dist/schemas/run-control-receipt.schema.json +19 -0
  65. package/dist/services/code-checks.js +93 -27
  66. package/dist/services/codex-capacity.js +251 -74
  67. package/dist/services/codex-hook-delivery.js +2 -2
  68. package/dist/services/compatibility-preflight.js +4 -1
  69. package/dist/services/controller-fanout.js +9 -75
  70. package/dist/services/engines.js +70 -20
  71. package/dist/services/eval-snapshots.js +179 -40
  72. package/dist/services/external-work-launch.js +21 -9
  73. package/dist/services/harness-adapter.js +195 -25
  74. package/dist/services/hooks.js +65 -21
  75. package/dist/services/lifecycle-invocations.js +404 -32
  76. package/dist/services/managed-lease-monitor.js +128 -0
  77. package/dist/services/managed-processes.js +117 -53
  78. package/dist/services/merge-server.js +66 -23
  79. package/dist/services/recovery-observation-budget.js +67 -7
  80. package/dist/services/recovery-snapshot-database.js +7 -4
  81. package/dist/services/review-deferral.js +23 -0
  82. package/dist/services/run-control-receipt.js +1 -1
  83. package/dist/services/run-control-worker.js +177 -118
  84. package/dist/services/run-control.js +99 -8
  85. package/dist/services/run-controller-adapter.js +177 -51
  86. package/dist/services/run-controller-capture.js +14 -2
  87. package/dist/services/run-controller-process.js +11 -4
  88. package/dist/services/run-controller-recovery.js +17 -0
  89. package/dist/services/run-controller-state.js +68 -7
  90. package/dist/services/run-controller.js +207 -107
  91. package/dist/services/run-fork.js +8 -3
  92. package/dist/services/run-recovery-guard.js +75 -0
  93. package/dist/services/run-recovery-runtime.js +21 -30
  94. package/dist/services/run-recovery.js +17 -13
  95. package/dist/services/runs.js +40 -7
  96. package/dist/services/runtime-budget.js +32 -15
  97. package/dist/services/runtime-scope-control.js +8 -0
  98. package/dist/services/runtime-scope-resume.js +30 -8
  99. package/dist/services/runtime-scope-worker.js +118 -73
  100. package/dist/services/runtime-service.js +6 -7
  101. package/dist/services/scope-writer-cleanup.js +3 -2
  102. package/dist/services/stage-context.js +135 -0
  103. package/dist/services/stage-lifecycle.js +3 -15
  104. package/dist/services/stage-pause.js +3 -2
  105. package/dist/services/usage.js +14 -3
  106. package/dist/services/verification-matrix.js +30 -1
  107. package/dist/services/vnext-code-review.js +26 -13
  108. package/dist/services/vnext-code.js +10 -8
  109. package/dist/services/vnext-contracts.js +10 -1
  110. package/dist/services/vnext-execution-profile.js +75 -2
  111. package/dist/services/vnext-fanout.js +5 -0
  112. package/dist/services/vnext-merge.js +30 -11
  113. package/dist/services/vnext-plan-review.js +102 -35
  114. package/dist/services/vnext-plan.js +9 -7
  115. package/dist/services/vnext-protocolize.js +15 -9
  116. package/dist/services/vnext-specify.js +6 -6
  117. package/dist/services/work-registry.js +43 -6
  118. package/dist/services/workspace-bootstrap.js +14 -3
  119. package/dist/shared/errors.js +11 -6
  120. package/dist/storage/agent-output.js +26 -0
  121. package/dist/storage/database.js +80 -21
  122. package/package.json +7 -4
@@ -14,6 +14,7 @@ import { appendFlowRunTimelineEvent, pauseFlowRunStage, resumeFlowRunStage } fro
14
14
  import { bindSessionForResume, refreshRunWorkProjection } from "./work-registry.js";
15
15
  import { assertRunMutationAllowed } from "./run-recovery.js";
16
16
  import { runtimeCommand } from "./runtime-command.js";
17
+ import { prepareAgentOutputFile } from "../storage/agent-output.js";
17
18
  export { flowCommand } from "./runtime-command.js";
18
19
  export function prepareStagePause(context, input) {
19
20
  if (typeof input.question !== "string" || !input.question.trim())
@@ -38,6 +39,7 @@ export function pauseStageForUser(context, input) {
38
39
  const questionPath = path.join(pauseRoot, "question.md");
39
40
  fs.mkdirSync(pauseRoot, { recursive: true });
40
41
  publishImmutableInput(questionPath, Buffer.from(question), "hitl_question_conflict");
42
+ const answerInputPath = prepareAgentOutputFile(path.join(pauseRoot, "answer-input.md"));
41
43
  const now = context.now();
42
44
  context.db.writeTransaction(() => {
43
45
  const paused = context.db.run("UPDATE works SET status = 'paused', updated_at = ? WHERE work_id = ? AND status = 'running'", [now, work.work_id]);
@@ -46,7 +48,6 @@ export function pauseStageForUser(context, input) {
46
48
  pauseFlowRunStage(context, { projectRoot, runId: run.id, stage: stage.stage, workId: work.work_id, pauseId, questionPath });
47
49
  context.db.afterCommit(() => refreshRunWorkProjection(context, project.id, run.id));
48
50
  });
49
- const answerInputPath = path.join(pauseRoot, "answer-input.md");
50
51
  const resume = stageResumeCommand(context, { runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, answerInputPath });
51
52
  const resumeTemplate = `Write the complete user answer exactly to ${quote([answerInputPath])}, then run:\n${resume}`;
52
53
  return {
@@ -146,7 +147,7 @@ export function resumeStageAfterUser(context, input) {
146
147
  export function stagePauseCommand(context, input) {
147
148
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
148
149
  const run = requireRun(context, project.id, input.runId);
149
- const questionInputPath = path.join(requireRunHome(run), "works", input.workId, "question-input.md");
150
+ const questionInputPath = prepareAgentOutputFile(path.join(requireRunHome(run), "works", input.workId, "question-input.md"));
150
151
  return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "pause", shortEntityReference(input.runId), "--stage", input.stage, "--work", shortEntityReference(input.workId), "--question-file", questionInputPath, "--project-root", input.projectRoot, "--json"]));
151
152
  }
152
153
  /** Render separate file-write and lifecycle-call instructions for HITL. */
@@ -369,7 +369,7 @@ function usageRowsForPhysicalSessions(context, links, observedAt, stage) {
369
369
  : completedTurn?.completed_at ?? sessionLinks.map((item) => item.completed_at ?? observedAt).sort().at(-1);
370
370
  const endedAt = stage?.completedAt ?? (Date.parse(naturalEnd) > Date.parse(observedAt) ? observedAt : naturalEnd);
371
371
  const harnessUsage = isExternalHarness(link.harness)
372
- ? readHarnessUsageWindow(context, link.harness, link.project_id, link.provider_session_id, startedAt, endedAt, observedAt)
372
+ ? readHarnessUsageWindow(context, link.harness, link.project_id, link.provider_session_id, startedAt, endedAt, observedAt, Boolean(stage))
373
373
  : null;
374
374
  const parsed = harnessUsage ?? readCodexTranscriptWindow(link.transcript_path, nativeSessionId(link), startedAt, endedAt);
375
375
  return {
@@ -393,18 +393,29 @@ function hasInclusiveUsageSnapshot(context, projectId, harness, providerSessionI
393
393
  return false;
394
394
  return Boolean(context.db.get("SELECT 1 FROM harness_usage_snapshots WHERE project_id = ? AND harness = ? AND provider_session_id = ? AND usage_scope = 'execution_tree_inclusive' AND observed_at <= ? LIMIT 1", [projectId, harness, providerSessionId, observedAt]));
395
395
  }
396
- function readHarnessUsageWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt) {
396
+ function readHarnessUsageWindow(context, harness, projectId, providerSessionId, startedAt, endedAt, observedAt, stageWindow) {
397
397
  if (!providerSessionId)
398
398
  return { status: "not_observable", diagnostic: "provider_session_id_missing" };
399
399
  const snapshots = context.db.all(`SELECT completeness, observed_at, total_tokens, input_tokens, cache_read_input_tokens, cache_write_input_tokens, output_tokens, reasoning_output_tokens
400
400
  FROM harness_usage_snapshots
401
401
  WHERE project_id = ? AND harness = ? AND provider_session_id = ? AND observed_at <= ?
402
402
  ORDER BY observed_at, id`, [projectId, harness, providerSessionId, observedAt]);
403
- const last = snapshots.find((snapshot) => Date.parse(snapshot.observed_at) > Date.parse(endedAt))
403
+ const last = (!stageWindow ? snapshots.find((snapshot) => Date.parse(snapshot.observed_at) > Date.parse(endedAt)) : undefined)
404
404
  ?? snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(endedAt)).at(-1);
405
405
  const before = snapshots.filter((snapshot) => Date.parse(snapshot.observed_at) <= Date.parse(startedAt)).at(-1);
406
406
  if (!last || !before || last === before)
407
407
  return { status: "not_yet_emitted", diagnostic: `${harness}_usage_window_incomplete` };
408
+ // Cumulative counters outside an attempt's boundaries cannot establish its
409
+ // spend: adjacent attempts would otherwise charge overlapping intervals.
410
+ if (stageWindow && (Date.parse(before.observed_at) !== Date.parse(startedAt)
411
+ || Date.parse(last.observed_at) !== Date.parse(endedAt)
412
+ || before.completeness !== "complete" || last.completeness !== "complete")) {
413
+ return { status: "not_observable", diagnostic: `${harness}_stage_window_unconfirmed` };
414
+ }
415
+ if (stageWindow && ["total_tokens", "input_tokens", "cache_read_input_tokens", "cache_write_input_tokens", "output_tokens", "reasoning_output_tokens"]
416
+ .some(key => last[key] !== null && before[key] !== null && last[key] < before[key])) {
417
+ return { status: "not_observable", diagnostic: `${harness}_stage_counter_reset` };
418
+ }
408
419
  if (harness === "droid-cli" && (last.completeness !== "complete" || before.completeness !== "complete"))
409
420
  return { status: "not_observable", diagnostic: "droid-cli_usage_window_partial" };
410
421
  const delta = (value, baseline) => value === null || baseline === null ? null : Math.max(0, value - baseline);
@@ -7,6 +7,7 @@ import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations }
7
7
  import { acceptStagePacket, prepareStagePacket, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
8
8
  import { assertRunMutationAllowed } from "./run-recovery.js";
9
9
  export const verificationMatrixContract = "dd-flow/verification-matrix@1";
10
+ export const finalCheckCoverageContract = "dd-flow/final-check-coverage@1";
10
11
  /** A view of already validated facts: never a gate or a receipt selector. */
11
12
  export function buildVerificationMatrix(input) {
12
13
  const sourceCatalog = new Map();
@@ -178,6 +179,10 @@ export function prepareRunVerificationMatrix(context, input) {
178
179
  let finalBindings;
179
180
  if (input.stage === "merge") {
180
181
  const gate = readSource("merge_gate", "run", "07-merge/merge-gate.json");
182
+ if (index.final_check_coverage_contract === finalCheckCoverageContract) {
183
+ for (const role of final ? ["baseline", "source", "target"] : ["baseline", "source"])
184
+ readSource(`check_profile_${role}`, "run", `07-merge/check-profile-${role}.json`);
185
+ }
181
186
  const semanticRefs = new Set(plans.flatMap(plan => plan.acceptance.flatMap(entry => entry.check_refs.map(id => `${plan.protocol_id}/${id}`))));
182
187
  policyChecks = gate.checks.filter(check => !semanticRefs.has(check.canonical_ref ?? check.id));
183
188
  if (final) {
@@ -224,6 +229,20 @@ export function prepareRunVerificationMatrix(context, input) {
224
229
  return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: "unavailable", reason: "receipt_file_missing" };
225
230
  const bytes = fs.readFileSync(receiptFile);
226
231
  sources.push({ role: `receipt:${ref}`, root: "run", path: portable.receipt_path, sha256: digest(bytes) });
232
+ const retainedReceipt = JSON.parse(bytes.toString("utf8"));
233
+ for (const artifact of retainedReceipt.artifacts ?? []) {
234
+ if (path.isAbsolute(artifact.path) || artifact.path.includes("\0") || artifact.path.split(/[\\/]/).includes(".."))
235
+ throw new AppError("verification_matrix_path_invalid", "Native artifact must remain inside its evidence directory", 1, { path: artifact.path });
236
+ const artifactPath = slash(path.join(path.dirname(portable.receipt_path), "artifacts", artifact.path));
237
+ const artifactFile = safePath(home, artifactPath);
238
+ if (fs.existsSync(artifactFile))
239
+ sources.push({ role: `artifact:${ref}:${artifact.path}`, root: "run", path: artifactPath, sha256: digest(fs.readFileSync(artifactFile)) });
240
+ }
241
+ if (receipt.status !== "aborted" && fs.existsSync(receipt.completion_path)) {
242
+ const completionPath = portable.completion_path;
243
+ const completionFile = safePath(home, completionPath);
244
+ sources.push({ role: `completion:${ref}`, root: "run", path: completionPath, sha256: digest(fs.readFileSync(completionFile)) });
245
+ }
227
246
  return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: valid ? "retained" : "unavailable", ...(!valid ? { reason: "receipt_evidence_invalid" } : {}) };
228
247
  });
229
248
  const works = context.db.all("SELECT work_id, payload_json FROM works WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]).flatMap(work => {
@@ -238,6 +257,8 @@ export function prepareRunVerificationMatrix(context, input) {
238
257
  const markdown = prepareStagePacket(safePath(home, `${root}/verification-matrix.md`), Buffer.from(renderVerificationMatrix(matrix)));
239
258
  packets.push(json, markdown);
240
259
  const metadata = { contract: verificationMatrixContract, role: header.role, completeness: header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(home, json.file)), sha256: json.sha256 }, markdown: { path: slash(path.relative(home, markdown.file)), sha256: markdown.sha256 } };
260
+ if (index.final_check_coverage_contract === finalCheckCoverageContract)
261
+ metadata.coverage_contract = finalCheckCoverageContract;
241
262
  const predecessors = (index.stage_runs ?? []).filter(item => item.stage !== input.stage && item.status === "done").map(item => ({ stage: item.stage, ...(item.attempt ? { attempt: item.attempt } : {}), ...(item.data_sha256 ? { data_sha256: item.data_sha256 } : {}) }));
242
263
  return { projectId: input.projectId, runId: input.runId, stage: input.stage, home, attempt, generation, packets, matrix, metadata, predecessors };
243
264
  }
@@ -297,6 +318,8 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
297
318
  const md = { file: mdFile, bytes: mdBytes, sha256: paired.sha256 };
298
319
  const json = { file: packet.file, bytes, sha256: packet.sha256 };
299
320
  const metadata = { contract: verificationMatrixContract, role: matrix.header.role, completeness: matrix.header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(run.run_root, packet.file)), sha256: packet.sha256 }, markdown: { path: slash(path.relative(run.run_root, mdFile)), sha256: md.sha256 } };
321
+ if (index.final_check_coverage_contract === finalCheckCoverageContract)
322
+ metadata.coverage_contract = finalCheckCoverageContract;
300
323
  const sourcePackets = matrix.sources.filter(source => (source.role.startsWith("plan:") || source.role === "code_work_batch") && source.root === "run").map(source => {
301
324
  const file = safePath(run.run_root, source.path, false);
302
325
  const retained = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && resolveStagePublicationPath(context, projectId, runId, item.file) === file && item.sha256 === source.sha256);
@@ -314,7 +337,13 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
314
337
  }
315
338
  function portableReceipt(receipt, home) {
316
339
  const relative = (file) => slash(path.relative(home, safePath(home, path.relative(home, file))));
317
- return { ...receipt, receipt_path: relative(receipt.receipt_path), completion_path: relative(receipt.completion_path), stdout_path: relative(receipt.stdout_path), stderr_path: relative(receipt.stderr_path), artifacts: receipt.artifacts.map(item => ({ ...item, path: relative(item.path) })) };
340
+ return { ...receipt, receipt_path: relative(receipt.receipt_path), completion_path: relative(receipt.completion_path), stdout_path: relative(receipt.stdout_path), stderr_path: relative(receipt.stderr_path), artifacts: receipt.artifacts.map(item => {
341
+ const artifactPath = path.isAbsolute(item.path) ? path.relative(path.join(path.dirname(receipt.receipt_path), "artifacts"), item.path) : item.path;
342
+ if (path.isAbsolute(artifactPath) || artifactPath.includes("\0") || artifactPath.split(/[\\/]/).includes(".."))
343
+ throw new AppError("verification_matrix_path_invalid", "Native artifact must remain inside its evidence directory", 1, { path: item.path });
344
+ safePath(home, path.join(path.dirname(relative(receipt.receipt_path)), "artifacts", artifactPath));
345
+ return { ...item, path: slash(artifactPath) };
346
+ }) };
318
347
  }
319
348
  function safePath(root, relative, inspectFilesystem = true) {
320
349
  if (path.isAbsolute(relative) || relative.split(/[\\/]/).includes(".."))
@@ -18,7 +18,7 @@ import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
18
18
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
19
19
  import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
20
20
  import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix, publishVerificationMatrix } from "./verification-matrix.js";
21
- import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
21
+ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
22
22
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
23
23
  import { stageGraphScope } from "./stage-work-graph.js";
24
24
  import { prepareVnextMergeRequest } from "./vnext-merge.js";
@@ -26,6 +26,8 @@ import { validateVnextCodeHandoff } from "./vnext-plan.js";
26
26
  import { assertRunMutationAllowed } from "./run-recovery.js";
27
27
  import { prepareJsonFile, prepareOutputFile } from "../cli/input-preparation.js";
28
28
  import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
29
+ import { reviewDefEvidence } from "./review-deferral.js";
30
+ import { stageSuccessor } from "./vnext-contracts.js";
29
31
  const stage = "code-review";
30
32
  const stageDir = vnextStageDirectory(stage);
31
33
  const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
@@ -75,11 +77,11 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
75
77
  if (prepared.resumed) {
76
78
  if (effectiveMode(prepared.home, run).mode !== "off")
77
79
  resumeVerificationMatrix(context, project.id, run.id, stage);
78
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
80
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
79
81
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
80
- const prompt = fs.readFileSync(existingPrompt, "utf8");
82
+ const delivered = currentStagePrompt(context, existingPrompt);
81
83
  const orchestration = prepared.orchestration;
82
- return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
84
+ return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.prior, id: binding.work_session_id, ...delivered, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
83
85
  }
84
86
  const { mode, cycle, groups, batch, items, template, checks, codeReportSha256 } = prepared;
85
87
  prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" }, true);
@@ -95,17 +97,17 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
95
97
  fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
96
98
  const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n${stageLifecycleInstruction}\n`;
97
99
  fs.writeFileSync(promptPath, prompt);
98
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
100
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
99
101
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
100
102
  const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" };
101
103
  attachFlowRunStage(context, attachmentInput, prepareFlowRunStageAttachment(context, attachmentInput));
102
- return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
104
+ return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
103
105
  }
104
106
  const batchFile = path.join(root, "review-work-batch.json");
105
107
  fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
106
108
  const prompt = `${orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks })}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, prepared.home, "reviewing whether current evidence proves the accepted obligations")}\n`;
107
109
  fs.writeFileSync(promptPath, prompt);
108
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
110
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
109
111
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
110
112
  const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items, stage, cycle });
111
113
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
@@ -113,7 +115,7 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
113
115
  const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
114
116
  commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
115
117
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
116
- return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
118
+ return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
117
119
  }
118
120
  export function prepareVnextCodeReviewRepair(context, input) {
119
121
  if (!Array.isArray(input.findingIds) || !input.findingIds.length || input.findingIds.some(id => typeof id !== "string" || !id.trim()))
@@ -196,6 +198,7 @@ export async function finishVnextCodeReview(context, input) {
196
198
  validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
197
199
  const cycle = reviewContext.cycle ?? "initial";
198
200
  const reviewers = reviewerWorks(context, project.id, run.id, cycle);
201
+ const defEvidence = decision.findings.filter(item => item.disposition === "defer").map(item => reviewDefEvidence(run.workspace_root, item.def_id));
199
202
  if (mode !== "off") {
200
203
  assertCodeReviewCoverage(reviewGroups(home), reviewers);
201
204
  const incomplete = reviewers.filter((work) => work.status !== "completed");
@@ -204,6 +207,8 @@ export async function finishVnextCodeReview(context, input) {
204
207
  for (const work of reviewers)
205
208
  validateReviewerResult(context, work, run);
206
209
  const canonical = reviewDecisionForRepair(decision, reviewers);
210
+ if (reviewContext.accepted_decision && JSON.stringify(reviewContext.def_evidence ?? []) !== JSON.stringify(defEvidence))
211
+ throw new AppError("deferral_invalid", "Accepted DEF evidence changed during review repair", 2);
207
212
  const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
208
213
  let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
209
214
  const frozenShaFile = path.join(root, ".decision-sha256");
@@ -215,7 +220,7 @@ export async function finishVnextCodeReview(context, input) {
215
220
  // Accept the value once. Repairs consume this decision, not a mutable
216
221
  // operator file. Reuse the existing context artifact and review cycle.
217
222
  const contextFile = path.join(root, "review-context.json"), temporary = `${contextFile}.tmp-${crypto.randomUUID()}`;
218
- fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
223
+ fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision, def_evidence: defEvidence }, null, 2)}\n`);
219
224
  fs.renameSync(temporary, contextFile);
220
225
  const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, decisionReasonsByFinding: Object.fromEntries(canonical.decision.findings.map((item) => [item.finding_ref, item.reason])), objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
221
226
  const retained = latestRunVerificationMatrix(context, project.id, run.id, stage, "input");
@@ -263,6 +268,9 @@ export async function finishVnextCodeReview(context, input) {
263
268
  const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
264
269
  if (matrix)
265
270
  Object.assign(report.semantic, { verification_matrix: matrix.metadata });
271
+ if (JSON.stringify(defEvidence) !== JSON.stringify(decision.findings.filter(item => item.disposition === "defer").map(item => reviewDefEvidence(run.workspace_root, item.def_id))))
272
+ throw new AppError("deferral_invalid", "DEF evidence changed while final checks ran", 2);
273
+ Object.assign(report.semantic, { def_evidence: defEvidence });
266
274
  const preparedReport = prepareStageReport(report);
267
275
  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 };
268
276
  withStageSettlement(context, () => {
@@ -286,7 +294,7 @@ export async function finishVnextCodeReview(context, input) {
286
294
  }
287
295
  refreshRunWorkProjection(context, project.id, run.id);
288
296
  });
289
- return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } } : {}) };
297
+ return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: stageSuccessor(context, { projectRoot, runId: run.id, stage: "merge", command: runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"]) }) } : {}) };
290
298
  }
291
299
  export function readCodeReviewDecision(file, accepted, explicit = false, prepared) {
292
300
  if (!accepted)
@@ -356,7 +364,10 @@ export function assertCodeReviewCoverage(groups, reviewers) {
356
364
  export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Verify that universal and exclusive constraints (only, any, all, never, whole) and stated exceptions were not narrowed. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Evidence closes a claim only when it exercises the named failure mechanism; a sequential negative test does not prove a concurrency race, and proof limits cannot waive an accepted obligation. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
357
365
  function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE passed its declared checks; each check proves only its named contour and proof limits, not complete semantic acceptance. Conduct independent quality review and do not manually rerun every declared check.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", "Ready reviewer Works: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]), `Write the decision to this exact absolute path, not a relative look-alike: ${decision}`, `Finish only after every reviewer Work settles and that exact file exists: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Do not invoke that Work's start_command from the coordinator Session: end this Turn so the controller can launch a fresh child, then call the returned next.finish_command after it completes. Internal invocation IDs are runtime-owned and must not be copied or reused. Review is not repeated.", "If the aggregate gate fails with repair_required, its repair Work is already registered with all causal checks. End this Turn so the controller launches a fresh child. Do not copy receipt IDs, create another repair, invoke its start_command in the coordinator, or edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
358
366
  function canonicalReviewEvidence(decision, reviewers) {
359
- const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
367
+ const known = new Map(canonicalCodeFindings(reviewers).map((item) => [item.finding_ref, item.finding]));
368
+ const refs = decision.findings.map(item => item.finding_ref);
369
+ if (new Set(refs).size !== refs.length || refs.some(ref => !known.has(ref)))
370
+ throw new AppError("review_evidence_invalid", "CODE decisions must use unique known canonical finding references", 2, { finding_refs: refs });
360
371
  const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
361
372
  const duplicates = new Set();
362
373
  for (const item of decision.findings.filter((entry) => entry.disposition === "duplicate")) {
@@ -372,6 +383,9 @@ function canonicalReviewEvidence(decision, reviewers) {
372
383
  }
373
384
  if (!cursor)
374
385
  throw new AppError("review_evidence_invalid", "Duplicate finding chain must end at a decided canonical finding", 2, { finding_ref: item.finding_ref });
386
+ if (["p0", "p1"].includes(known.get(item.finding_ref).priority) && cursor.disposition !== "fix") {
387
+ throw new AppError("review_material_disposition_invalid", "P0/P1 duplicate findings must resolve to a fixed canonical finding", 2, { finding_ref: item.finding_ref, canonical_finding_ref: cursor.finding_ref, disposition: cursor.disposition });
388
+ }
375
389
  duplicates.add(item.finding_ref);
376
390
  }
377
391
  return {
@@ -422,8 +436,7 @@ function validateDecision(context, input) {
422
436
  if (item.disposition === "fix" && !repaired.some((work) => requiredChecks.every((ref) => reviewCheckRefs(work).includes(ref))))
423
437
  throw new AppError("review_repair_check_missing", "A fixed CODE-REVIEW finding needs a repair Work that retains every selected causal check", 2, { finding_ref: findingRef, check_refs: requiredChecks });
424
438
  if (finding.priority === "p2" && item.disposition === "defer") {
425
- if (!item.def_id || !fs.existsSync(path.join(input.workspaceRoot, ".memory-bank", "defs", `${item.def_id}.md`)))
426
- throw new AppError("deferral_invalid", "A deferred P2 requires a named durable DEF", 2, { finding_ref: findingRef, def_id: item.def_id ?? null });
439
+ reviewDefEvidence(input.workspaceRoot, item.def_id);
427
440
  }
428
441
  if (finding.priority === "p3" && item.disposition === "defer")
429
442
  throw new AppError("deferral_invalid", "P3 observations are not deferred as DEF", 2, { finding_ref: findingRef });
@@ -4,6 +4,7 @@ import { ensureAggregateRepair, ensureRepairIntent } from "./repair-intents.js";
4
4
  import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
5
5
  import crypto from "node:crypto";
6
6
  import fs from "node:fs";
7
+ import { prepareAgentOutputFile } from "../storage/agent-output.js";
7
8
  import path from "node:path";
8
9
  import { AppError } from "../shared/errors.js";
9
10
  import { resolveProjectRoot } from "../storage/paths.js";
@@ -21,11 +22,12 @@ import { vnextStageDirectory } from "../domain/stage-catalog.js";
21
22
  import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
22
23
  import { prepareVnextMergeRequest } from "./vnext-merge.js";
23
24
  import { assertStageStartHookEvent } from "./hooks.js";
24
- import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
25
+ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
25
26
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
26
27
  import { assertRunMutationAllowed } from "./run-recovery.js";
27
28
  import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
28
29
  import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, publishVerificationMatrix, latestRunVerificationMatrix } from "./verification-matrix.js";
30
+ import { stageSuccessor } from "./vnext-contracts.js";
29
31
  const stage = "code";
30
32
  const stageDir = vnextStageDirectory("code");
31
33
  /** A controller prepares slow, deterministic project tooling before it asks an
@@ -93,12 +95,13 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
93
95
  const { projectRoot, project, run, root, promptPath, rootWork, sourceRepairAttempt } = prepared;
94
96
  assertRunMutationAllowed(context, project.id, run.id);
95
97
  assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
98
+ prepareAgentOutputFile(path.join(requireHome(run), "works", rootWork.work_id, "block-summary.md"));
96
99
  if (prepared.resumed) {
97
100
  resumeVerificationMatrix(context, project.id, run.id, stage);
98
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
101
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
99
102
  bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
100
103
  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) });
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")) } };
104
+ return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, ...currentStagePrompt(context, promptPath), ...(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")) } };
102
105
  }
103
106
  if (!sourceRepairAttempt)
104
107
  prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" }, true);
@@ -108,7 +111,7 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
108
111
  const graph = codeWorkGraph(context, project.id, run.id);
109
112
  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`;
110
113
  fs.writeFileSync(promptPath, prompt);
111
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
114
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
112
115
  const binding = bindStageCoordinatorWork(context, {
113
116
  workId: rootWork.work_id,
114
117
  hookEventId: input.hookEventId,
@@ -139,8 +142,7 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
139
142
  stage,
140
143
  root_work_id: rootWork.work_id,
141
144
  id: binding.work_session_id,
142
- prompt_path: promptPath,
143
- worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"),
145
+ ...currentStagePrompt(context, promptPath),
144
146
  ...(externalContext ? { external_context: externalContext } : {}),
145
147
  graph,
146
148
  orchestration,
@@ -332,7 +334,7 @@ export async function finishVnextCode(context, input) {
332
334
  outcome: "accepted",
333
335
  report_path: path.join(root, "stage-report.json"),
334
336
  next_action: next,
335
- ...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "code-review", "--project-root", projectRoot, "--json"])) } } : next === "start_merge" ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } } : {})
337
+ ...(next === "start_code_review" ? { next: stageSuccessor(context, { projectRoot, runId: run.id, stage: "code-review", command: runtimeCommand(context, ["stage", "start", run.id, "--stage", "code-review", "--project-root", projectRoot, "--json"]) }) } : next === "start_merge" ? { merge_request: mergeRequest, next: stageSuccessor(context, { projectRoot, runId: run.id, stage: "merge", command: runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"]) }) } : {})
336
338
  };
337
339
  }
338
340
  export function registerAggregateCodeRepair(context, input) {
@@ -539,7 +541,7 @@ function receiptRepairPaths(workspaceRoot, receipt) {
539
541
  }
540
542
  return [...paths].sort();
541
543
  }
542
- function coordinatorPrompt(context, input) {
544
+ export function coordinatorPrompt(context, input) {
543
545
  const { graph, capacity, template } = input;
544
546
  return [
545
547
  "<stage_identity>",
@@ -1 +1,10 @@
1
- export {};
1
+ import { isControllerManagedInvocation, managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ /** Successor authority belongs to the controller, not the finishing Session.
3
+ * Manual flows still need an executable next step. Current-stage finish and
4
+ * paused/child Work commands do not pass through this boundary. */
5
+ export function stageSuccessor(context, input) {
6
+ const next = { ...input.metadata, kind: "start_stage", stage: input.stage };
7
+ return isControllerManagedInvocation(context, input.projectRoot, input.runId)
8
+ ? { ...next, controller_owned: true, stop_turn: true, instruction: "This Stage is complete. End this Turn now and await the controller's next Stage assignment. Do not start the successor Stage or prepare its command." }
9
+ : { ...next, command: managedLifecycleCommand(context, input.command) };
10
+ }
@@ -1,18 +1,89 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { isDeepStrictEqual } from "node:util";
3
5
  import { AppError } from "../shared/errors.js";
6
+ import { canonicalPath } from "../storage/paths.js";
4
7
  import { isExecutionRouting } from "./execution-policy.js";
8
+ export const RUN_OPERATIONAL_DECISION_CONTRACT = "run-operational-decision@1";
9
+ const object = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
10
+ const text = (value) => typeof value === "string" && Boolean(value.trim());
11
+ const keys = (value, allowed) => object(value) && Object.keys(value).every(key => allowed.includes(key));
12
+ /** Structural validation only: acceptance is supplied by the caller, never inferred from settings. */
13
+ export function validateRunOperationalDecision(value, settings) {
14
+ if (!object(value) || value.schema_id !== RUN_OPERATIONAL_DECISION_CONTRACT || !text(value.id)
15
+ || !object(value.scope) || !text(value.scope.project_root) || !path.isAbsolute(value.scope.project_root)
16
+ || [value.scope.eval_id, value.scope.execution_id].some(item => item !== undefined && !text(item))
17
+ || !object(value.settings) || !object(value.settings.merge_delivery) || value.settings.merge_delivery.strategy !== settings.merge_delivery.strategy
18
+ || !object(value.settings.merge_cleanup) || value.settings.merge_cleanup.source !== settings.merge_cleanup.source
19
+ || !object(value.policy) || !text(value.policy.path) || !text(value.policy.sha256) || !/^[a-f0-9]{64}$/.test(value.policy.sha256)
20
+ || !Array.isArray(value.policy.clauses) || !value.policy.clauses.length || !value.policy.clauses.every(text)
21
+ || !object(value.acceptance) || !text(value.acceptance.source) || !text(value.acceptance.reference)
22
+ || value.exception !== undefined && (!object(value.exception) || !text(value.exception.reason) || !text(value.exception.owner)
23
+ || !text(value.exception.review_at) || !Number.isFinite(Date.parse(value.exception.review_at))
24
+ || value.exception.expires_at !== undefined && (!text(value.exception.expires_at) || !Number.isFinite(Date.parse(value.exception.expires_at))))) {
25
+ throw new AppError("operational_decision_invalid", "Operational decision requires explicit acceptance, scoped authority and matching settings", 2);
26
+ }
27
+ if (!keys(value, ["schema_id", "id", "scope", "settings", "policy", "acceptance", "exception", "declaration"])
28
+ || !keys(value.scope, ["project_root", "eval_id", "execution_id"])
29
+ || !keys(value.settings, ["merge_delivery", "merge_cleanup"])
30
+ || !keys(value.settings.merge_delivery, ["strategy"]) || !keys(value.settings.merge_cleanup, ["source"])
31
+ || !keys(value.policy, ["path", "sha256", "clauses"]) || !keys(value.acceptance, ["source", "reference"])
32
+ || value.exception !== undefined && !keys(value.exception, ["reason", "owner", "review_at", "expires_at"])
33
+ || value.declaration !== undefined && !keys(value.declaration, ["bytes", "sha256"]))
34
+ throw new AppError("operational_decision_invalid", "Operational decision contains unsupported authority fields", 2);
35
+ if (value.declaration !== undefined) {
36
+ if (!object(value.declaration) || !text(value.declaration.bytes) || !text(value.declaration.sha256)
37
+ || crypto.createHash("sha256").update(value.declaration.bytes).digest("hex") !== value.declaration.sha256)
38
+ throw new AppError("operational_decision_invalid", "Original operational declaration checksum does not match", 2);
39
+ let declaration;
40
+ try {
41
+ declaration = JSON.parse(value.declaration.bytes);
42
+ }
43
+ catch {
44
+ throw new AppError("operational_decision_invalid", "Original operational declaration must be JSON", 2);
45
+ }
46
+ if (!object(declaration) || !keys(declaration, ["schema_id", "id", "scope", "settings", "policy", "acceptance", "exception"])
47
+ || declaration.scope !== undefined && !keys(declaration.scope, ["project_root", "eval_id", "execution_id"])
48
+ || ["schema_id", "id", "settings", "policy", "acceptance", "exception"].some(key => !isDeepStrictEqual(declaration[key], value[key])))
49
+ throw new AppError("operational_decision_invalid", "Materialized decision changed the accepted declaration", 2);
50
+ }
51
+ return value;
52
+ }
53
+ /** Machine validation of declared policy bytes, not an NLP interpretation of Markdown. */
54
+ export function assertOperationalDecisionSource(decision, projectRoot, now) {
55
+ if (canonicalPath(decision.scope.project_root, false) !== canonicalPath(projectRoot, false))
56
+ throw new AppError("operational_decision_scope_mismatch", "Operational decision belongs to another project", 2);
57
+ const file = path.resolve(projectRoot, decision.policy.path);
58
+ const relative = path.relative(fs.realpathSync(projectRoot), fs.existsSync(file) ? fs.realpathSync(file) : file);
59
+ if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || relative === ".." || !fs.existsSync(file) || crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") !== decision.policy.sha256)
60
+ throw new AppError("operational_decision_policy_changed", "Operational decision policy source checksum does not match an owned file", 2, { file });
61
+ if (decision.exception?.expires_at && Date.parse(decision.exception.expires_at) <= Date.parse(now))
62
+ throw new AppError("operational_decision_expired", "Explicit operational decision expiration has passed", 2);
63
+ }
5
64
  const relativePath = path.join(".memory-bank", "dd-flow", "project-execution.json");
6
65
  /** The project owns defaults; a RUN snapshots them once and never re-infers them. */
7
66
  export function loadVnextExecutionProfile(projectRoot) {
67
+ return loadVnextExecutionProfileSnapshot(projectRoot).profile;
68
+ }
69
+ /** Parse and identify the same source bytes; a second read cannot bind another declaration. */
70
+ export function loadVnextExecutionProfileSnapshot(projectRoot) {
8
71
  const file = path.join(projectRoot, relativePath);
72
+ let bytes;
9
73
  let value;
10
74
  try {
11
- value = JSON.parse(fs.readFileSync(file, "utf8"));
75
+ bytes = fs.readFileSync(file);
76
+ value = JSON.parse(bytes.toString("utf8"));
12
77
  }
13
78
  catch (error) {
14
79
  throw new AppError("execution_profile_missing", "vNext flow requires .memory-bank/dd-flow/project-execution.json", 1, { file, cause: String(error) });
15
80
  }
81
+ return { profile: validateVnextExecutionProfile(value), bytes };
82
+ }
83
+ /** Pure shared admission contract for project-execution@2 and @3. */
84
+ export function validateVnextExecutionProfile(value) {
85
+ if (!object(value))
86
+ throw new AppError("execution_profile_invalid", "Project execution profile must be an object", 1);
16
87
  const profile = value;
17
88
  if ((profile.schema_id !== "dd-flow/project-execution@2" && profile.schema_id !== "dd-flow/project-execution@3")
18
89
  || (profile.stage_session_mode !== undefined && profile.stage_session_mode !== "same_session" && profile.stage_session_mode !== "new_session")
@@ -25,8 +96,10 @@ export function loadVnextExecutionProfile(projectRoot) {
25
96
  || !profile.code_bootstrap || typeof profile.code_bootstrap.command !== "string" || !profile.code_bootstrap.command.trim()
26
97
  || typeof profile.code_bootstrap.policy_ref !== "string" || !profile.code_bootstrap.policy_ref.trim()
27
98
  || (profile.execution === undefined ? profile.schema_id === "dd-flow/project-execution@3" : !isExecutionRouting(profile.execution))) {
28
- throw new AppError("execution_profile_invalid", "Project execution profile does not match its supported dd-flow schema", 1, { file });
99
+ throw new AppError("execution_profile_invalid", "Project execution profile does not match its supported dd-flow schema", 1);
29
100
  }
101
+ if (profile.operational_decision !== undefined)
102
+ validateRunOperationalDecision(profile.operational_decision, profile);
30
103
  return { ...profile, stage_session_mode: profile.stage_session_mode ?? "same_session", code_review_mode: profile.code_review_mode ?? "off" };
31
104
  }
32
105
  export function executionProfilePath(projectRoot) { return path.join(projectRoot, relativePath); }
@@ -10,6 +10,7 @@ import { storageSessionId } from "./session-identity.js";
10
10
  import { reconcileUnfinishedChecks } from "./code-checks.js";
11
11
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
12
12
  import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
13
+ import { eligibleCodexChildOverload } from "./codex-capacity.js";
13
14
  export const subagentCapacityKey = "runtime.subagent.available_slots";
14
15
  /** External concurrency is policy, never evidence of native child qualification. */
15
16
  export function getStageFanoutCapacity(context, input) {
@@ -143,6 +144,10 @@ export async function reconcileVnextFanout(context, input) {
143
144
  continue;
144
145
  }
145
146
  const link = active[0];
147
+ if (value.harness_id === "codex-desktop" && eligibleCodexChildOverload(child, value.parent_session_id)) {
148
+ issues.push({ code: "child_capacity_pending", work_id: link.work_id, session_id: child.session_id });
149
+ continue;
150
+ }
146
151
  const pendingCheck = () => context.db.get("SELECT 1 FROM check_receipts WHERE project_id = ? AND run_id = ? AND work_id = ? AND status = 'running' LIMIT 1", [project.id, input.runId, link.work_id]);
147
152
  if (pendingCheck()) {
148
153
  const run = context.db.get("SELECT workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);