@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
@@ -17,13 +17,14 @@ import { runtimeCommand, checkRetryArgs, recoveryEvidenceInput } from "./runtime
17
17
  import { acceptStageReport, prepareStageReport, publishStageReport } from "./stage-report-renderer.js";
18
18
  import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
19
19
  import { addVnextCodeRepair } from "./vnext-code.js";
20
- import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations, mergeCheckProfileHash, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
21
- import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
20
+ import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations, mergeCheckProfileHash, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
21
+ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
22
22
  import { ensureLaneWorkspace, prepareLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
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
26
  import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix, publishVerificationMatrix } from "./verification-matrix.js";
27
+ import { stageSuccessor } from "./vnext-contracts.js";
27
28
  const stage = "merge";
28
29
  const stageDir = vnextStageDirectory(stage);
29
30
  export function isVnextMergeRun(context, input) {
@@ -64,6 +65,8 @@ export function prepareVnextMergeRequest(context, input) {
64
65
  validateMergeAcceptance(run.workspace_root, protocols, semantic);
65
66
  const acceptanceRefs = mergeAcceptanceRefs(run.workspace_root, protocols);
66
67
  const effective = effectiveCheckDeclarations(targetWorkspace, semantic, ["work", "code", "readiness", "merge"]);
68
+ for (const check of effective)
69
+ validateCheckDeclaration(run.workspace_root, check);
67
70
  if (run.workspace_root !== targetWorkspace && effective.length === 0)
68
71
  throw new AppError("merge_gate_missing", "A real source/target integration requires at least one semantic or policy merge check", 2, { run_id: run.id, protocol_ids: protocols });
69
72
  const ignored = ignoredGitPaths(context, project.id);
@@ -73,6 +76,8 @@ export function prepareVnextMergeRequest(context, input) {
73
76
  const gateFile = path.join(freezeRoot, "merge-gate.json");
74
77
  fs.mkdirSync(freezeRoot, { recursive: true });
75
78
  const profileHash = mergeCheckProfileHash(run.workspace_root, targetWorkspace, effective);
79
+ retainMergeProfile(freezeRoot, "baseline", targetWorkspace);
80
+ retainMergeProfile(freezeRoot, "source", run.workspace_root);
76
81
  freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, acceptanceRefs, profileHash, now: context.now() });
77
82
  let sourceCommit;
78
83
  let sourcePaths;
@@ -172,11 +177,11 @@ export async function startVnextMerge(context, input, prepared = prepareVnextMer
172
177
  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`;
173
178
  if (existingPrompt === null)
174
179
  fs.writeFileSync(promptPath, prompt);
175
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
180
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
176
181
  const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" };
177
182
  const attachment = alreadyAttached ? null : prepareFlowRunStageAttachment(context, attachmentInput);
178
183
  commitVerificationMatrixStart(context, prepared.matrix, () => attachment ? attachFlowRunStage(context, attachmentInput, attachment) : undefined);
179
- return startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
184
+ return startPacket(context, run, request, binding, promptPath, resultPath, projectRoot, checks, externalContext);
180
185
  }
181
186
  const current = requireRequest(context, request.merge_request_id);
182
187
  const targetHead = gitValue(current.target_workspace, ["rev-parse", current.target_branch], true);
@@ -192,13 +197,13 @@ export async function startVnextMerge(context, input, prepared = prepareVnextMer
192
197
  const started = startStageCoordinatorWork(context, { workId: current.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
193
198
  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`;
194
199
  fs.writeFileSync(promptPath, prompt);
195
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
200
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
196
201
  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]);
197
202
  const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" };
198
203
  const attachment = alreadyAttached ? null : prepareFlowRunStageAttachment(context, attachmentInput);
199
204
  commitVerificationMatrixStart(context, prepared.matrix, () => attachment ? attachFlowRunStage(context, attachmentInput, attachment) : undefined);
200
205
  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 });
201
- return startPacket(context, run, current, started, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
206
+ return startPacket(context, run, current, started, promptPath, resultPath, projectRoot, checks, externalContext);
202
207
  }
203
208
  catch (error) {
204
209
  const latest = requireRequest(context, request.merge_request_id);
@@ -212,8 +217,8 @@ export async function startVnextMerge(context, input, prepared = prepareVnextMer
212
217
  throw error;
213
218
  }
214
219
  }
215
- function startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, checks, externalContext) {
216
- return { ok: true, run_id: run.id, stage, merge_request_id: request.merge_request_id, work_id: request.executor_work_id, session_binding: binding.session_binding ?? binding, prompt_path: promptPath, result_path: resultPath, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), queue: queueStatus(context, request), effective_checks: checks, next: { apply_command: applyCommand(context, request, projectRoot), finish_command: finishCommand(context, run.id, request, projectRoot) } };
220
+ function startPacket(context, run, request, binding, promptPath, resultPath, projectRoot, checks, externalContext) {
221
+ return { ok: true, run_id: run.id, stage, merge_request_id: request.merge_request_id, work_id: request.executor_work_id, session_binding: binding.session_binding ?? binding, result_path: resultPath, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}), queue: queueStatus(context, request), effective_checks: checks, next: { apply_command: applyCommand(context, request, projectRoot), finish_command: finishCommand(context, run.id, request, projectRoot) } };
217
222
  }
218
223
  export function applyVnextMerge(context, input) {
219
224
  const existing = requireRequest(context, input.requestId);
@@ -294,6 +299,9 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
294
299
  const gate = prepared.gate, checks = gate.checks;
295
300
  if (input.retryCheckId)
296
301
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
302
+ for (const check of checks)
303
+ validateCheckDeclaration(request.target_workspace, check);
304
+ retainMergeProfile(path.join(requireHome(run), stageDir), "target", request.target_workspace);
297
305
  const acceptancePath = path.join(requireHome(run), stageDir, "merge-gate-acceptance.json");
298
306
  const recovered = readMergeGateAcceptance(context, acceptancePath, request, gate);
299
307
  const resumingGate = ["checks_passed", "integration_committed", "delivered"].includes(request.checkpoint);
@@ -463,7 +471,7 @@ export async function repairVnextMerge(context, input) {
463
471
  if (request.project_id === project.id && request.status === "superseded" && errorCode(request) === "merge_source_repair") {
464
472
  const saved = JSON.parse(request.last_error_json);
465
473
  releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
466
- return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: { repair_work_id: saved.repair_work_id }, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } };
474
+ return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: { repair_work_id: saved.repair_work_id }, next: stageSuccessor(context, { projectRoot, runId: request.run_id, stage: "code", command: runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"]) }) };
467
475
  }
468
476
  if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
469
477
  throw new AppError("invalid_merge_repair_state", "MERGE source repair requires an action-required failed integration gate", 2, { merge_request_id: input.requestId, status: request.status, code: errorCode(request) });
@@ -487,7 +495,7 @@ export async function repairVnextMerge(context, input) {
487
495
  context.db.run("UPDATE merge_requests SET status = 'superseded', checkpoint = 'source_repair_required', last_error_json = ?, completed_at = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_source_repair", replacement_pending: true, failed_receipt_ids: failed, repair_work_id: repair.repair_work_id }), now, now, request.merge_request_id]);
488
496
  releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
489
497
  appendFlowRunTimelineEvent(context, project.id, request.run_id, { type: "merge_source_repair_created", merge_request_id: request.merge_request_id, repair_work_id: repair.repair_work_id, cycle: prepared.cycle, failed_receipt_ids: failed });
490
- return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: repair, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } };
498
+ return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: repair, next: stageSuccessor(context, { projectRoot, runId: request.run_id, stage: "code", command: runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"]) }) };
491
499
  }
492
500
  function mergePrompt(context, input, checks, settings) { const pause = stagePauseCommand(context, { runId: input.run.id, stage: "merge", workId: input.request.executor_work_id, projectRoot: input.projectRoot }); const repair = repairCommand(context, input.request, input.projectRoot); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(settings.merge_delivery)}`, `- cleanup: ${JSON.stringify(settings.merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...checks.map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails, read its receipt and logs and determine the cause. For a product defect use ${repair}; it restores the target baseline and opens CODE → independent CODE-REVIEW → replacement MRG. For an environment failure restore the declared environment in this MERGE and write the returned recovery JSON file and use continuation.command_after_input. The CLI reruns the real gate and retains the old receipt. Never edit product code in the integration target to make a gate pass.`, "If a material conflict has no reasonable answer in accepted evidence, pause this same Work by writing the question file and then running the separate standalone command below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
493
501
  function mergeReport(context, run, request, semantic, receipts) {
@@ -501,7 +509,7 @@ function mergeReport(context, run, request, semantic, receipts) {
501
509
  }
502
510
  function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
503
511
  function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
504
- return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.availability === "available").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
512
+ throw new AppError("merge_plan_missing", "MERGE requires every accepted PLAN", 2, { protocol_id: protocol }); const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
505
513
  function validateMergeAcceptance(workspace, protocols, checks) { const known = new Set(checks.map((check) => check.canonical_ref)); const missing = mergeAcceptanceRefs(workspace, protocols).filter((ref) => !known.has(ref)); if (missing.length)
506
514
  throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a missing check", 2, { check_refs: missing }); }
507
515
  function freezeMergeGate(file, input) { const canonical = JSON.stringify({ checks: input.checks, acceptance_refs: [...input.acceptanceRefs].sort() }); const hash = cryptoHash(canonical); if (fs.existsSync(file)) {
@@ -519,6 +527,17 @@ function readFrozenMergeGate(file, requestId) { try {
519
527
  catch (cause) {
520
528
  throw new AppError("merge_gate_missing", "MERGE gate is missing or invalid", 1, { merge_request_id: requestId, file, cause: String(cause) });
521
529
  } }
530
+ function retainMergeProfile(root, role, workspace) {
531
+ const { profile, hash, file: profileFile } = readCodeCheckProfile(workspace);
532
+ const file = path.join(root, `check-profile-${role}.json`);
533
+ const bytes = `${JSON.stringify({ schema_id: "dd-flow/retained-check-profile@1", profile, profile_sha256: hash, profile_bytes: profile ? fs.readFileSync(profileFile, "utf8") : null }, null, 2)}\n`;
534
+ if (fs.existsSync(file)) {
535
+ if (fs.readFileSync(file, "utf8") !== bytes)
536
+ throw new AppError("check_profile_drift", "Retained MERGE profile changed", 2, { role, file });
537
+ }
538
+ else
539
+ fs.writeFileSync(file, bytes, { flag: "wx" });
540
+ }
522
541
  function frozenGateRefs(gate) { return [...new Set([...gate.checks.map(check => check.canonical_ref ?? check.id), ...gate.acceptanceRefs])].sort(); }
523
542
  function writeMergeGateAcceptance(context, file, request, gate, acceptedTree, receipts) { const persisted = new Map(checkReceipts(context, { projectId: request.project_id, runId: request.run_id }).map(receipt => [receipt.id, receipt])); const value = { schema_id: "dd-flow/merge-gate-acceptance@2", merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, gate_hash: gate.gateHash, profile_hash: gate.profileHash, accepted_tree: acceptedTree, receipts: receipts.map(receipt => ({ id: receipt.id, execution_refs: [...(persisted.get(receipt.id)?.check_refs ?? receipt.check_refs)].sort(), binding_refs: [...receipt.check_refs].sort(), input_hash: receipt.input_hash })) }; const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
524
543
  function readMergeGateAcceptance(context, file, request, gate) { try {
@@ -11,19 +11,21 @@ import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, ensureWorkReg
11
11
  import { validateSchema } from "./schema-validation.js";
12
12
  import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
13
13
  import { assertStageStartHookEvent } from "./hooks.js";
14
- import { prepareVnextPlanArtifacts, validateVnextPlanArtifacts } from "./vnext-plan.js";
14
+ import { planCheckInstruction, prepareVnextPlanArtifacts, validateVnextPlanArtifacts } from "./vnext-plan.js";
15
15
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
16
16
  import { nextWorkId } from "./ids.js";
17
17
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
18
18
  import { acceptStagePacket, acceptStageReport, prepareStagePacket, prepareStageReport } from "./stage-report-renderer.js";
19
19
  import { assertPortableArtifactRef } from "./portable-refs.js";
20
- import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
20
+ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
21
21
  import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
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
25
  import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
26
26
  import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
27
+ import { reviewDefEvidence } from "./review-deferral.js";
28
+ import { stageSuccessor } from "./vnext-contracts.js";
27
29
  const stage = "plan-review";
28
30
  const stageDir = vnextStageDirectory(stage);
29
31
  const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
@@ -65,7 +67,7 @@ export function prepareVnextPlanReviewStart(context, input) {
65
67
  if (input.externalContext)
66
68
  prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
67
69
  prepareOutputFile(path.join(root, "stage-prompt.md"));
68
- const attachment = existing?.status === "running" ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
70
+ const attachment = existing?.status === "running" ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" });
69
71
  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 } : {}) });
70
72
  const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum, matrix, reviewerInputs };
71
73
  if (existing?.status === "running") {
@@ -107,32 +109,32 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
107
109
  assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
108
110
  if (prepared.state === "terminal") {
109
111
  const { report, outcome } = prepared;
110
- return { ok: true, run_id: run.id, stage, outcome: outcome === "off" ? "review_off" : "accepted", ...(report.semantic?.code ? { code: report.semantic.code } : {}), next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) }, idempotent: true };
112
+ return { ok: true, run_id: run.id, stage, outcome: outcome === "off" ? "review_off" : "accepted", ...(report.semantic?.code ? { code: report.semantic.code } : {}), next: codeSuccessor(context, run.id, projectRoot), idempotent: true };
111
113
  }
112
114
  const { requested, effective, groups, batchChecksum, planChecksum, workspaceRoute } = prepared;
113
115
  if (prepared.state === "resumed") {
114
116
  resumeVerificationMatrix(context, project.id, run.id, stage);
115
117
  const promptPath = path.join(root, "stage-prompt.md");
116
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
118
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
117
119
  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 } : {}) };
118
120
  }
119
121
  ensureWorkRegistry(context);
120
- prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" }, true);
122
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" }, true);
121
123
  fs.mkdirSync(root, { recursive: true });
122
124
  const now = context.now(), rootWork = prepared.rootWork;
123
125
  if (prepared.state === "off") {
124
126
  const promptPath = path.join(root, "stage-prompt.md");
125
127
  fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
126
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
128
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
127
129
  bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
128
- const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" };
130
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" };
129
131
  const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
130
132
  commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
131
133
  const reportGit = gitFacts(run.workspace_root);
132
- const nextCommand = codeCommand(context, run.id, projectRoot);
134
+ const next = codeSuccessor(context, run.id, projectRoot);
133
135
  return withStageSettlement(context, () => {
134
136
  const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
135
- const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, nextCommand });
137
+ const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, ...(next.command ? { nextCommand: next.command } : {}) });
136
138
  acceptVerificationMatrix(context, prepared.outputMatrix);
137
139
  if (prepared.outputMatrix)
138
140
  Object.assign(report.semantic, { verification_matrix: prepared.outputMatrix.metadata });
@@ -142,7 +144,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
142
144
  completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
143
145
  advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
144
146
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
145
- return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command: nextCommand } };
147
+ return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next };
146
148
  });
147
149
  }
148
150
  const workId = nextWorkId(context, project.id, "plan-review");
@@ -151,7 +153,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
151
153
  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`;
152
154
  const promptPath = path.join(root, "stage-prompt.md");
153
155
  fs.writeFileSync(promptPath, prompt);
154
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
156
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
155
157
  let binding;
156
158
  try {
157
159
  binding = bindStageCoordinatorWork(context, { workId, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
@@ -161,17 +163,17 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
161
163
  fs.rmSync(promptPath, { force: true });
162
164
  throw error;
163
165
  }
164
- const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" };
166
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" };
165
167
  const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
166
168
  commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
167
169
  markReviewPending(home, groups);
168
170
  const workSessionId = String(binding.work_session_id);
169
171
  const sessionId = String(binding.session_id);
170
172
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
171
- writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", reviewer_inputs: prepared.reviewerInputs, read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
173
+ writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", reviewer_inputs: prepared.reviewerInputs, read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_sources: readJson(prepared.batch).sources.map(source => ({ ...source, content_sha256: planContentHash(readJson(path.join(run.workspace_root, ".memory-bank", "protocol", source.protocol_id, "plan.json"))) })), plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
172
174
  refreshRunWorkProjection(context, project.id, run.id);
173
175
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
174
- return { ok: true, run_id: run.id, stage, outcome: "review_required", work_id: workId, id: workSessionId, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), groups }, next: { dispatch_command: dispatchCommand(context, run.id, projectRoot), finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
176
+ return { ok: true, run_id: run.id, stage, outcome: "review_required", work_id: workId, id: workSessionId, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}), orchestration, plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), groups }, next: { dispatch_command: dispatchCommand(context, run.id, projectRoot), finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
175
177
  }
176
178
  export function dispatchVnextPlanReview(context, input) {
177
179
  ensureWorkRegistry(context);
@@ -260,7 +262,6 @@ function finishPlanReviewOwned(context, input) {
260
262
  if (!Number.isInteger(reviewedPlanRevision) || reviewedPlanRevision < 1)
261
263
  throw new AppError("runtime_missing", "PLAN-REVIEW starting revision is unavailable", 1);
262
264
  const planRevision = currentPlanRevision(home, run.workspace_root);
263
- const currentPlanChecksum = planSetChecksum(home, run.workspace_root);
264
265
  if (decision.outcome === "blocked") {
265
266
  throw new AppError("stage_pause_required", "A user or decision blocker must pause the current PLAN-REVIEW Work with `dd-flow stage pause`; do not finish the stage as blocked", 2, { run_id: run.id, stage, work_id: parent.work_id });
266
267
  }
@@ -287,7 +288,6 @@ function finishPlanReviewOwned(context, input) {
287
288
  // plan uncertainty prevents it from approving its aspect. That is evidence
288
289
  // for the coordinator to classify and fix in this one review pass, not an
289
290
  // infrastructure failure or a reason to start another review cycle.
290
- const needsCorrection = latestChildren.some((child) => ["needs_changes", "blocked"].includes(reviewerVerdict(child.result)));
291
291
  if (latestChildren.some((child) => child.status !== "completed"))
292
292
  throw new AppError("blocked", "PLAN-REVIEW cannot accept incomplete reviewer evidence", 2);
293
293
  const canonicalFindings = canonicalReviewerFindings(latestChildren);
@@ -298,19 +298,23 @@ function finishPlanReviewOwned(context, input) {
298
298
  const unknownDecisions = decisionFindingIds.filter((id) => !allFindingIds.includes(id));
299
299
  if (new Set(allFindingIds).size !== allFindingIds.length || decidedFindingIds.size !== decisionFindingIds.length || unknownDecisions.length || materialFindingIds.some((id) => !decidedFindingIds.has(id)))
300
300
  throw new AppError("review_evidence_invalid", "Reviewer findings and coordinator decisions must use unique known canonical references, with every material finding decided", 2, { material_finding_ids: materialFindingIds, decided_finding_ids: decisionFindingIds, unknown_decisions: unknownDecisions });
301
- if (needsCorrection) {
302
- if (decision.correction.status !== "applied")
303
- throw new AppError("validation", "Reviewer findings require an in-place correction receipt", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
304
- if (decision.correction.previous_plan_revision !== reviewedPlanRevision || planRevision <= reviewedPlanRevision || decision.correction.changed_paths.length === 0)
305
- throw new AppError("validation", "Applied PLAN correction must advance revision and list changed paths", 2, { reviewed_plan_revision: reviewedPlanRevision, final_plan_revision: planRevision, phase: "prepare", effect: "no_effect", recoverable: true });
306
- }
307
- else if (decision.correction.status !== "not_required" || planRevision !== reviewedPlanRevision || currentPlanChecksum !== contextFile.system?.plan_checksum) {
308
- throw new AppError("validation", "A clean PLAN review must not claim an unverified correction", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
309
- }
301
+ const defEvidence = validatePlanFindingDecisions(decision.finding_decisions, allFindingIds, run.workspace_root);
302
+ const acceptedMatrix = latestRunVerificationMatrix(context, project.id, run.id, "plan", "output");
303
+ const planSources = acceptedMatrix ? acceptedMatrix.matrix.sources.filter(source => source.role.startsWith("plan:")).map(source => {
304
+ const packet = acceptedMatrix.packets.find(packet => packet.file === path.join(source.root === "run" ? home : run.workspace_root, source.path) && packet.sha256 === source.sha256);
305
+ if (!packet)
306
+ throw new AppError("review_input_contract_missing", "Accepted PLAN source lacks its immutable publication bytes", 1);
307
+ const value = JSON.parse(packet.bytes.toString("utf8"));
308
+ return { plan_id: String(value.plan_id), protocol_id: String(value.protocol_id), revision: Number(value.revision), sha256: source.sha256, content_sha256: planContentHash(value) };
309
+ }) : contextFile.system?.plan_sources ?? [];
310
+ if (checksum(path.join(planRoot, "stage-report.json")) !== readIndex(run).stage_runs?.find(entry => entry.stage === "plan")?.data_sha256)
311
+ throw new AppError("read_only_input_changed", "Accepted PLAN report changed during review", 2);
312
+ const acceptedPlan = readJson(path.join(planRoot, "stage-report.json"));
313
+ validatePlanCorrection({ decision, reviewedPlanRevision: reviewedPlanRevision, sources: planSources, maps: acceptedPlan.semantic.aspect_map_checksums, workspaceRoot: run.workspace_root, home });
310
314
  const prepared = prepareVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIdsForReview(home) });
311
315
  if (prepared.failures.length)
312
316
  throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors: prepared.failures, phase: "prepare", effect: "no_effect", recoverable: true });
313
- if (needsCorrection) {
317
+ if (decision.correction.status === "applied") {
314
318
  // Regeneration above is authoritative. A valid PLAN correction need not
315
319
  // change the derived CODE tasks (for example, a clarified explanation).
316
320
  if (decision.correction.changed_paths.some((item) => item.endsWith("code-work-batch.json")))
@@ -323,13 +327,14 @@ function finishPlanReviewOwned(context, input) {
323
327
  const codeParentId = rootWorkId(context, project.id, run.id);
324
328
  const codeItems = prepareWorkBatch(context, { parentWorkId: codeParentId, file: batch, stage: "code" });
325
329
  const reportGit = gitFacts(run.workspace_root);
326
- const nextCommand = codeCommand(context, run.id, projectRoot);
330
+ const next = codeSuccessor(context, run.id, projectRoot);
327
331
  const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", semanticFile: decisionPath });
328
332
  return withStageSettlement(context, () => {
329
333
  const registered = addWorkBatch(context, { parentWorkId: codeParentId, prepared: codeItems, stage: "code" });
330
334
  const now = context.now();
331
335
  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") });
332
- 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 });
336
+ 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, ...(next.command ? { nextCommand: next.command } : {}) });
337
+ Object.assign(report.semantic, { def_evidence: defEvidence });
333
338
  applyReviewResults(home, latestChildren, decision);
334
339
  if (matrix)
335
340
  Object.assign(report.semantic, { verification_matrix: matrix.metadata });
@@ -342,25 +347,27 @@ function finishPlanReviewOwned(context, input) {
342
347
  refreshRunWorkProjection(context, project.id, run.id);
343
348
  advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
344
349
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
345
- return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: nextCommand } };
350
+ return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next };
346
351
  });
347
352
  }
348
353
  function preparedExisting(context, input) {
349
354
  const promptPath = path.join(input.root, "stage-prompt.md");
350
355
  const binding = bindStageCoordinatorWork(context, { workId: input.workId, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(input.root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
351
356
  const orchestration = input.orchestration ?? writeFanoutDescriptor(input.root, { stage, parent_work_id: input.workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, input.run.project_id, input.run.id, stage) });
352
- return { ok: true, resumed: true, run_id: input.run.id, stage, outcome: "review_required", work_id: input.workId, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), orchestration, plan_review: { requested_mode: input.requested, effective_mode: input.effective, policy_source: policySource(input.run), groups: input.groups }, next: { dispatch_command: dispatchCommand(context, input.run.id, input.projectRoot), finish_command: finishCommand(context, input.run.id, input.projectRoot, path.join(input.root, "decision.json")) } };
357
+ return { ok: true, resumed: true, run_id: input.run.id, stage, outcome: "review_required", work_id: input.workId, id: binding.work_session_id, ...currentStagePrompt(context, promptPath), orchestration, plan_review: { requested_mode: input.requested, effective_mode: input.effective, policy_source: policySource(input.run), groups: input.groups }, next: { dispatch_command: dispatchCommand(context, input.run.id, input.projectRoot), finish_command: finishCommand(context, input.run.id, input.projectRoot, path.join(input.root, "decision.json")) } };
353
358
  }
354
359
  function orchestratorPrompt(context, input) {
355
360
  const { revision, capacity } = input;
356
361
  const decision = path.join(input.root, "decision.json");
357
362
  const workspaceContract = ["<workspace_contract>", `- route: ${input.workspaceRoute.route}`, `- feature branch: ${input.workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${input.workspaceRoute.base_ref ?? "not applicable"}`, `- read/write workspace: ${input.run.workspace_root}`, "The CLI verified this frozen route. All plan and correction writes belong in the named workspace; project root remains only the stable lifecycle identity. Do not create, switch, merge or delete branches/worktrees.", "</workspace_contract>"].join("\n");
358
363
  const acceptanceOwnership = "For every deferred acceptance or document update, identify the named CODE Work owner, its causal checks, and when it can truthfully close the obligation. Aggregate checks run only after implementation Work completes: assign a CODE owner to pending documents and use CODE needs_checks → verification_required → repair after those receipts. A promise that MERGE or an unnamed readiness owner will finish acceptance is a material PLAN finding; bootstrap workspace readiness is not proof of the new feature.";
359
- const template = [acceptanceOwnership, input.template].join("\n\n");
364
+ const template = [acceptanceOwnership, planCheckInstruction,
365
+ `The accepted check profile is ${path.join(input.run.workspace_root, ".memory-bank", "spec", "engineering", "code-check-profile.json")}. A new check required by a correction is planned, not available: name its provider Work and order consumers after that Work.`,
366
+ input.template].join("\n\n");
360
367
  const reviewerLaunch = capacity.source === "external_policy"
361
368
  ? `After dispatch, return at the Work-graph boundary. The shared runtime launches the queued reviewer Works as separate external Sessions, at most ${capacity.available_slots} in parallel under the RUN's frozen profiles. Do not launch native children, create provider roots, run a capacity probe, or record this external limit as native capacity. Reviewers are read-only leaf workers. Continue the semantic decision only after their Work receipts settle; do not substitute a missing result or relaunch a settled reviewer.`
362
369
  : "After dispatch, stop at the Work-graph boundary. The shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract; do not choose a provider tool from this stage prompt or substitute a shell command. It launches at most the qualified capacity at once and starts unchanged queued Works only after the current wave settles. A launch rejected before it starts is not review evidence: do not create a replacement. Each reviewer must be a genuinely fresh harness child Session; the lifecycle adapter binds that observed Session, so do not bind or supply a Session ID manually. Reviewers are read-only and must not create children. As soon as a reviewer result is accepted, release that reviewer Session when the harness permits.";
363
- return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch reports qualified_capacity_required, stop. The external harness controller qualifies the selected profile outside this RUN and records the resulting integer with ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<qualified-native-child-count>")}. PLAN-REVIEW never launches a capacity probe.`, reviewerLaunch, "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be explicit in one Work's task and verification and ordered before its consumer. planned_write_areas may advertise likely overlap, but do not treat them as ownership; required_read alone is not a delivery plan.", "If the final decision needs user input with no reasonable default, write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message, stop, then resume this same PLAN-REVIEW Work. Do not write decision.json or finish first.", `When all reviewer results are complete and every user question is resolved, classify every material finding, fix accepted findings in this same PLAN-REVIEW Work, then write ${decision} and finish: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer findings use local FIND-NNN ids. dd-flow exposes each finding to this coordinator as WRK-.../FIND-NNN; use that canonical finding_ref in the decision.", "A completed reviewer result with needs_changes or blocked is evidence, not the stage outcome. Classify its material findings and apply accepted fixes in this one review pass; do not start a second review automatically. Only a missing, malformed or unfinished reviewer result blocks the stage. For an accepted correction, increment PLAN revision and update only plan.json and the relevant aspect map. Do not edit or list code-work-batch.json: the CLI validates final PLAN and regenerates it. If no material correction is needed, set correction.status=not_required. The CLI checks mechanical handoff coherence; it does not prove semantic correctness.", "```json", JSON.stringify({ schema_id: "dd-flow/plan-review-decision@3", outcome: "accepted | failed | cancelled", summary: "Concise evidence-backed final decision.", finding_decisions: [{ finding_ref: "WRK-001-review/FIND-001", decision: "accepted_fix | rejected | deferred_as_DEF | requires_user | duplicate", reason: "Why." }], correction: { status: "not_required | applied", previous_plan_revision: revision, changed_paths: [], summary: "No material correction was needed, or summarize the applied correction." } }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
370
+ return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch reports qualified_capacity_required, stop. The external harness controller qualifies the selected profile outside this RUN and records the resulting integer with ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<qualified-native-child-count>")}. PLAN-REVIEW never launches a capacity probe.`, reviewerLaunch, "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be explicit in one Work's task and verification and ordered before its consumer. planned_write_areas may advertise likely overlap, but do not treat them as ownership; required_read alone is not a delivery plan.", "If the final decision needs user input with no reasonable default, write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message, stop, then resume this same PLAN-REVIEW Work. Do not write decision.json or finish first.", `When all reviewer results are complete and every user question is resolved, classify every material finding, fix accepted findings in this same PLAN-REVIEW Work, then write ${decision} and finish: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer findings use local FIND-NNN ids. dd-flow exposes each finding to this coordinator as WRK-.../FIND-NNN; use that canonical finding_ref in the decision.", "A completed reviewer result with needs_changes or blocked is evidence, not the stage outcome. Classify its material findings and apply accepted fixes in this one review pass; do not start a second review automatically. Only a missing, malformed or unfinished reviewer result blocks the stage. Accepted fixes need an actual authored delta. Rejected/duplicate findings need no artificial correction. Coordinator-only correction is allowed. Increment only the changed protocol PLAN's own revision; an aspect-map-only correction needs no fake PLAN revision. changed_paths must list exactly the changed workspace-relative PLAN paths and RUN/<aspect-map-path>. Do not edit or list code-work-batch.json: the CLI validates final PLAN and regenerates it. If no material correction is needed, set correction.status=not_required. The CLI checks mechanical handoff coherence; it does not prove semantic correctness.", "```json", JSON.stringify({ schema_id: "dd-flow/plan-review-decision@4", outcome: "accepted | failed | cancelled", summary: "Concise evidence-backed final decision.", finding_decisions: [{ finding_ref: "WRK-001-review/FIND-001", decision: "accepted_fix | rejected | deferred_as_DEF | requires_user | duplicate", reason: "Why.", duplicate_of: "A distinct canonical finding_ref only for duplicate", def_id: "DEF-0001 only for deferred_as_DEF" }], correction: { status: "not_required | applied", previous_plan_revision: revision, changed_paths: [], summary: "No material correction was needed, or summarize the applied correction." } }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
364
371
  }
365
372
  function reviewGroups(home, workspaceRoot) {
366
373
  const root = path.join(home, "03-plan");
@@ -438,6 +445,65 @@ function markReviewPending(_home, _groups) { }
438
445
  /** Review evidence is immutable child Work output plus the stage decision. */
439
446
  function applyReviewResults(_home, _children, _decision) { }
440
447
  function canonicalReviewerFindings(children) { return children.flatMap((child) => (reviewerResult(child.result)?.aspects ?? []).flatMap((aspect) => aspect.findings.map((finding) => ({ finding_ref: `${child.work_id}/${finding.finding_id}`, finding })))); }
448
+ export function validatePlanFindingDecisions(items, knownRefs, workspaceRoot) {
449
+ const known = new Set(knownRefs), decisions = new Map(items.map(item => [item.finding_ref, item]));
450
+ if (decisions.size !== items.length || items.some(item => !item.finding_ref || !known.has(item.finding_ref)))
451
+ throw new AppError("review_evidence_invalid", "PLAN decisions require unique known finding references", 2);
452
+ if (items.some(item => item.decision === "requires_user"))
453
+ throw new AppError("stage_pause_required", "Resolve the user question with stage pause/resume before accepting PLAN-REVIEW", 2);
454
+ for (const item of items.filter(item => item.decision === "duplicate")) {
455
+ let cursor = item;
456
+ const seen = new Set();
457
+ while (cursor?.decision === "duplicate") {
458
+ if (!cursor.finding_ref || seen.has(cursor.finding_ref) || !cursor.duplicate_of || !known.has(cursor.duplicate_of) || cursor.duplicate_of === cursor.finding_ref)
459
+ throw new AppError("review_evidence_invalid", "PLAN duplicates must form an acyclic chain of distinct known findings", 2);
460
+ seen.add(cursor.finding_ref);
461
+ cursor = decisions.get(cursor.duplicate_of);
462
+ }
463
+ if (!cursor)
464
+ throw new AppError("review_evidence_invalid", "A PLAN duplicate must end at a final decided finding", 2);
465
+ }
466
+ return items.filter(item => item.decision === "deferred_as_DEF").map(item => reviewDefEvidence(workspaceRoot, item.def_id));
467
+ }
468
+ function planContentHash(plan) {
469
+ const content = { ...plan };
470
+ delete content.revision;
471
+ return crypto.createHash("sha256").update(JSON.stringify(content, (_key, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value)).digest("hex");
472
+ }
473
+ export function validatePlanCorrection(input) {
474
+ const changed = [];
475
+ if (!input.sources.length || !input.maps?.length)
476
+ throw new AppError("review_input_contract_missing", "PLAN correction needs its accepted per-protocol sources", 1);
477
+ const invalid = (message) => { throw new AppError("validation", message, 2, { phase: "prepare", effect: "no_effect", recoverable: true }); };
478
+ for (const source of input.sources) {
479
+ const relative = `.memory-bank/protocol/${source.protocol_id}/plan.json`;
480
+ const file = assertPortableArtifactRef(relative, { workspaceRoot: input.workspaceRoot, runHome: input.home, runId: path.basename(input.home) });
481
+ const current = readJson(file);
482
+ if (current.plan_id !== source.plan_id || current.protocol_id !== source.protocol_id)
483
+ invalid("PLAN correction cannot change protocol identity");
484
+ if (checksum(file) !== source.sha256) {
485
+ if (!Number.isSafeInteger(current.revision) || Number(current.revision) <= source.revision || planContentHash(current) === source.content_sha256)
486
+ invalid("A changed PLAN must advance its own revision and contain a real authored change");
487
+ changed.push(relative);
488
+ }
489
+ }
490
+ for (const map of input.maps) {
491
+ const file = assertPortableArtifactRef(`run://${path.basename(input.home)}/${map.path}`, { workspaceRoot: input.workspaceRoot, runHome: input.home, runId: path.basename(input.home) });
492
+ if (checksum(file) !== map.sha256)
493
+ changed.push(`RUN/${map.path}`);
494
+ }
495
+ const correction = input.decision.correction;
496
+ const actual = [...new Set(changed)].sort();
497
+ const named = correction.changed_paths.map(ref => ref.startsWith(`run://${path.basename(input.home)}/`) ? `RUN/${ref.slice(`run://${path.basename(input.home)}/`.length)}` : ref).sort();
498
+ if (new Set(named).size !== named.length || JSON.stringify(actual) !== JSON.stringify(named))
499
+ invalid("Correction changed_paths must exactly name the changed PLAN/aspect-map artifacts");
500
+ if (actual.length) {
501
+ if (correction.status !== "applied" || correction.previous_plan_revision !== input.reviewedPlanRevision)
502
+ invalid("An authored correction requires an applied receipt bound to the reviewed revision");
503
+ }
504
+ else if (correction.status !== "not_required" || input.decision.finding_decisions.some(item => item.decision === "accepted_fix"))
505
+ invalid("An accepted fix needs a real authored correction; unchanged rejected findings need not_required");
506
+ }
441
507
  function resolveMode(requested, groups, home, projectRoot) {
442
508
  if (requested !== "auto")
443
509
  return requested;
@@ -573,10 +639,11 @@ function reportFor(context, input) {
573
639
  const sessionCommand = `${input.flow} stat run sessions ls --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
574
640
  const usageCommand = `${input.flow} stat usage --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
575
641
  const changedFiles = correction?.status === "applied" ? correction.changed_paths : [];
576
- return { schema_id: "dd-flow/stage-report@1", run_id: input.run.id, stage, generated_at: input.now, verdict: accepted ? "done" : input.outcome, semantic: { result, acceptance: [input.outcome], changed_files: changedFiles, checks: ["accepted PLAN report", "generated CODE batch", "reviewer evidence", ...(correction ? ["mechanical correction receipt"] : [])], evidence: [], next_action: accepted ? "start_code" : "resolve_plan_review_blocker", plan_review: { requested_mode: input.requested, effective_mode: input.mode, outcome: input.outcome, groups: input.groups.map((group) => group.key), initial_plan_revision: input.initialPlanRevision ?? null, final_plan_revision: input.finalPlanRevision ?? input.initialPlanRevision ?? null, initial_plan_checksum: input.planChecksum, final_plan_checksum: input.finalPlanChecksum ?? input.planChecksum, initial_batch_checksum: input.batchChecksum, final_batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, correction, decision: input.decision ?? null, reviewer_works: input.children?.map((child) => ({ work_id: child.work_id, status: child.status, verdict: reviewerVerdict(child.result) })) ?? [] }, plan_checksum: input.finalPlanChecksum ?? input.planChecksum, batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, code: input.code }, mechanical: { started_at: timing.started_at, finished_at: input.now, wall_clock_ms: timing.wall_clock_ms, git: input.git ?? gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: input.nextCommand ?? managedLifecycleCommand(context, `${input.flow} stage start ${input.run.id} --stage code --project-root ${JSON.stringify(input.projectRoot)} --json`) } : {}) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
642
+ const nextCommand = accepted ? input.nextCommand : undefined;
643
+ return { schema_id: "dd-flow/stage-report@1", run_id: input.run.id, stage, generated_at: input.now, verdict: accepted ? "done" : input.outcome, semantic: { result, acceptance: [input.outcome], changed_files: changedFiles, checks: ["accepted PLAN report", "generated CODE batch", "reviewer evidence", ...(correction ? ["mechanical correction receipt"] : [])], evidence: [], next_action: accepted ? "start_code" : "resolve_plan_review_blocker", plan_review: { requested_mode: input.requested, effective_mode: input.mode, outcome: input.outcome, groups: input.groups.map((group) => group.key), initial_plan_revision: input.initialPlanRevision ?? null, final_plan_revision: input.finalPlanRevision ?? input.initialPlanRevision ?? null, initial_plan_checksum: input.planChecksum, final_plan_checksum: input.finalPlanChecksum ?? input.planChecksum, initial_batch_checksum: input.batchChecksum, final_batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, correction, decision: input.decision ?? null, reviewer_works: input.children?.map((child) => ({ work_id: child.work_id, status: child.status, verdict: reviewerVerdict(child.result) })) ?? [] }, plan_checksum: input.finalPlanChecksum ?? input.planChecksum, batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, code: input.code }, mechanical: { started_at: timing.started_at, finished_at: input.now, wall_clock_ms: timing.wall_clock_ms, git: input.git ?? gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(nextCommand ? { next_command: nextCommand } : {}) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
577
644
  }
578
645
  function stageTiming(run, now) { const started = readIndex(run).stage_runs?.find((item) => item.stage === stage && typeof item.started_at === "string"); const startedAt = started?.started_at ?? now; const milliseconds = Date.parse(now) - Date.parse(startedAt); return { started_at: startedAt, wall_clock_ms: Number.isFinite(milliseconds) && milliseconds >= 0 ? milliseconds : null }; }
579
- function codeCommand(context, runId, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`); }
646
+ function codeSuccessor(context, runId, projectRoot) { return stageSuccessor(context, { projectRoot, runId, stage: "code", command: `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json` }); }
580
647
  function dispatchCommand(context, runId, projectRoot) { return `${flowCommand(context)} plan-review dispatch ${runId} --project-root ${JSON.stringify(projectRoot)} --json`; }
581
648
  function finishCommand(context, runId, projectRoot, decision) { return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --decision-file ${JSON.stringify(decision)} --json`); }
582
649
  function capacityRecordCommand(context, runId, projectRoot, availableSlots) { return `${flowCommand(context)} run capacity record ${runId} --available-slots ${availableSlots} --project-root ${JSON.stringify(projectRoot)} --json`; }