@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100

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 (153) hide show
  1. package/CHANGELOG.md +561 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +91 -15
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/run-cli.js +1506 -334
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  11. package/dist/harness-runtime/bin/dd-agy.mjs +39 -0
  12. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  13. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  14. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  15. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  16. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +449 -0
  19. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  21. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  22. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  23. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  24. package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
  25. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
  26. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  27. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -0
  28. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  29. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  30. package/dist/harness-runtime/lib/dd-zcode.mjs +941 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  33. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  34. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  35. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  36. package/dist/harness-runtime/lib/managed-daemon.mjs +267 -0
  37. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  38. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  39. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  40. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  41. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  42. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  43. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  44. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  45. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  46. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  47. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  48. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  49. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  50. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  51. package/dist/runtime/context.js +3 -1
  52. package/dist/schemas/agent-profile.schema.json +1 -1
  53. package/dist/schemas/code-review-result.schema.json +2 -2
  54. package/dist/schemas/code-work-batch.schema.json +4 -3
  55. package/dist/schemas/code-work-result.schema.json +4 -4
  56. package/dist/schemas/harness-config.schema.json +23 -0
  57. package/dist/schemas/plan-review-decision.schema.json +1 -1
  58. package/dist/schemas/plan-review-result.schema.json +2 -2
  59. package/dist/schemas/run-control-receipt.schema.json +99 -0
  60. package/dist/schemas/run-control-request.schema.json +36 -0
  61. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  62. package/dist/services/canon.js +11 -8
  63. package/dist/services/cleanup.js +75 -21
  64. package/dist/services/cli-operation-classifier.js +22 -24
  65. package/dist/services/code-checks.js +676 -79
  66. package/dist/services/codex-hook-delivery.js +28 -0
  67. package/dist/services/command-context.js +80 -0
  68. package/dist/services/config.js +11 -8
  69. package/dist/services/continuation-outcome.js +20 -0
  70. package/dist/services/controller-fanout.js +134 -0
  71. package/dist/services/dashboard.js +103 -31
  72. package/dist/services/engines.js +125 -80
  73. package/dist/services/eval-snapshots.js +838 -74
  74. package/dist/services/execution-policy.js +170 -0
  75. package/dist/services/external-work-launch.js +110 -0
  76. package/dist/services/harness-adapter.js +147 -24
  77. package/dist/services/harness-config.js +68 -0
  78. package/dist/services/hooks.js +609 -310
  79. package/dist/services/lanes.js +62 -53
  80. package/dist/services/lifecycle-command.js +103 -17
  81. package/dist/services/lifecycle-contract.js +30 -0
  82. package/dist/services/lifecycle-invocations.js +1128 -0
  83. package/dist/services/managed-daemon-binding.js +38 -0
  84. package/dist/services/managed-processes.js +425 -0
  85. package/dist/services/merge-queue.js +179 -104
  86. package/dist/services/merge-server.js +78 -27
  87. package/dist/services/migrations.js +14 -9
  88. package/dist/services/native-daemon-history.js +49 -0
  89. package/dist/services/native-session-control.js +39 -0
  90. package/dist/services/plan-runtime.js +19 -8
  91. package/dist/services/plans.js +28 -25
  92. package/dist/services/portable-refs.js +57 -0
  93. package/dist/services/projects.js +18 -5
  94. package/dist/services/prompts.js +27 -18
  95. package/dist/services/protocols.js +64 -20
  96. package/dist/services/recovery-observation-budget.js +61 -0
  97. package/dist/services/recovery-snapshot-database.js +107 -0
  98. package/dist/services/repair-intents.js +40 -0
  99. package/dist/services/run-control-receipt.js +56 -0
  100. package/dist/services/run-control-worker.js +363 -0
  101. package/dist/services/run-control.js +876 -0
  102. package/dist/services/run-controller-adapter.js +222 -0
  103. package/dist/services/run-controller-capture.js +136 -0
  104. package/dist/services/run-controller-process.js +189 -0
  105. package/dist/services/run-controller-recovery.js +222 -0
  106. package/dist/services/run-controller-state.js +36 -0
  107. package/dist/services/run-controller.js +803 -0
  108. package/dist/services/run-engine-bindings.js +20 -62
  109. package/dist/services/run-fork.js +138 -0
  110. package/dist/services/run-observations.js +112 -0
  111. package/dist/services/run-projection.js +10 -8
  112. package/dist/services/run-recovery-runtime.js +69 -0
  113. package/dist/services/run-recovery.js +343 -0
  114. package/dist/services/runs.js +437 -81
  115. package/dist/services/runtime-budget.js +439 -0
  116. package/dist/services/runtime-command.js +43 -0
  117. package/dist/services/runtime-scope-capture.js +50 -0
  118. package/dist/services/runtime-scope-control.js +450 -0
  119. package/dist/services/runtime-scope-resume.js +565 -0
  120. package/dist/services/runtime-scope-stop.js +99 -0
  121. package/dist/services/runtime-scope-worker.js +243 -0
  122. package/dist/services/runtime-service.js +97 -0
  123. package/dist/services/schema-validation.js +21 -19
  124. package/dist/services/session-identity.js +19 -0
  125. package/dist/services/sessions.js +61 -80
  126. package/dist/services/stage-blocker.js +17 -7
  127. package/dist/services/stage-context.js +44 -18
  128. package/dist/services/stage-lifecycle.js +112 -108
  129. package/dist/services/stage-pause.js +117 -73
  130. package/dist/services/stage-work-graph.js +35 -0
  131. package/dist/services/usage.js +108 -132
  132. package/dist/services/vnext-code-review.js +245 -98
  133. package/dist/services/vnext-code.js +355 -257
  134. package/dist/services/vnext-execution-profile.js +5 -3
  135. package/dist/services/vnext-fanout.js +166 -26
  136. package/dist/services/vnext-merge.js +413 -147
  137. package/dist/services/vnext-plan-review.js +182 -109
  138. package/dist/services/vnext-plan.js +199 -82
  139. package/dist/services/vnext-protocolize.js +133 -60
  140. package/dist/services/vnext-specify.js +94 -71
  141. package/dist/services/work-registry.js +813 -160
  142. package/dist/services/workspace-bootstrap.js +76 -0
  143. package/dist/services/worktrees.js +85 -35
  144. package/dist/shared/entity-references.js +8 -0
  145. package/dist/shared/errors.js +12 -0
  146. package/dist/storage/database.js +569 -16
  147. package/dist/storage/paths.js +17 -4
  148. package/dist/storage/work-references.js +23 -0
  149. package/dist/storage/writer-contract.js +77 -0
  150. package/dist/storage/writer-migration.js +102 -0
  151. package/package.json +24 -13
  152. package/tools/audit-runtime-fix-boundaries.mjs +96 -0
  153. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,90 +1,134 @@
1
+ import { managedLifecycleCommand } from "./lifecycle-invocations.js";
1
2
  import crypto from "node:crypto";
3
+ import { withStageSettlement, prepareFlowRunStageAttachment } 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";
5
7
  import { resolveProjectRoot } from "../storage/paths.js";
6
8
  import { requireProjectByRoot } from "./projects.js";
7
9
  import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunVariables, gitFacts, setRuntimeRunVariable } from "./runs.js";
8
- import { addWorkBatch, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunWorkProjection, workStartCommand } from "./work-registry.js";
10
+ import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunWorkProjection, workStartCommand } from "./work-registry.js";
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
- import { applyExternalStageContext } from "./stage-context.js";
18
- import { capacityProbe, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
19
+ import { assertPortableArtifactRef } from "./portable-refs.js";
20
+ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
21
+ import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
22
+ import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
23
+ import { stageGraphScope } from "./stage-work-graph.js";
19
24
  const stage = "plan-review";
20
25
  const stageDir = vnextStageDirectory(stage);
21
26
  const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
22
- const capacityProbeFanoutSize = capacityProbe.fanout_size;
23
- const capacityProbeHoldSeconds = capacityProbe.probe_hold_seconds;
24
- const capacityProbeDeadlineSeconds = capacityProbe.cleanup_deadline_seconds;
25
27
  export function isVnextPlanReviewRun(context, input) {
26
28
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
27
29
  return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
28
30
  }
29
- export function startVnextPlanReview(context, input) {
30
- ensureWorkRegistry(context);
31
+ export function prepareVnextPlanReviewStart(context, input) {
31
32
  const projectRoot = resolveProjectRoot(input.projectRoot);
32
33
  const project = requireProjectByRoot(context, projectRoot);
33
34
  const run = requireRun(context, projectRoot, input.runId);
34
35
  const home = requireHome(run);
35
- assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
36
- const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
37
36
  const planRoot = path.join(home, "03-plan");
38
37
  const root = path.join(home, stageDir);
39
38
  const batch = path.join(planRoot, "code-work-batch.json");
39
+ const common = { projectRoot, project, run, home, planRoot, root, batch };
40
+ const priorStage = readIndex(run).stage_runs?.find((entry) => entry.stage === stage);
41
+ if (priorStage?.status === "done") {
42
+ const report = readJson(path.join(root, "stage-report.json"));
43
+ const outcome = report.semantic?.plan_review?.outcome;
44
+ if (outcome === "off" || outcome === "accepted")
45
+ return { ...common, state: "terminal", report, outcome };
46
+ throw new AppError("invalid_work_state", "PLAN-REVIEW is already terminal; corrections are applied in the same review Work and are not re-reviewed automatically", 2, { run_id: run.id, outcome });
47
+ }
48
+ const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
40
49
  if (!fs.existsSync(path.join(planRoot, "stage-report.json")) || !fs.existsSync(batch))
41
- throw new AppError("not_found", "PLAN-REVIEW requires the accepted PLAN report and proposed CODE batch", 1);
50
+ throw new AppError("runtime_artifact_missing", "PLAN-REVIEW requires the accepted PLAN report and proposed CODE batch", 1);
42
51
  const requested = requestedMode(run);
43
52
  const protocolIds = protocolIdsForReview(home);
44
53
  assertReviewWorkspace(run.workspace_root, protocolIds);
45
54
  const groups = reviewGroups(home, run.workspace_root);
46
55
  const effective = resolveMode(requested, groups, home, run.workspace_root);
47
56
  const existing = context.db.get("SELECT work_id, status FROM works WHERE project_id = ? AND run_id = ? AND task = ? ORDER BY created_at DESC LIMIT 1", [project.id, run.id, reviewTask]);
57
+ const batchChecksum = checksum(batch), planChecksum = planSetChecksum(home, run.workspace_root);
58
+ if (input.externalContext)
59
+ prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
60
+ prepareOutputFile(path.join(root, "stage-prompt.md"));
61
+ const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum };
48
62
  if (existing?.status === "running") {
49
- const promptPath = path.join(root, "stage-prompt.md");
50
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
51
- return { ...preparedExisting(context, { projectRoot, projectId: project.id, run, root, workId: existing.work_id, hookEventId: input.hookEventId, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}), requested, effective, groups, batchChecksum: checksum(batch), planChecksum: checksum(path.join(planRoot, "stage-report.json")) }), ...(externalContext ? { external_context: externalContext } : {}) };
63
+ const orchestration = readFanoutDescriptor(root);
64
+ if (!orchestration)
65
+ prepareOutputFile(path.join(root, "fanout.json"));
66
+ return { ...active, state: "resumed", workId: existing.work_id, prompt: fs.readFileSync(path.join(root, "stage-prompt.md"), "utf8"), orchestration };
52
67
  }
53
- const priorStage = readIndex(run).stage_runs?.find((entry) => entry.stage === stage);
54
- if (priorStage?.status === "done") {
55
- const report = readJson(path.join(root, "stage-report.json"));
56
- const outcome = report.semantic?.plan_review?.outcome;
57
- if (outcome === "off" || outcome === "accepted")
58
- 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 };
59
- throw new AppError("invalid_work_state", "PLAN-REVIEW is already terminal; corrections are applied in the same review Work and are not re-reviewed automatically", 2, { run_id: run.id, outcome });
60
- }
61
- fs.mkdirSync(root, { recursive: true });
62
- const now = context.now();
63
- const batchChecksum = checksum(batch);
64
- const planChecksum = checksum(path.join(planRoot, "stage-report.json"));
68
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
69
+ const rootWork = rootWorkId(context, project.id, run.id);
65
70
  if (effective === "off") {
66
71
  const failures = validateVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIds });
67
72
  if (failures.length)
68
- throw new AppError("validation", "PLAN artifacts have validation errors", 2, { errors: failures });
69
- const rootWork = rootWorkId(context, project.id, run.id);
73
+ throw new AppError("runtime_artifact_invalid", "Accepted PLAN artifacts have validation errors", 1, { errors: failures });
74
+ // This is retained runtime data, not a newly supplied --file argument.
75
+ const codeBatch = readJson(batch);
76
+ let codeItems;
77
+ try {
78
+ codeItems = prepareWorkBatch(context, { parentWorkId: rootWork, value: codeBatch });
79
+ }
80
+ catch (error) {
81
+ if (error instanceof AppError && error.exitCode === 2)
82
+ throw new AppError("runtime_artifact_invalid", "Accepted CODE batch cannot be registered", 1, { file: batch, cause_code: error.code });
83
+ throw error;
84
+ }
85
+ for (const name of ["stage-report.json", "stage-report.md", "stage-report.html"])
86
+ prepareOutputFile(path.join(root, name));
87
+ return { ...active, state: "off", rootWork, codeItems };
88
+ }
89
+ const template = read(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "plan-review.md"));
90
+ const revision = currentPlanRevision(home, run.workspace_root);
91
+ const capacity = getStageFanoutCapacity(context, { projectRoot, runId: run.id, stage });
92
+ for (const name of ["work-context.json", "fanout.json"])
93
+ prepareOutputFile(path.join(root, name));
94
+ return { ...active, state: "fresh", rootWork, template, revision, capacity };
95
+ }
96
+ export function startVnextPlanReview(context, input, prepared = prepareVnextPlanReviewStart(context, input)) {
97
+ const { projectRoot, project, run, home, root, planRoot } = prepared;
98
+ assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
99
+ if (prepared.state === "terminal") {
100
+ const { report, outcome } = prepared;
101
+ 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 };
102
+ }
103
+ const { requested, effective, groups, batchChecksum, planChecksum, workspaceRoute } = prepared;
104
+ if (prepared.state === "resumed") {
105
+ const promptPath = path.join(root, "stage-prompt.md");
106
+ const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
107
+ 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 } : {}) };
108
+ }
109
+ ensureWorkRegistry(context);
110
+ fs.mkdirSync(root, { recursive: true });
111
+ const now = context.now(), rootWork = prepared.rootWork;
112
+ if (prepared.state === "off") {
70
113
  const promptPath = path.join(root, "stage-prompt.md");
71
114
  fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
72
115
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
73
116
  bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
74
117
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
75
- const registered = registerCode(context, project.id, run.id, batch);
76
- const report = reportFor({ run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
77
- writeReport(root, report);
78
- 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" });
79
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
80
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
81
- 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) } };
118
+ return withStageSettlement(context, () => {
119
+ const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
120
+ const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
121
+ writeReport(root, report);
122
+ 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" });
123
+ advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
124
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
125
+ 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) } };
126
+ });
82
127
  }
83
- const rootWork = rootWorkId(context, project.id, run.id);
84
128
  const workId = nextWorkId(context, project.id, "plan-review");
85
129
  context.db.run("INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, depends_on_json, status, result, started_at, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, 'reuse_allowed', NULL, '[]', 'running', NULL, ?, ?, ?, NULL)", [workId, project.id, run.id, rootWork, reviewTask, now, now, now]);
86
130
  const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId, projectRoot });
87
- const prompt = orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand) });
131
+ 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 });
88
132
  const promptPath = path.join(root, "stage-prompt.md");
89
133
  fs.writeFileSync(promptPath, prompt);
90
134
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
@@ -101,8 +145,8 @@ export function startVnextPlanReview(context, input) {
101
145
  markReviewPending(home, groups);
102
146
  const workSessionId = String(binding.work_session_id);
103
147
  const sessionId = String(binding.session_id);
104
- const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true });
105
- 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 } });
148
+ const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
149
+ 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: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
106
150
  refreshRunWorkProjection(context, project.id, run.id);
107
151
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
108
152
  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")) } };
@@ -125,15 +169,15 @@ export function dispatchVnextPlanReview(context, input) {
125
169
  if (capacity.pending)
126
170
  return capacity.response;
127
171
  if (capacity.availableSlots < 1)
128
- throw new AppError("no_subagent_capacity", "PLAN-REVIEW requires one fresh reviewer Session but no probe launch succeeded", 1, { run_id: run.id });
172
+ throw new AppError("subagent_capacity_unqualified", "PLAN-REVIEW requires one qualified native subagent slot", 1, { run_id: run.id });
129
173
  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]);
130
174
  const latest = (group) => existing.find((item) => item.task === group.task);
131
175
  const pending = groups.filter((group) => !latest(group));
132
176
  if (pending.length) {
133
177
  const file = path.join(root, ".dispatch.json");
134
- writeJson(file, { works: pending.map((group) => ({ key: group.key, task: group.task, depends_on: group.depends_on, launch_policy: "fresh_agent_required", result_schema: "dd-flow/plan-review-result@1", payload: { kind: "plan-review", group: { key: group.key, aspect_ids: group.aspect_ids } } })) });
178
+ writeJson(file, { works: pending.map((group) => ({ key: group.key, task: group.task, depends_on: group.depends_on, launch_policy: "fresh_agent_required", result_schema: "dd-flow/plan-review-result@1", payload: { kind: "plan-review", read_only: true, group: { key: group.key, aspect_ids: group.aspect_ids } } })) });
135
179
  try {
136
- addWorkBatch(context, { parentWorkId: parent.work_id, file });
180
+ addWorkBatch(context, { parentWorkId: parent.work_id, file, stage });
137
181
  }
138
182
  finally {
139
183
  fs.rmSync(file, { force: true });
@@ -145,7 +189,7 @@ export function dispatchVnextPlanReview(context, input) {
145
189
  refreshRunWorkProjection(context, project.id, run.id);
146
190
  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 })) });
147
191
  const waves = plannedReviewWaves(groups, capacity.availableSlots);
148
- 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" };
192
+ 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" };
149
193
  }
150
194
  // Capacity is a RUN fact shared by every fan-out stage.
151
195
  const reviewCapacityKey = subagentCapacityKey;
@@ -161,16 +205,19 @@ export function recordVnextPlanReviewCapacity(context, input) {
161
205
  const prior = variables.variables[reviewCapacityKey];
162
206
  if (typeof prior === "number") {
163
207
  if (prior === input.availableSlots)
164
- return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "bounded_probe", reused: true } };
208
+ return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "harness_qualification", reused: true } };
165
209
  setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
166
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots, fanout_size: capacityProbeFanoutSize });
167
- return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "bounded_probe", refreshed: true } };
210
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots, source: "harness_qualification" });
211
+ return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "harness_qualification", refreshed: true } };
168
212
  }
169
213
  setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
170
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots, fanout_size: capacityProbeFanoutSize });
171
- return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "one_shot_fanout", fanout_size: capacityProbeFanoutSize } };
214
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots, source: "harness_qualification" });
215
+ return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "harness_qualification" } };
172
216
  }
173
217
  export function finishVnextPlanReview(context, input) {
218
+ return withStageSettlement(context, () => finishPlanReviewOwned(context, input));
219
+ }
220
+ function finishPlanReviewOwned(context, input) {
174
221
  const projectRoot = resolveProjectRoot(input.projectRoot);
175
222
  const project = requireProjectByRoot(context, projectRoot);
176
223
  const run = requireRun(context, projectRoot, input.runId);
@@ -182,22 +229,26 @@ export function finishVnextPlanReview(context, input) {
182
229
  if (!parent || parent.status !== "running")
183
230
  throw new AppError("invalid_work_state", "PLAN-REVIEW has no running parent Work", 2);
184
231
  const decisionPath = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
185
- const decision = readDecision(context, projectRoot, run.id, decisionPath);
186
- 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]);
232
+ const decision = input.decision ?? readDecision(context, projectRoot, run.id, home, decisionPath);
233
+ validateSchema({ schemaName: "plan-review-decision", file: decisionPath, data: decision, projectRoot, runId: run.id, runRoot: home, ddFlowHome: context.ddFlowHome });
234
+ 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]);
187
235
  const contextFile = readJson(path.join(root, "work-context.json"));
188
236
  const groups = contextFile.groups ?? [];
189
237
  const reviewedPlanRevision = contextFile.system?.plan_revision;
190
238
  if (!Number.isInteger(reviewedPlanRevision) || reviewedPlanRevision < 1)
191
239
  throw new AppError("runtime_missing", "PLAN-REVIEW starting revision is unavailable", 1);
192
240
  const planRevision = currentPlanRevision(home, run.workspace_root);
193
- if (["blocked", "failed", "cancelled"].includes(decision.outcome)) {
194
- preserveDecisionReceipt(root, decisionPath);
195
- return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision });
241
+ const currentPlanChecksum = planSetChecksum(home, run.workspace_root);
242
+ if (decision.outcome === "blocked") {
243
+ 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 });
244
+ }
245
+ if (["failed", "cancelled"].includes(decision.outcome)) {
246
+ return withStageSettlement(context, () => { preserveDecisionReceipt(root, decision, input.decisionText); return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision }); });
196
247
  }
197
248
  if (children.some((child) => child.status === "created" || child.status === "running"))
198
249
  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 })) });
199
250
  const latestChildren = latestReviewChildren(children);
200
- const evidenceFailures = reviewerEvidenceFailures(context, { projectId: project.id, parentWorkId: parent.work_id, children: latestChildren, groups, planRevision: reviewedPlanRevision });
251
+ const evidenceFailures = reviewerEvidenceFailures(context, { projectId: project.id, parentWorkId: parent.work_id, children: latestChildren, groups, planRevision: reviewedPlanRevision, workspaceRoot: run.workspace_root, runHome: home, runId: run.id });
201
252
  if (evidenceFailures.length)
202
253
  throw new AppError("review_evidence_invalid", "PLAN-REVIEW reviewer evidence is incomplete, stale, or not isolated", 2, { errors: evidenceFailures });
203
254
  if (decision.outcome !== "accepted")
@@ -209,56 +260,64 @@ export function finishVnextPlanReview(context, input) {
209
260
  const needsCorrection = latestChildren.some((child) => ["needs_changes", "blocked"].includes(reviewerVerdict(child.result)));
210
261
  if (latestChildren.some((child) => child.status !== "completed"))
211
262
  throw new AppError("blocked", "PLAN-REVIEW cannot accept incomplete reviewer evidence", 2);
212
- const materialFindingIds = canonicalReviewerFindings(latestChildren).filter(({ finding }) => ["blocker", "high", "medium"].includes(finding.severity)).map(({ finding_ref }) => finding_ref);
213
- const decidedFindingIds = new Set(decision.finding_decisions.map((finding) => finding.finding_ref).filter((id) => Boolean(id)));
214
- if (new Set(materialFindingIds).size !== materialFindingIds.length || materialFindingIds.some((id) => !decidedFindingIds.has(id)))
215
- throw new AppError("review_evidence_invalid", "Every material reviewer finding needs one stable decision", 2, { material_finding_ids: materialFindingIds, decided_finding_ids: [...decidedFindingIds] });
263
+ const canonicalFindings = canonicalReviewerFindings(latestChildren);
264
+ const allFindingIds = canonicalFindings.map(({ finding_ref }) => finding_ref);
265
+ const materialFindingIds = canonicalFindings.filter(({ finding }) => ["blocker", "high", "medium"].includes(finding.severity)).map(({ finding_ref }) => finding_ref);
266
+ const decisionFindingIds = decision.finding_decisions.map((finding) => finding.finding_ref).filter((id) => Boolean(id));
267
+ const decidedFindingIds = new Set(decisionFindingIds);
268
+ const unknownDecisions = decisionFindingIds.filter((id) => !allFindingIds.includes(id));
269
+ if (new Set(allFindingIds).size !== allFindingIds.length || decidedFindingIds.size !== decisionFindingIds.length || unknownDecisions.length || materialFindingIds.some((id) => !decidedFindingIds.has(id)))
270
+ 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 });
216
271
  if (needsCorrection) {
217
272
  if (decision.correction.status !== "applied")
218
273
  throw new AppError("validation", "Reviewer findings require an in-place correction receipt", 2);
219
274
  if (decision.correction.previous_plan_revision !== reviewedPlanRevision || planRevision <= reviewedPlanRevision || decision.correction.changed_paths.length === 0)
220
275
  throw new AppError("validation", "Applied PLAN correction must advance revision and list changed paths", 2, { reviewed_plan_revision: reviewedPlanRevision, final_plan_revision: planRevision });
221
276
  }
222
- else if (decision.correction.status !== "not_required" || planRevision !== reviewedPlanRevision) {
277
+ else if (decision.correction.status !== "not_required" || planRevision !== reviewedPlanRevision || currentPlanChecksum !== contextFile.system?.plan_checksum) {
223
278
  throw new AppError("validation", "A clean PLAN review must not claim an unverified correction", 2);
224
279
  }
225
- const planFailures = validateVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIdsForReview(home) });
226
- if (planFailures.length)
227
- throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors: planFailures });
280
+ const prepared = prepareVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIdsForReview(home) });
281
+ if (prepared.failures.length)
282
+ throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors: prepared.failures });
228
283
  if (needsCorrection) {
229
- const initialBatchChecksum = contextFile.system?.batch_checksum;
230
- if (!initialBatchChecksum || checksum(batch) === initialBatchChecksum)
231
- throw new AppError("stale_code_work_batch", "Applied PLAN correction must regenerate the proposed CODE batch", 2, { batch, initial_batch_checksum: initialBatchChecksum });
284
+ // Regeneration above is authoritative. A valid PLAN correction need not
285
+ // change the derived CODE tasks (for example, a clarified explanation).
232
286
  if (decision.correction.changed_paths.some((item) => item.endsWith("code-work-batch.json")))
233
287
  throw new AppError("validation", "Generated CODE batch must not be listed as an agent-authored correction", 2, { changed_paths: decision.correction.changed_paths });
234
288
  }
235
- preserveDecisionReceipt(root, decisionPath);
236
- const finalPlanChecksum = checksum(path.join(run.workspace_root, ".memory-bank", "protocol", protocolIdsForReview(home)[0], "plan.json"));
237
- const finalBatchChecksum = checksum(batch);
238
- const registered = registerCode(context, project.id, run.id, batch);
239
- const now = context.now();
240
- 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") });
241
- 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) });
242
- applyReviewResults(home, latestChildren, decision);
243
- writeReport(root, report);
244
- refreshRunWorkProjection(context, project.id, run.id);
245
- 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" });
246
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
247
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
248
- 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) } };
289
+ const finalPlanChecksum = planSetChecksum(home, run.workspace_root);
290
+ const finalBatchChecksum = prepared.batchChecksum;
291
+ return withStageSettlement(context, () => {
292
+ prepared.publish();
293
+ preserveDecisionReceipt(root, decision, input.decisionText);
294
+ const registered = registerCode(context, project.id, run.id, batch);
295
+ const now = context.now();
296
+ 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") });
297
+ 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) });
298
+ applyReviewResults(home, latestChildren, decision);
299
+ writeReport(root, report);
300
+ refreshRunWorkProjection(context, project.id, run.id);
301
+ 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" });
302
+ advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
303
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
304
+ 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) } };
305
+ });
249
306
  }
250
307
  function preparedExisting(context, input) {
251
308
  const promptPath = path.join(input.root, "stage-prompt.md");
252
309
  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 } : {}) });
253
- const orchestration = readFanoutDescriptor(input.root) ?? writeFanoutDescriptor(input.root, { stage, parent_work_id: input.workId, dispatch: "plan-review", capacity_required: true });
310
+ 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) });
254
311
  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")) } };
255
312
  }
256
313
  function orchestratorPrompt(context, input) {
257
- const template = read(path.join(input.projectRoot, ".memory-bank", "dd-flow", "vnext", "plan-review.md"));
314
+ const { template, revision, capacity } = input;
258
315
  const decision = path.join(input.root, "decision.json");
259
- const revision = currentPlanRevision(input.home, input.run.workspace_root);
260
316
  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");
261
- 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 | blocked | 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");
317
+ const reviewerLaunch = capacity.source === "external_policy"
318
+ ? `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.`
319
+ : "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.";
320
+ 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");
262
321
  }
263
322
  function reviewGroups(home, workspaceRoot) {
264
323
  const root = path.join(home, "03-plan");
@@ -318,15 +377,14 @@ function plannedReviewWaves(groups, capacity) {
318
377
  }
319
378
  return waves;
320
379
  }
321
- export function capacityProbeResponse(context, input) {
322
- 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) } };
380
+ export function capacityQualificationResponse(context, input) {
381
+ 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) } };
323
382
  }
324
383
  function ensureReviewCapacity(context, input) {
325
- const variables = getFlowRunVariables(context, { projectRoot: input.projectRoot, runId: input.runId });
326
- const known = variables.variables[reviewCapacityKey];
327
- if (typeof known === "number" && Number.isInteger(known) && known >= 0)
328
- return { pending: false, availableSlots: known };
329
- return { pending: true, response: capacityProbeResponse(context, { projectRoot: input.projectRoot, runId: input.runId }) };
384
+ const capacity = getStageFanoutCapacity(context, { ...input, stage });
385
+ if (capacity.available_slots !== null)
386
+ return { pending: false, availableSlots: capacity.available_slots, source: capacity.source };
387
+ return { pending: true, response: capacityQualificationResponse(context, { projectRoot: input.projectRoot, runId: input.runId }) };
330
388
  }
331
389
  function protocolIdsForReview(home) {
332
390
  const report = readJson(path.join(home, "02-protocolize", "stage-report.json"));
@@ -375,11 +433,14 @@ catch {
375
433
  } }
376
434
  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)
377
435
  throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
378
- function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch }); }
379
- function readDecision(context, projectRoot, runId, file) { validateSchema({ schemaName: "plan-review-decision", file, projectRoot, runId, ddFlowHome: context.ddFlowHome }); return readJson(file); }
380
- function preserveDecisionReceipt(root, source) {
436
+ function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch, stage: "code" }); }
437
+ function readDecision(context, projectRoot, runId, runRoot, file) { return prepareJsonSchema(file, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, runId, runRoot, ddFlowHome: context.ddFlowHome }).value; }
438
+ function preserveDecisionReceipt(root, decision, preparedText) {
381
439
  const receipt = path.join(root, "decision.receipt.json");
382
- const bytes = fs.readFileSync(source);
440
+ // The CLI has already parsed and validated this decision during prepare.
441
+ // Persist that immutable value rather than re-opening a caller-controlled
442
+ // filename after lifecycle admission.
443
+ const bytes = Buffer.from(preparedText ?? `${JSON.stringify(decision, null, 2)}\n`);
383
444
  if (fs.existsSync(receipt)) {
384
445
  if (!bytes.equals(fs.readFileSync(receipt)))
385
446
  throw new AppError("decision_receipt_conflict", "The immutable PLAN-REVIEW decision receipt already contains a different payload", 2, { receipt });
@@ -417,8 +478,19 @@ function reviewerEvidenceFailures(context, input) {
417
478
  continue;
418
479
  }
419
480
  const actual = result.aspects.map((item) => item.aspect_id);
420
- 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))
481
+ if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id)))
421
482
  failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_coverage_invalid", expected_aspects: group.aspect_ids, actual_aspects: actual });
483
+ const missingEvidence = result.aspects.filter((item) => item.evidence_refs.length === 0).map((item) => item.aspect_id);
484
+ if (missingEvidence.length)
485
+ failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_evidence_missing", missing_aspects: missingEvidence });
486
+ for (const ref of result.aspects.flatMap((item) => [...item.evidence_refs, ...item.findings.flatMap((finding) => finding.evidence_refs)])) {
487
+ try {
488
+ assertPortableArtifactRef(ref, { workspaceRoot: input.workspaceRoot, runHome: input.runHome, runId: input.runId });
489
+ }
490
+ catch (error) {
491
+ failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_evidence_ref_invalid", ref, detail: error instanceof Error ? error.message : String(error) });
492
+ }
493
+ }
422
494
  const workSession = context.db.get("SELECT session_id, status FROM work_sessions WHERE work_id = ? ORDER BY created_at DESC LIMIT 1", [child.work_id]);
423
495
  if (!workSession?.session_id || workSession.status !== "completed" || workSession.session_id === parent?.session_id)
424
496
  failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_session_not_fresh", parent_session_id: parent?.session_id ?? null, reviewer_session_id: workSession?.session_id ?? null });
@@ -437,15 +509,15 @@ function finishTerminalReview(context, input) {
437
509
  const now = context.now();
438
510
  const childStatus = input.decision.outcome === "cancelled" ? "cancelled" : "failed";
439
511
  // A terminal decision is structured cancellation of the review subtree. It
440
- // must settle probes as well as semantic reviewers before its parent Work
441
- // can settle; otherwise the RUN contains abandoned active Work/Session links.
512
+ // must settle semantic reviewers before its parent Work can settle; otherwise
513
+ // the RUN contains abandoned active Work/Session links.
442
514
  settleReviewChildren(context, { projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, reason: `PLAN-REVIEW terminal decision: ${input.decision.summary}`, now });
443
515
  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") });
444
516
  const rootWork = rootWorkId(context, input.projectId, input.run.id);
445
517
  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]);
446
518
  closeRunningWorkSession(context, rootWork, childStatus, now);
447
- 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]);
448
- 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 ?? checksum(path.join(input.planRoot, "stage-report.json")), code: {}, now, projectRoot: input.projectRoot, decision: input.decision, children: latestReviewChildren(settledChildren), flow: flowCommand(context) });
519
+ 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]);
520
+ 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) });
449
521
  writeReport(input.root, report);
450
522
  refreshRunWorkProjection(context, input.projectId, input.run.id);
451
523
  const stageStatus = input.decision.outcome === "cancelled" ? "skipped" : input.decision.outcome === "failed" ? "failed" : "blocked";
@@ -464,7 +536,7 @@ function settleReviewChildren(context, input) {
464
536
  function closeRunningWorkSession(context, workId, status, now) {
465
537
  context.db.run("UPDATE work_sessions SET status = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [status, now, now, workId]);
466
538
  }
467
- function reportFor(input) {
539
+ function reportFor(context, input) {
468
540
  const accepted = input.outcome === "accepted" || input.outcome === "off";
469
541
  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}.`;
470
542
  const decision = input.decision;
@@ -473,27 +545,28 @@ function reportFor(input) {
473
545
  const sessionCommand = `${input.flow} stat run sessions ls --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
474
546
  const usageCommand = `${input.flow} stat usage --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
475
547
  const changedFiles = correction?.status === "applied" ? correction.changed_paths : [];
476
- 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" } };
548
+ 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" } };
477
549
  }
478
550
  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 }; }
479
551
  function writeReport(root, report) {
480
552
  writeStageReport(root, report);
481
553
  }
482
- function codeCommand(context, runId, projectRoot) { return `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`; }
554
+ function codeCommand(context, runId, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`); }
483
555
  function dispatchCommand(context, runId, projectRoot) { return `${flowCommand(context)} plan-review dispatch ${runId} --project-root ${JSON.stringify(projectRoot)} --json`; }
484
- 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`; }
556
+ 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`); }
485
557
  function capacityRecordCommand(context, runId, projectRoot, availableSlots) { return `${flowCommand(context)} run capacity record ${runId} --available-slots ${availableSlots} --project-root ${JSON.stringify(projectRoot)} --json`; }
486
- 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)
558
+ 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)
487
559
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
488
- function requireHome(run) { if (!run.run_home_path)
489
- throw new AppError("runtime_missing", "RUN workspace is unavailable", 1); return run.run_home_path; }
560
+ function requireHome(run) { if (!run.run_root)
561
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
490
562
  function read(file) { if (!fs.existsSync(file))
491
- throw new AppError("not_found", "Required vNext prompt is missing", 1, { file }); return fs.readFileSync(file, "utf8"); }
563
+ throw new AppError("runtime_artifact_missing", "Required vNext prompt is missing", 1, { file }); return fs.readFileSync(file, "utf8"); }
492
564
  function readJson(file) { try {
493
565
  return JSON.parse(fs.readFileSync(file, "utf8"));
494
566
  }
495
567
  catch {
496
- throw new AppError("validation", "Expected valid JSON artifact", 2, { file });
568
+ throw new AppError("runtime_artifact_invalid", "Expected valid retained JSON artifact", 1, { file });
497
569
  } }
498
570
  function writeJson(file, value) { const temporary = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
499
571
  function checksum(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
572
+ function planSetChecksum(home, workspaceRoot) { const entries = protocolIdsForReview(home).sort().map((protocolId) => { const file = path.join(workspaceRoot, ".memory-bank", "protocol", protocolId, "plan.json"); return { protocol_id: protocolId, path: path.relative(workspaceRoot, file).split(path.sep).join("/"), sha256: checksum(file) }; }); return crypto.createHash("sha256").update(JSON.stringify(entries)).digest("hex"); }