@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.74

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 (121) hide show
  1. package/CHANGELOG.md +396 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/help.js +81 -9
  5. package/dist/cli/run-cli.js +442 -46
  6. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  7. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  8. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  9. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  10. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  11. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  12. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  13. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  14. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  15. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
  16. package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
  17. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  18. package/dist/harness-runtime/lib/dd-droid.mjs +470 -0
  19. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  20. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  21. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  22. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  23. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  24. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  25. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  26. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  27. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  28. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  29. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  30. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  31. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  32. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  33. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  34. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  35. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  36. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  37. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  38. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  39. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  40. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  41. package/dist/runtime/context.js +3 -1
  42. package/dist/schemas/agent-profile.schema.json +1 -1
  43. package/dist/schemas/code-review-result.schema.json +2 -2
  44. package/dist/schemas/code-work-batch.schema.json +4 -3
  45. package/dist/schemas/code-work-result.schema.json +4 -4
  46. package/dist/schemas/harness-config.schema.json +23 -0
  47. package/dist/schemas/plan-review-result.schema.json +2 -2
  48. package/dist/schemas/run-control-receipt.schema.json +99 -0
  49. package/dist/schemas/run-control-request.schema.json +36 -0
  50. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  51. package/dist/services/cleanup.js +8 -5
  52. package/dist/services/cli-operation-classifier.js +11 -1
  53. package/dist/services/code-checks.js +543 -96
  54. package/dist/services/controller-fanout.js +120 -0
  55. package/dist/services/dashboard.js +9 -0
  56. package/dist/services/engines.js +50 -19
  57. package/dist/services/eval-snapshots.js +639 -60
  58. package/dist/services/execution-policy.js +177 -0
  59. package/dist/services/external-work-launch.js +110 -0
  60. package/dist/services/harness-adapter.js +147 -24
  61. package/dist/services/harness-config.js +68 -0
  62. package/dist/services/hooks.js +413 -168
  63. package/dist/services/lanes.js +1 -1
  64. package/dist/services/lifecycle-command.js +74 -6
  65. package/dist/services/lifecycle-invocations.js +637 -0
  66. package/dist/services/managed-daemon-binding.js +38 -0
  67. package/dist/services/managed-processes.js +138 -22
  68. package/dist/services/merge-queue.js +6 -6
  69. package/dist/services/merge-server.js +75 -29
  70. package/dist/services/migrations.js +1 -1
  71. package/dist/services/native-daemon-history.js +49 -0
  72. package/dist/services/native-session-control.js +39 -0
  73. package/dist/services/projects.js +7 -3
  74. package/dist/services/prompts.js +4 -2
  75. package/dist/services/protocols.js +11 -2
  76. package/dist/services/recovery-observation-budget.js +61 -0
  77. package/dist/services/recovery-snapshot-database.js +107 -0
  78. package/dist/services/run-control-receipt.js +56 -0
  79. package/dist/services/run-control-worker.js +363 -0
  80. package/dist/services/run-control.js +876 -0
  81. package/dist/services/run-controller-adapter.js +211 -0
  82. package/dist/services/run-controller-capture.js +134 -0
  83. package/dist/services/run-controller-process.js +189 -0
  84. package/dist/services/run-controller-recovery.js +222 -0
  85. package/dist/services/run-controller-state.js +31 -0
  86. package/dist/services/run-controller.js +733 -0
  87. package/dist/services/run-engine-bindings.js +20 -62
  88. package/dist/services/run-fork.js +124 -0
  89. package/dist/services/run-observations.js +112 -0
  90. package/dist/services/run-recovery-runtime.js +69 -0
  91. package/dist/services/run-recovery.js +327 -0
  92. package/dist/services/runs.js +280 -40
  93. package/dist/services/runtime-budget.js +362 -0
  94. package/dist/services/runtime-scope-capture.js +50 -0
  95. package/dist/services/runtime-scope-control.js +450 -0
  96. package/dist/services/runtime-scope-resume.js +543 -0
  97. package/dist/services/runtime-scope-stop.js +99 -0
  98. package/dist/services/runtime-scope-worker.js +210 -0
  99. package/dist/services/runtime-service.js +96 -0
  100. package/dist/services/schema-validation.js +12 -12
  101. package/dist/services/stage-context.js +2 -2
  102. package/dist/services/stage-lifecycle.js +32 -10
  103. package/dist/services/stage-pause.js +25 -13
  104. package/dist/services/usage.js +37 -93
  105. package/dist/services/vnext-code-review.js +149 -89
  106. package/dist/services/vnext-code.js +223 -215
  107. package/dist/services/vnext-execution-profile.js +5 -3
  108. package/dist/services/vnext-fanout.js +135 -21
  109. package/dist/services/vnext-merge.js +258 -89
  110. package/dist/services/vnext-plan-review.js +75 -64
  111. package/dist/services/vnext-plan.js +77 -29
  112. package/dist/services/vnext-protocolize.js +21 -14
  113. package/dist/services/vnext-specify.js +26 -16
  114. package/dist/services/work-registry.js +592 -154
  115. package/dist/services/workspace-bootstrap.js +76 -0
  116. package/dist/shared/errors.js +12 -0
  117. package/dist/storage/database.js +341 -31
  118. package/dist/storage/writer-contract.js +77 -0
  119. package/dist/storage/writer-migration.js +102 -0
  120. package/package.json +20 -13
  121. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,4 +1,6 @@
1
+ import { managedLifecycleCommand } from "./lifecycle-invocations.js";
1
2
  import crypto from "node:crypto";
3
+ import { withStageSettlement } from "./runs.js";
2
4
  import fs from "node:fs";
3
5
  import path from "node:path";
4
6
  import { AppError } from "../shared/errors.js";
@@ -9,20 +11,17 @@ import { addWorkBatch, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunW
9
11
  import { validateSchema } from "./schema-validation.js";
10
12
  import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
11
13
  import { assertStageStartHookEvent } from "./hooks.js";
12
- import { validateVnextPlanArtifacts } from "./vnext-plan.js";
14
+ import { prepareVnextPlanArtifacts, validateVnextPlanArtifacts } from "./vnext-plan.js";
13
15
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
14
16
  import { nextWorkId } from "./ids.js";
15
17
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
16
18
  import { writeStageReport } from "./stage-report-renderer.js";
17
19
  import { assertPortableArtifactRef } from "./portable-refs.js";
18
20
  import { applyExternalStageContext } from "./stage-context.js";
19
- import { capacityProbe, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
21
+ import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
20
22
  const stage = "plan-review";
21
23
  const stageDir = vnextStageDirectory(stage);
22
24
  const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
23
- const capacityProbeFanoutSize = capacityProbe.fanout_size;
24
- const capacityProbeHoldSeconds = capacityProbe.probe_hold_seconds;
25
- const capacityProbeDeadlineSeconds = capacityProbe.cleanup_deadline_seconds;
26
25
  export function isVnextPlanReviewRun(context, input) {
27
26
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
28
27
  return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
@@ -73,13 +72,15 @@ export function startVnextPlanReview(context, input) {
73
72
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
74
73
  bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
75
74
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
76
- const registered = registerCode(context, project.id, run.id, batch);
77
- const report = reportFor({ run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
78
- writeReport(root, report);
79
- completeFlowRunStage(context, { 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" });
80
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
81
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
82
- 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: codeCommand(context, run.id, projectRoot) } };
75
+ return withStageSettlement(context, () => {
76
+ const registered = registerCode(context, project.id, run.id, batch);
77
+ const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
78
+ writeReport(root, report);
79
+ completeFlowRunStage(context, { 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" });
80
+ advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
81
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
82
+ 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: codeCommand(context, run.id, projectRoot) } };
83
+ });
83
84
  }
84
85
  const rootWork = rootWorkId(context, project.id, run.id);
85
86
  const workId = nextWorkId(context, project.id, "plan-review");
@@ -103,7 +104,7 @@ export function startVnextPlanReview(context, input) {
103
104
  const workSessionId = String(binding.work_session_id);
104
105
  const sessionId = String(binding.session_id);
105
106
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true });
106
- writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: currentPlanRevision(home, run.workspace_root), plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
107
+ writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", read_only_input: { parent_work_id: workId }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: currentPlanRevision(home, run.workspace_root), plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
107
108
  refreshRunWorkProjection(context, project.id, run.id);
108
109
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
109
110
  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")) } };
@@ -126,7 +127,7 @@ export function dispatchVnextPlanReview(context, input) {
126
127
  if (capacity.pending)
127
128
  return capacity.response;
128
129
  if (capacity.availableSlots < 1)
129
- throw new AppError("no_subagent_capacity", "PLAN-REVIEW requires one fresh reviewer Session but no probe launch succeeded", 1, { run_id: run.id });
130
+ throw new AppError("subagent_capacity_unqualified", "PLAN-REVIEW requires one qualified native subagent slot", 1, { run_id: run.id });
130
131
  const existing = context.db.all("SELECT work_id, task, status, result, created_at, launch_policy FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [project.id, run.id, parent.work_id]);
131
132
  const latest = (group) => existing.find((item) => item.task === group.task);
132
133
  const pending = groups.filter((group) => !latest(group));
@@ -146,7 +147,7 @@ export function dispatchVnextPlanReview(context, input) {
146
147
  refreshRunWorkProjection(context, project.id, run.id);
147
148
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_dispatched", work_id: parent.work_id, groups: reviews.map((item) => ({ group: item.group, work_id: item.work_id })) });
148
149
  const waves = plannedReviewWaves(groups, capacity.availableSlots);
149
- return { ok: true, run_id: run.id, stage, capacity: { available_slots: capacity.availableSlots }, routing: { group_count: groups.length, planned_waves: waves.length, waves, preference: "Use one wave when semantic compatibility permits; hard dependencies create later waves." }, review_works: reviews, next_action: "launch_ready_reviews" };
150
+ return { ok: true, run_id: run.id, stage, capacity: { available_slots: capacity.availableSlots, source: capacity.source }, routing: { group_count: groups.length, planned_waves: waves.length, waves, preference: "Use one wave when semantic compatibility permits; hard dependencies create later waves." }, review_works: reviews, next_action: "launch_ready_reviews" };
150
151
  }
151
152
  // Capacity is a RUN fact shared by every fan-out stage.
152
153
  const reviewCapacityKey = subagentCapacityKey;
@@ -162,16 +163,19 @@ export function recordVnextPlanReviewCapacity(context, input) {
162
163
  const prior = variables.variables[reviewCapacityKey];
163
164
  if (typeof prior === "number") {
164
165
  if (prior === input.availableSlots)
165
- return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "bounded_probe", reused: true } };
166
+ return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "harness_qualification", reused: true } };
166
167
  setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
167
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots, fanout_size: capacityProbeFanoutSize });
168
- return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "bounded_probe", refreshed: true } };
168
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots, source: "harness_qualification" });
169
+ return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "harness_qualification", refreshed: true } };
169
170
  }
170
171
  setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
171
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots, fanout_size: capacityProbeFanoutSize });
172
- return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "one_shot_fanout", fanout_size: capacityProbeFanoutSize } };
172
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots, source: "harness_qualification" });
173
+ return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "harness_qualification" } };
173
174
  }
174
175
  export function finishVnextPlanReview(context, input) {
176
+ return withStageSettlement(context, () => finishPlanReviewOwned(context, input));
177
+ }
178
+ function finishPlanReviewOwned(context, input) {
175
179
  const projectRoot = resolveProjectRoot(input.projectRoot);
176
180
  const project = requireProjectByRoot(context, projectRoot);
177
181
  const run = requireRun(context, projectRoot, input.runId);
@@ -183,8 +187,8 @@ export function finishVnextPlanReview(context, input) {
183
187
  if (!parent || parent.status !== "running")
184
188
  throw new AppError("invalid_work_state", "PLAN-REVIEW has no running parent Work", 2);
185
189
  const decisionPath = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
186
- const decision = readDecision(context, projectRoot, run.id, decisionPath);
187
- const children = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ? AND task <> 'Capacity probe: return ready and finish this Work.'", [project.id, run.id, parent.work_id]);
190
+ const decision = readDecision(context, projectRoot, run.id, home, decisionPath);
191
+ const children = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [project.id, run.id, parent.work_id]);
188
192
  const contextFile = readJson(path.join(root, "work-context.json"));
189
193
  const groups = contextFile.groups ?? [];
190
194
  const reviewedPlanRevision = contextFile.system?.plan_revision;
@@ -196,8 +200,7 @@ export function finishVnextPlanReview(context, input) {
196
200
  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 });
197
201
  }
198
202
  if (["failed", "cancelled"].includes(decision.outcome)) {
199
- preserveDecisionReceipt(root, decisionPath);
200
- return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision });
203
+ return withStageSettlement(context, () => { preserveDecisionReceipt(root, decisionPath); return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision }); });
201
204
  }
202
205
  if (children.some((child) => child.status === "created" || child.status === "running"))
203
206
  throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
@@ -231,30 +234,32 @@ export function finishVnextPlanReview(context, input) {
231
234
  else if (decision.correction.status !== "not_required" || planRevision !== reviewedPlanRevision || currentPlanChecksum !== contextFile.system?.plan_checksum) {
232
235
  throw new AppError("validation", "A clean PLAN review must not claim an unverified correction", 2);
233
236
  }
234
- const planFailures = validateVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIdsForReview(home) });
235
- if (planFailures.length)
236
- throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors: planFailures });
237
+ const prepared = prepareVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIdsForReview(home) });
238
+ if (prepared.failures.length)
239
+ throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors: prepared.failures });
237
240
  if (needsCorrection) {
238
- const initialBatchChecksum = contextFile.system?.batch_checksum;
239
- if (!initialBatchChecksum || checksum(batch) === initialBatchChecksum)
240
- throw new AppError("stale_code_work_batch", "Applied PLAN correction must regenerate the proposed CODE batch", 2, { batch, initial_batch_checksum: initialBatchChecksum });
241
+ // Regeneration above is authoritative. A valid PLAN correction need not
242
+ // change the derived CODE tasks (for example, a clarified explanation).
241
243
  if (decision.correction.changed_paths.some((item) => item.endsWith("code-work-batch.json")))
242
244
  throw new AppError("validation", "Generated CODE batch must not be listed as an agent-authored correction", 2, { changed_paths: decision.correction.changed_paths });
243
245
  }
244
- preserveDecisionReceipt(root, decisionPath);
245
246
  const finalPlanChecksum = planSetChecksum(home, run.workspace_root);
246
- const finalBatchChecksum = checksum(batch);
247
- const registered = registerCode(context, project.id, run.id, batch);
248
- const now = context.now();
249
- 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") });
250
- const report = reportFor({ 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) });
251
- applyReviewResults(home, latestChildren, decision);
252
- writeReport(root, report);
253
- refreshRunWorkProjection(context, project.id, run.id);
254
- completeFlowRunStage(context, { 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" });
255
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
256
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
257
- return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
247
+ const finalBatchChecksum = prepared.batchChecksum;
248
+ return withStageSettlement(context, () => {
249
+ prepared.publish();
250
+ preserveDecisionReceipt(root, decisionPath);
251
+ const registered = registerCode(context, project.id, run.id, batch);
252
+ const now = context.now();
253
+ 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") });
254
+ 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) });
255
+ applyReviewResults(home, latestChildren, decision);
256
+ writeReport(root, report);
257
+ refreshRunWorkProjection(context, project.id, run.id);
258
+ completeFlowRunStage(context, { 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" });
259
+ advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
260
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
261
+ return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
262
+ });
258
263
  }
259
264
  function preparedExisting(context, input) {
260
265
  const promptPath = path.join(input.root, "stage-prompt.md");
@@ -267,7 +272,11 @@ function orchestratorPrompt(context, input) {
267
272
  const decision = path.join(input.root, "decision.json");
268
273
  const revision = currentPlanRevision(input.home, input.run.workspace_root);
269
274
  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");
270
- 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 requests capacity, run exactly one concurrent fan-out of ${capacityProbeFanoutSize} probes. This measures the harness limit; it is not a task to obtain ${capacityProbeFanoutSize} successful probes. Start #01…#${capacityProbeFanoutSize} once, all together, using all-settled handling so one rejection does not hide the other outcomes. A rejected launch is expected evidence. Never retry, replace, or add a probe. Each started probe calls no tools, reads no files, creates no children, waits ${capacityProbeHoldSeconds} seconds, then returns exactly AGENT-NN. For cleanup, wait at most ${capacityProbeDeadlineSeconds} seconds from the first launch, terminate every unfinished probe, then release every finished probe session that the harness permits. Only after that cleanup record the number of launches that started successfully, not the number of replacement attempts or late completions: ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<successful-initial-launches>")}. Capacity probes are not Works and are never registered.`, "After dispatch, launch at most the measured capacity at once. If more independent reviewer Works remain, wait for the current wave to settle, then start the unchanged queued Works in the next wave. A reviewer launch rejected before it starts is not review evidence: do not create a replacement; wait for a running wave to settle and start that same queued Work. Each reviewer must be a genuinely fresh harness child Session. The lifecycle adapter binds that observed Session; 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; do not let finished disposable workers occupy slots before the next wave.", "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, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second 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");
275
+ const capacity = getStageFanoutCapacity(context, { projectRoot: input.projectRoot, runId: input.run.id, stage });
276
+ const reviewerLaunch = capacity.source === "external_policy"
277
+ ? `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.`
278
+ : "After dispatch, launch at most the qualified capacity at once. If more independent reviewer Works remain, wait for the current wave to settle, then start the unchanged queued Works in the next wave. A reviewer launch rejected before it starts is not review evidence: do not create a replacement; wait for a running wave to settle and start that same queued Work. Each reviewer must be a genuinely fresh harness child Session. The lifecycle adapter binds that observed Session; 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; do not let finished disposable workers occupy slots before the next wave.";
279
+ 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, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second 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");
271
280
  }
272
281
  function reviewGroups(home, workspaceRoot) {
273
282
  const root = path.join(home, "03-plan");
@@ -327,15 +336,14 @@ function plannedReviewWaves(groups, capacity) {
327
336
  }
328
337
  return waves;
329
338
  }
330
- export function capacityProbeResponse(context, input) {
331
- return { ok: true, run_id: input.runId, stage, outcome: "capacity_probe_required", capacity_probe: { ...capacityProbe, controller_instruction: "Launch exactly one concurrent batch of 15 independent leaf probes. Use all-settled behavior: every initial launch is attempted once, and a launch rejection is expected evidence of the current limit. Do not retry, replace, or add probes. Count the initial launches that returned a live session handle. Successful probes must not call tools, read files, create children or run dd-flow; they wait 60 seconds then return exactly AGENT-NN. Wait only for cleanup, at most 180 seconds from first launch; terminate unfinished probes, then close/delete every finished probe session that the harness permits. Record only the initial successful-launch count after cleanup." }, next_action: "run_one_shot_capacity_probe_then_retry_dispatch", next: { record_command: capacityRecordCommand(context, input.runId, input.projectRoot, "<successful-initial-launches>"), retry_command: dispatchCommand(context, input.runId, input.projectRoot) } };
339
+ export function capacityQualificationResponse(context, input) {
340
+ return { ok: true, run_id: input.runId, stage, outcome: "qualified_capacity_required", next_action: "qualify_harness_profile_then_record_capacity", next: { record_command: capacityRecordCommand(context, input.runId, input.projectRoot, "<qualified-native-child-count>"), retry_command: dispatchCommand(context, input.runId, input.projectRoot) } };
332
341
  }
333
342
  function ensureReviewCapacity(context, input) {
334
- const variables = getFlowRunVariables(context, { projectRoot: input.projectRoot, runId: input.runId });
335
- const known = variables.variables[reviewCapacityKey];
336
- if (typeof known === "number" && Number.isInteger(known) && known >= 0)
337
- return { pending: false, availableSlots: known };
338
- return { pending: true, response: capacityProbeResponse(context, { projectRoot: input.projectRoot, runId: input.runId }) };
343
+ const capacity = getStageFanoutCapacity(context, { ...input, stage });
344
+ if (capacity.available_slots !== null)
345
+ return { pending: false, availableSlots: capacity.available_slots, source: capacity.source };
346
+ return { pending: true, response: capacityQualificationResponse(context, { projectRoot: input.projectRoot, runId: input.runId }) };
339
347
  }
340
348
  function protocolIdsForReview(home) {
341
349
  const report = readJson(path.join(home, "02-protocolize", "stage-report.json"));
@@ -385,7 +393,7 @@ catch {
385
393
  function rootWorkId(context, projectId, runId) { const root = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!root)
386
394
  throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
387
395
  function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch }); }
388
- function readDecision(context, projectRoot, runId, file) { validateSchema({ schemaName: "plan-review-decision", file, projectRoot, runId, ddFlowHome: context.ddFlowHome }); return readJson(file); }
396
+ function readDecision(context, projectRoot, runId, runRoot, file) { const data = readJson(file); validateSchema({ schemaName: "plan-review-decision", file, data, projectRoot, runId, runRoot, ddFlowHome: context.ddFlowHome }); return data; }
389
397
  function preserveDecisionReceipt(root, source) {
390
398
  const receipt = path.join(root, "decision.receipt.json");
391
399
  const bytes = fs.readFileSync(source);
@@ -426,8 +434,11 @@ function reviewerEvidenceFailures(context, input) {
426
434
  continue;
427
435
  }
428
436
  const actual = result.aspects.map((item) => item.aspect_id);
429
- if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id)) || result.aspects.some((item) => item.evidence_refs.length === 0))
437
+ if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id)))
430
438
  failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_coverage_invalid", expected_aspects: group.aspect_ids, actual_aspects: actual });
439
+ const missingEvidence = result.aspects.filter((item) => item.evidence_refs.length === 0).map((item) => item.aspect_id);
440
+ if (missingEvidence.length)
441
+ failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_evidence_missing", missing_aspects: missingEvidence });
431
442
  for (const ref of result.aspects.flatMap((item) => [...item.evidence_refs, ...item.findings.flatMap((finding) => finding.evidence_refs)])) {
432
443
  try {
433
444
  assertPortableArtifactRef(ref, { workspaceRoot: input.workspaceRoot, runHome: input.runHome, runId: input.runId });
@@ -454,15 +465,15 @@ function finishTerminalReview(context, input) {
454
465
  const now = context.now();
455
466
  const childStatus = input.decision.outcome === "cancelled" ? "cancelled" : "failed";
456
467
  // A terminal decision is structured cancellation of the review subtree. It
457
- // must settle probes as well as semantic reviewers before its parent Work
458
- // can settle; otherwise the RUN contains abandoned active Work/Session links.
468
+ // must settle semantic reviewers before its parent Work can settle; otherwise
469
+ // the RUN contains abandoned active Work/Session links.
459
470
  settleReviewChildren(context, { projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, reason: `PLAN-REVIEW terminal decision: ${input.decision.summary}`, now });
460
471
  settleReviewParent(context, { projectRoot: input.projectRoot, projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, result: input.decision.summary, reason: `plan_review_${input.decision.outcome}`, resultPath: path.join(input.root, "stage-report.json") });
461
472
  const rootWork = rootWorkId(context, input.projectId, input.run.id);
462
473
  context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [childStatus, input.decision.summary, now, now, rootWork]);
463
474
  closeRunningWorkSession(context, rootWork, childStatus, now);
464
- const settledChildren = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ? AND task <> 'Capacity probe: return ready and finish this Work.'", [input.projectId, input.run.id, input.parentWorkId]);
465
- const report = reportFor({ run: input.run, mode: input.contextFile.system?.effective_mode ?? "standard", requested: input.contextFile.system?.requested_mode ?? "auto", outcome: input.decision.outcome, groups: input.contextFile.groups ?? [], batchChecksum: input.contextFile.system?.batch_checksum ?? checksum(input.batch), planChecksum: input.contextFile.system?.plan_checksum ?? planSetChecksum(requireHome(input.run), input.run.workspace_root), code: {}, now, projectRoot: input.projectRoot, decision: input.decision, children: latestReviewChildren(settledChildren), flow: flowCommand(context) });
475
+ const settledChildren = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [input.projectId, input.run.id, input.parentWorkId]);
476
+ const report = reportFor(context, { run: input.run, mode: input.contextFile.system?.effective_mode ?? "standard", requested: input.contextFile.system?.requested_mode ?? "auto", outcome: input.decision.outcome, groups: input.contextFile.groups ?? [], batchChecksum: input.contextFile.system?.batch_checksum ?? checksum(input.batch), planChecksum: input.contextFile.system?.plan_checksum ?? planSetChecksum(requireHome(input.run), input.run.workspace_root), code: {}, now, projectRoot: input.projectRoot, decision: input.decision, children: latestReviewChildren(settledChildren), flow: flowCommand(context) });
466
477
  writeReport(input.root, report);
467
478
  refreshRunWorkProjection(context, input.projectId, input.run.id);
468
479
  const stageStatus = input.decision.outcome === "cancelled" ? "skipped" : input.decision.outcome === "failed" ? "failed" : "blocked";
@@ -481,7 +492,7 @@ function settleReviewChildren(context, input) {
481
492
  function closeRunningWorkSession(context, workId, status, now) {
482
493
  context.db.run("UPDATE work_sessions SET status = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [status, now, now, workId]);
483
494
  }
484
- function reportFor(input) {
495
+ function reportFor(context, input) {
485
496
  const accepted = input.outcome === "accepted" || input.outcome === "off";
486
497
  const result = input.outcome === "off" ? "Independent PLAN review was explicitly or automatically skipped." : accepted ? "Independent PLAN review accepted the plan." : `PLAN-REVIEW stopped with outcome: ${input.outcome}.`;
487
498
  const decision = input.decision;
@@ -490,20 +501,20 @@ function reportFor(input) {
490
501
  const sessionCommand = `${input.flow} stat run sessions ls --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
491
502
  const usageCommand = `${input.flow} stat usage --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
492
503
  const changedFiles = correction?.status === "applied" ? correction.changed_paths : [];
493
- 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: gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: `${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" } };
504
+ 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: gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: 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" } };
494
505
  }
495
506
  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 }; }
496
507
  function writeReport(root, report) {
497
508
  writeStageReport(root, report);
498
509
  }
499
- function codeCommand(context, runId, projectRoot) { return `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`; }
510
+ function codeCommand(context, runId, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`); }
500
511
  function dispatchCommand(context, runId, projectRoot) { return `${flowCommand(context)} plan-review dispatch ${runId} --project-root ${JSON.stringify(projectRoot)} --json`; }
501
- function finishCommand(context, runId, projectRoot, decision) { return `${flowCommand(context)} stage finish ${runId} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --decision-file ${JSON.stringify(decision)} --json`; }
512
+ 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`); }
502
513
  function capacityRecordCommand(context, runId, projectRoot, availableSlots) { return `${flowCommand(context)} run capacity record ${runId} --available-slots ${availableSlots} --project-root ${JSON.stringify(projectRoot)} --json`; }
503
- function requireRun(context, projectRoot, runId) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, project_id, workspace_root, run_home_path, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]); if (!run)
514
+ function requireRun(context, projectRoot, runId) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, project_id, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]); if (!run)
504
515
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
505
- function requireHome(run) { if (!run.run_home_path)
506
- throw new AppError("runtime_missing", "RUN workspace is unavailable", 1); return run.run_home_path; }
516
+ function requireHome(run) { if (!run.run_root)
517
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
507
518
  function read(file) { if (!fs.existsSync(file))
508
519
  throw new AppError("not_found", "Required vNext prompt is missing", 1, { file }); return fs.readFileSync(file, "utf8"); }
509
520
  function readJson(file) { try {