@deksden-com/dd-flow-cli 0.9.0-beta.9 → 0.9.0-beta.91

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 (144) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +89 -13
  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 +1421 -384
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  11. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  12. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  13. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  14. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  15. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  16. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  17. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  18. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  19. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  21. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  22. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  23. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  24. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  25. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  26. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  27. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  28. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  29. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  30. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  32. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  33. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  34. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  36. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  37. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  38. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  39. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  40. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  41. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  42. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  43. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  44. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  45. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  46. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  47. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  48. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  49. package/dist/schemas/agent-profile.schema.json +1 -1
  50. package/dist/schemas/code-review-result.schema.json +2 -2
  51. package/dist/schemas/code-work-batch.schema.json +4 -3
  52. package/dist/schemas/code-work-result.schema.json +4 -4
  53. package/dist/schemas/plan-review-result.schema.json +2 -2
  54. package/dist/schemas/run-control-receipt.schema.json +99 -0
  55. package/dist/schemas/run-control-request.schema.json +36 -0
  56. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  57. package/dist/services/canon.js +11 -8
  58. package/dist/services/cleanup.js +57 -13
  59. package/dist/services/cli-operation-classifier.js +22 -24
  60. package/dist/services/code-checks.js +543 -96
  61. package/dist/services/codex-hook-delivery.js +28 -0
  62. package/dist/services/command-context.js +80 -0
  63. package/dist/services/config.js +11 -8
  64. package/dist/services/continuation-outcome.js +20 -0
  65. package/dist/services/controller-fanout.js +132 -0
  66. package/dist/services/dashboard.js +103 -31
  67. package/dist/services/engines.js +121 -76
  68. package/dist/services/eval-snapshots.js +750 -73
  69. package/dist/services/execution-policy.js +170 -0
  70. package/dist/services/external-work-launch.js +110 -0
  71. package/dist/services/harness-adapter.js +147 -24
  72. package/dist/services/harness-config.js +2 -0
  73. package/dist/services/hooks.js +587 -299
  74. package/dist/services/lanes.js +61 -53
  75. package/dist/services/lifecycle-command.js +103 -17
  76. package/dist/services/lifecycle-contract.js +30 -0
  77. package/dist/services/lifecycle-invocations.js +1122 -0
  78. package/dist/services/managed-daemon-binding.js +38 -0
  79. package/dist/services/managed-processes.js +283 -27
  80. package/dist/services/merge-queue.js +179 -104
  81. package/dist/services/merge-server.js +73 -28
  82. package/dist/services/migrations.js +14 -9
  83. package/dist/services/native-daemon-history.js +49 -0
  84. package/dist/services/native-session-control.js +39 -0
  85. package/dist/services/plan-runtime.js +19 -8
  86. package/dist/services/plans.js +28 -25
  87. package/dist/services/projects.js +18 -5
  88. package/dist/services/prompts.js +23 -16
  89. package/dist/services/protocols.js +64 -20
  90. package/dist/services/recovery-observation-budget.js +61 -0
  91. package/dist/services/recovery-snapshot-database.js +107 -0
  92. package/dist/services/repair-intents.js +40 -0
  93. package/dist/services/run-control-receipt.js +56 -0
  94. package/dist/services/run-control-worker.js +363 -0
  95. package/dist/services/run-control.js +876 -0
  96. package/dist/services/run-controller-adapter.js +222 -0
  97. package/dist/services/run-controller-capture.js +134 -0
  98. package/dist/services/run-controller-process.js +189 -0
  99. package/dist/services/run-controller-recovery.js +222 -0
  100. package/dist/services/run-controller-state.js +36 -0
  101. package/dist/services/run-controller.js +793 -0
  102. package/dist/services/run-engine-bindings.js +1 -1
  103. package/dist/services/run-fork.js +138 -0
  104. package/dist/services/run-observations.js +112 -0
  105. package/dist/services/run-recovery-runtime.js +69 -0
  106. package/dist/services/run-recovery.js +343 -0
  107. package/dist/services/runs.js +368 -74
  108. package/dist/services/runtime-budget.js +434 -0
  109. package/dist/services/runtime-command.js +43 -0
  110. package/dist/services/runtime-scope-capture.js +50 -0
  111. package/dist/services/runtime-scope-control.js +450 -0
  112. package/dist/services/runtime-scope-resume.js +565 -0
  113. package/dist/services/runtime-scope-stop.js +99 -0
  114. package/dist/services/runtime-scope-worker.js +243 -0
  115. package/dist/services/runtime-service.js +97 -0
  116. package/dist/services/schema-validation.js +10 -8
  117. package/dist/services/sessions.js +35 -69
  118. package/dist/services/stage-blocker.js +17 -7
  119. package/dist/services/stage-context.js +44 -18
  120. package/dist/services/stage-lifecycle.js +99 -102
  121. package/dist/services/stage-pause.js +84 -55
  122. package/dist/services/stage-work-graph.js +35 -0
  123. package/dist/services/usage.js +37 -93
  124. package/dist/services/vnext-code-review.js +195 -88
  125. package/dist/services/vnext-code.js +285 -252
  126. package/dist/services/vnext-execution-profile.js +5 -3
  127. package/dist/services/vnext-fanout.js +160 -17
  128. package/dist/services/vnext-merge.js +291 -104
  129. package/dist/services/vnext-plan-review.js +139 -81
  130. package/dist/services/vnext-plan.js +138 -69
  131. package/dist/services/vnext-protocolize.js +129 -56
  132. package/dist/services/vnext-specify.js +88 -65
  133. package/dist/services/work-registry.js +716 -168
  134. package/dist/services/workspace-bootstrap.js +76 -0
  135. package/dist/services/worktrees.js +85 -35
  136. package/dist/shared/entity-references.js +8 -0
  137. package/dist/shared/errors.js +12 -0
  138. package/dist/storage/database.js +459 -32
  139. package/dist/storage/paths.js +17 -4
  140. package/dist/storage/work-references.js +23 -0
  141. package/dist/storage/writer-contract.js +77 -0
  142. package/dist/storage/writer-migration.js +102 -0
  143. package/package.json +24 -13
  144. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,24 +1,28 @@
1
+ import { managedLifecycleCommand } from "./lifecycle-invocations.js";
1
2
  import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
- import { execFileSync } from "node:child_process";
5
5
  import { AppError } from "../shared/errors.js";
6
6
  import { resolveProjectRoot } from "../storage/paths.js";
7
7
  import { requireProjectByRoot } from "./projects.js";
8
8
  import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
9
9
  import { validateSchema } from "./schema-validation.js";
10
- import { flowCommand } from "./stage-pause.js";
10
+ import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
11
11
  import { assertStageStartHookEvent } from "./hooks.js";
12
12
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
13
- import { addVnextCodeRepair } from "./vnext-code.js";
14
- import { finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
15
- import { addWorkBatch, bindStageCoordinatorWork, finishWork, refreshRunWorkProjection } from "./work-registry.js";
13
+ import { addVnextCodeRepair, prepareVnextCodeRepair, registerAggregateCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
14
+ import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
15
+ import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
16
+ import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
16
17
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
17
18
  import { writeStageReport } from "./stage-report-renderer.js";
18
- import { applyExternalStageContext } from "./stage-context.js";
19
+ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
19
20
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
20
- import { ensureVnextMergeRequest } from "./vnext-merge.js";
21
+ import { stageGraphScope } from "./stage-work-graph.js";
22
+ import { prepareVnextMergeRequest } from "./vnext-merge.js";
21
23
  import { validateVnextCodeHandoff } from "./vnext-plan.js";
24
+ import { assertRunMutationAllowed } from "./run-recovery.js";
25
+ import { prepareJsonFile, prepareOutputFile } from "../cli/input-preparation.js";
22
26
  const stage = "code-review";
23
27
  const stageDir = vnextStageDirectory(stage);
24
28
  const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
@@ -26,33 +30,57 @@ export function isVnextCodeReviewRun(context, input) {
26
30
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
27
31
  return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
28
32
  }
29
- export function startVnextCodeReview(context, input) {
33
+ export function prepareVnextCodeReviewStart(context, input) {
30
34
  const projectRoot = resolveProjectRoot(input.projectRoot);
31
35
  const project = requireProjectByRoot(context, projectRoot);
32
36
  const run = requireRun(context, project.id, input.runId);
33
37
  const home = requireHome(run);
34
- assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
38
+ assertRunMutationAllowed(context, project.id, run.id);
35
39
  requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
36
40
  if (!fs.existsSync(path.join(home, "05-code", "stage-report.json")))
37
- throw new AppError("not_found", "CODE-REVIEW requires an accepted CODE report", 1);
41
+ throw new AppError("runtime_artifact_missing", "CODE-REVIEW requires an accepted CODE report", 1);
38
42
  const root = path.join(home, stageDir);
39
- fs.mkdirSync(root, { recursive: true });
40
43
  const rootWork = requireRootWork(context, project.id, run.id);
41
44
  const prior = stageStatus(run, stage);
42
45
  const existingPrompt = path.join(root, "stage-prompt.md");
43
- if (prior && fs.existsSync(existingPrompt)) {
46
+ if (input.externalContext)
47
+ prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
48
+ prepareOutputFile(existingPrompt);
49
+ const common = { projectRoot, project, run, home, root, rootWork, existingPrompt };
50
+ if (prior && fs.existsSync(existingPrompt))
51
+ return { ...common, resumed: true, prior, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(root) };
52
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
53
+ const mode = effectiveMode(home, run);
54
+ const cycle = reviewCycle(run);
55
+ const codeReportSha256 = sha256File(path.join(home, "05-code", "stage-report.json"));
56
+ const groups = mode.mode === "off" ? [] : reviewGroups(home);
57
+ const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", review_cycle: cycle, group, read_only: true } })) };
58
+ const items = mode.mode === "off" ? [] : prepareWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch });
59
+ const template = mode.mode === "off" ? "" : read(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md"));
60
+ const checks = mode.mode === "off" ? [] : acceptedCodeChecks(context, project.id, run.id).map(({ id, purpose }) => ({ id, purpose }));
61
+ for (const name of ["review-context.json", ...(mode.mode === "off" ? ["decision.json"] : ["review-work-batch.json", "fanout.json"])])
62
+ prepareOutputFile(path.join(root, name));
63
+ return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks };
64
+ }
65
+ export function startVnextCodeReview(context, input, prepared = prepareVnextCodeReviewStart(context, input)) {
66
+ const { projectRoot, project, run, root, rootWork, existingPrompt } = prepared;
67
+ assertRunMutationAllowed(context, project.id, run.id);
68
+ assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
69
+ fs.mkdirSync(root, { recursive: true });
70
+ if (prepared.resumed) {
44
71
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
45
72
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
46
73
  const prompt = fs.readFileSync(existingPrompt, "utf8");
47
- const orchestration = readFanoutDescriptor(root);
48
- return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
74
+ const orchestration = prepared.orchestration;
75
+ return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
49
76
  }
50
- const mode = effectiveMode(home, run);
77
+ const { mode, cycle, groups, batch, items, template, checks, codeReportSha256 } = prepared;
51
78
  freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
52
79
  const promptPath = path.join(root, "stage-prompt.md");
53
80
  const reviewContextPath = path.join(root, "review-context.json");
54
- fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, workspace_fingerprint: workspaceFingerprint(run.workspace_root), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
81
+ fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id } } : {}), code_report_sha256: codeReportSha256 }, null, 2)}\n`);
55
82
  if (mode.mode === "off") {
83
+ fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
56
84
  const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n`;
57
85
  fs.writeFileSync(promptPath, prompt);
58
86
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
@@ -60,26 +88,27 @@ export function startVnextCodeReview(context, input) {
60
88
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
61
89
  return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
62
90
  }
63
- const groups = reviewGroups(home);
64
- const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", group, read_only: true } })) };
65
91
  const batchFile = path.join(root, "review-work-batch.json");
66
92
  fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
67
- const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups });
93
+ const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks });
68
94
  fs.writeFileSync(promptPath, prompt);
69
95
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
70
96
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
71
- const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, file: batchFile });
72
- const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
97
+ const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items, stage, cycle });
98
+ const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
73
99
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
74
100
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
75
101
  return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
76
102
  }
77
- /** Create a narrow CODE repair from accepted independent-review evidence. */
78
- export function addVnextCodeReviewRepair(context, input) {
103
+ export function prepareVnextCodeReviewRepair(context, input) {
104
+ if (!Array.isArray(input.findingIds) || !input.findingIds.length || input.findingIds.some(id => typeof id !== "string" || !id.trim()))
105
+ throw new AppError("validation", "Repair requires nonempty finding IDs", 2);
106
+ if (!input.checkRefsByFinding || typeof input.checkRefsByFinding !== "object" || Array.isArray(input.checkRefsByFinding) || Object.entries(input.checkRefsByFinding).some(([id, refs]) => !input.findingIds.includes(id) || !Array.isArray(refs) || refs.some(ref => typeof ref !== "string" || !ref.trim())))
107
+ throw new AppError("validation", "Repair check references must map selected findings to check IDs", 2);
79
108
  const projectRoot = resolveProjectRoot(input.projectRoot);
80
109
  const project = requireProjectByRoot(context, projectRoot);
81
110
  const run = requireRun(context, project.id, input.runId);
82
- const reviewers = reviewerWorks(context, project.id, run.id);
111
+ const reviewers = reviewerWorks(context, project.id, run.id, reviewCycle(run));
83
112
  const findings = canonicalCodeFindings(reviewers);
84
113
  const selected = findings.filter((finding) => input.findingIds.includes(finding.finding_ref));
85
114
  if (selected.length !== new Set(input.findingIds).size)
@@ -99,19 +128,20 @@ export function addVnextCodeReviewRepair(context, input) {
99
128
  const unknown = requestedCheckRefs.filter((ref) => !checks.has(ref));
100
129
  if (unknown.length)
101
130
  throw new AppError("review_check_reference_unknown", "CODE-REVIEW finding cites a check absent from the accepted CODE handoff", 2, { check_refs: unknown });
102
- const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref)).filter((check) => check.run_at !== "external");
103
- if (!reviewChecks.length)
104
- throw new AppError("review_repair_executable_check_required", "CODE-REVIEW repair requires at least one executable causal check; external-only evidence cannot validate a repair Work", 2, { finding_ids: input.findingIds, check_refs: requestedCheckRefs });
105
- return addVnextCodeRepair(context, {
131
+ const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref));
132
+ const repairInput = {
106
133
  projectRoot,
107
134
  runId: run.id,
108
- reviewFindingIds: selected.map((finding) => finding.finding_ref),
109
- reviewEvidenceRefs: selected.flatMap(({ finding }) => finding.evidence_refs),
135
+ reviewFindings: selected.map(({ finding_ref, finding }) => ({ finding_ref, priority: finding.priority, problem: finding.problem, impact: finding.impact, required_outcome: finding.required_outcome, evidence_refs: finding.evidence_refs, obligation_refs: finding.obligation_refs, decision_reason: input.decisionReasonsByFinding?.[finding_ref] ?? input.objective, check_refs: input.checkRefsByFinding[finding_ref] ?? [] })),
110
136
  reviewChecks,
111
- reviewCheckRefs: requestedCheckRefs,
112
137
  originWorkIds: origins,
113
- objective: input.objective
114
- });
138
+ objective: input.objective,
139
+ reviewCycle: reviewCycle(run)
140
+ };
141
+ return { input: repairInput, value: prepareVnextCodeRepair(context, repairInput) };
142
+ }
143
+ export function addVnextCodeReviewRepair(context, input, prepared = prepareVnextCodeReviewRepair(context, input)) {
144
+ return addVnextCodeRepair(context, prepared.input, prepared.value);
115
145
  }
116
146
  export async function finishVnextCodeReview(context, input) {
117
147
  const projectRoot = resolveProjectRoot(input.projectRoot);
@@ -119,6 +149,14 @@ export async function finishVnextCodeReview(context, input) {
119
149
  const run = requireRun(context, project.id, input.runId);
120
150
  const home = requireHome(run);
121
151
  const root = path.join(home, stageDir);
152
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
153
+ const reportPath = path.join(root, "stage-report.json");
154
+ if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
155
+ const report = readJson(reportPath);
156
+ const next = report.semantic?.next_action ?? "code_review_completed";
157
+ resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next);
158
+ return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: next };
159
+ }
122
160
  const reviewContext = readJson(path.join(root, "review-context.json"));
123
161
  const mode = reviewContext.mode;
124
162
  if (!mode)
@@ -128,22 +166,23 @@ export async function finishVnextCodeReview(context, input) {
128
166
  const handoffFailures = validateVnextCodeHandoff(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home });
129
167
  if (handoffFailures.length)
130
168
  throw new AppError("validation", "CODE-REVIEW handoff no longer matches the accepted PLAN", 2, { errors: handoffFailures });
131
- const reportPath = path.join(root, "stage-report.json");
132
- if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
133
- const report = readJson(reportPath);
134
- return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: report.semantic?.next_action ?? "code_review_completed" };
135
- }
136
169
  const rootWork = requireRootWork(context, project.id, run.id);
137
170
  const coordinator = context.db.get("SELECT session_id, result_path FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [rootWork.work_id]);
138
171
  if (!coordinator || path.resolve(coordinator.result_path ?? "") !== path.resolve(reportPath))
139
172
  throw new AppError("code_review_session_binding_conflict", "CODE-REVIEW can finish only after its successful stage start bound the coordinator Session and report path", 1, { run_id: run.id, expected_result_path: reportPath, actual_result_path: coordinator?.result_path ?? null });
140
- const decisionFile = input.decisionFile ?? path.join(root, "decision.json");
141
- if (!fs.existsSync(decisionFile) && mode === "off")
142
- fs.writeFileSync(decisionFile, `${JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] }, null, 2)}\n`);
143
- validateSchema({ schemaName: "code-review-decision", file: decisionFile, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
144
- const decision = readJson(decisionFile);
145
- const reviewers = reviewerWorks(context, project.id, run.id);
173
+ const decisionFile = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
174
+ const disabledDecision = disabledReviewDecision();
175
+ // The implicit review-off decision is the one deliberate no-file path. An
176
+ // explicitly supplied --decision-file is still caller input and must be
177
+ // readable before any lifecycle work can proceed.
178
+ const decision = mode === "off" && !input.decision && !input.decisionFile && !fs.existsSync(decisionFile)
179
+ ? disabledDecision
180
+ : readCodeReviewDecision(decisionFile, reviewContext.accepted_decision, Boolean(input.decisionFile), input.decision);
181
+ validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
182
+ const cycle = reviewContext.cycle ?? "initial";
183
+ const reviewers = reviewerWorks(context, project.id, run.id, cycle);
146
184
  if (mode !== "off") {
185
+ assertCodeReviewCoverage(reviewGroups(home), reviewers);
147
186
  const incomplete = reviewers.filter((work) => work.status !== "completed");
148
187
  if (incomplete.length)
149
188
  throw new AppError("reviewer_jobs_incomplete", "CODE-REVIEW cannot finish while reviewer Work is unsettled", 2, { works: incomplete.map((work) => ({ work_id: work.work_id, status: work.status })) });
@@ -151,30 +190,35 @@ export async function finishVnextCodeReview(context, input) {
151
190
  validateReviewerResult(context, work, run);
152
191
  const canonical = reviewDecisionForRepair(decision, reviewers);
153
192
  const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
154
- let repairs = reviewRepairWorks(context, project.id, run.id, root);
155
- const decisionSha = sha256File(decisionFile);
193
+ let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
156
194
  const frozenShaFile = path.join(root, ".decision-sha256");
157
- if (fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== decisionSha)
195
+ const decisionHash = sha256Value(input.decisionText ?? JSON.stringify(decision));
196
+ if (!reviewContext.accepted_decision && fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== decisionHash)
158
197
  throw new AppError("review_decision_changed", "The accepted CODE-REVIEW decision cannot change during repair", 2, { decision_file: decisionFile });
159
198
  const fixIds = canonical.fix_ids;
160
199
  if (fixIds.length && repairs.length === 0) {
161
- const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
162
- fs.writeFileSync(frozenShaFile, `${decisionSha}\n`);
163
- return { ok: true, run_id: run.id, stage, outcome: "repair_required", decision_sha256: decisionSha, repair, instruction: "Run the returned repair Work in one fresh child session. When it completes, invoke the same stage finish command again with the unchanged decision file.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
200
+ // Accept the value once. Repairs consume this decision, not a mutable
201
+ // operator file. Reuse the existing context artifact and review cycle.
202
+ const contextFile = path.join(root, "review-context.json"), temporary = `${contextFile}.tmp-${crypto.randomUUID()}`;
203
+ fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
204
+ fs.renameSync(temporary, contextFile);
205
+ const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, decisionReasonsByFinding: Object.fromEntries(canonical.decision.findings.map((item) => [item.finding_ref, item.reason])), objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
206
+ return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, instruction: "The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. When it completes, invoke next.finish_command; this review cycle uses the decision already accepted by the runtime.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
164
207
  }
165
- repairs = reviewRepairWorks(context, project.id, run.id, root);
208
+ repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
166
209
  const incompleteRepairs = repairs.filter((work) => work.status !== "completed");
167
210
  if (incompleteRepairs.length)
168
211
  throw new AppError("review_repair_incomplete", "CODE-REVIEW cannot finish while a repair Work is unsettled", 2, { works: incompleteRepairs.map((work) => ({ work_id: work.work_id, status: work.status })) });
169
212
  validateDecision(context, { decision: canonical.decision, reviewers: canonical.reviewers, repairs, workspaceRoot: run.workspace_root });
170
213
  }
171
214
  const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
172
- const repairs = reviewRepairWorks(context, project.id, run.id, root);
215
+ const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
173
216
  const finalChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }));
174
- const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: finalChecks });
175
- if (unchangedFailures.length)
176
- throw new AppError("code_review_gate_repair_required", "CODE-REVIEW final gate already failed for the unchanged workspace; repair the evidenced failure before retrying", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures });
177
- const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.progress ? { progress: input.progress } : {}) });
217
+ if (input.retryCheckId) {
218
+ validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: finalChecks, reason: input.retryReason });
219
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
220
+ }
221
+ const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}), ...(input.progress ? { progress: input.progress } : {}) });
178
222
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
179
223
  if (failed.length)
180
224
  throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
@@ -182,30 +226,65 @@ export async function finishVnextCodeReview(context, input) {
182
226
  outcome: "repair_required",
183
227
  retry_after_workspace_change: true,
184
228
  workspace_fingerprint: failed[0].workspace_fingerprint,
185
- repair_command: `${flowCommand(context)} work repair add --run ${run.id} --from-check ${failed[0].id} --origin-work <WORK-ID> --task-stdin --project-root ${JSON.stringify(projectRoot)} --json`
229
+ continuation: registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks: finalChecks, generation }),
230
+ instruction: "Repair Work is already registered. End this Turn so the controller can launch a fresh child; after it finishes, repeat finish with the already accepted review decision. Do not redo review."
186
231
  });
187
232
  const stopTarget = executionStopTarget(run);
188
233
  const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
189
234
  const now = context.now();
190
235
  const startedAt = stageStartedAt(run, now);
191
- const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result: `Independent CODE review completed: ${reviewers.length} reviewer Work(s), ${repairs.length} repair Work(s).`, acceptance: ["all_selected_review_groups_completed", "all_material_findings_classified", ...(repairs.length ? ["all_accepted_repairs_completed"] : []), "current_final_gate_passed"], run_changed_files: changedPaths(run.workspace_root), checks: receipts.map((receipt) => receipt.command), evidence: [...reviewers.flatMap((work) => readJsonString(work.result).findings.flatMap((finding) => finding.evidence_refs)), ...receipts.map((receipt) => runRef(run.id, home, receipt.receipt_path))], next_action: nextAction, code_review: { mode, reviewer_work_ids: reviewers.map((work) => work.work_id), repair_work_ids: repairs.map((work) => work.work_id), decision } }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json` }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } };
236
+ const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result: `Independent CODE review completed: ${reviewers.length} reviewer Work(s), ${repairs.length} repair Work(s).`, acceptance: ["all_selected_review_groups_completed", "all_material_findings_classified", ...(repairs.length ? ["all_accepted_repairs_completed"] : []), "current_final_gate_passed"], run_changed_files: changedPaths(run.workspace_root), checks: receipts.map((receipt) => receipt.command), evidence: [...reviewers.flatMap((work) => readJsonString(work.result).findings.flatMap((finding) => finding.evidence_refs)), ...receipts.map((receipt) => runRef(run.id, home, receipt.receipt_path))], next_action: nextAction, code_review: { mode, reviewer_work_ids: reviewers.map((work) => work.work_id), repair_work_ids: repairs.map((work) => work.work_id), decision } }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command: runtimeCommand(context, ["stat", "run", "sessions", "ls", "--run", run.id, "--project-root", projectRoot, "--json"]), usage_stats_command: runtimeCommand(context, ["stat", "usage", "--run", run.id, "--project-root", projectRoot, "--json"]) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } };
192
237
  // The request must exist before CODE-REVIEW becomes terminal. Otherwise a
193
238
  // merge-gate error creates an irrecoverable `done` report with no MRG-*.
194
239
  const acceptedPaths = changedPaths(run.workspace_root);
195
- const mergeRequest = nextAction === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
196
- writeReport(root, report);
197
- 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" });
198
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
199
- if (nextAction === "start_merge")
200
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
201
- else {
202
- await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
203
- completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
240
+ let mergeRequest = null;
241
+ assertRunMutationAllowed(context, project.id, run.id, generation);
242
+ const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
243
+ withStageSettlement(context, () => {
244
+ assertRunMutationAllowed(context, project.id, run.id, generation);
245
+ const currentReviewers = reviewerWorks(context, project.id, run.id, cycle);
246
+ const currentRepairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
247
+ if (currentReviewers.length !== reviewers.length || currentRepairs.length !== repairs.length || [...currentReviewers, ...currentRepairs].some(work => work.status !== "completed" || ![...reviewers, ...repairs].some(accepted => accepted.work_id === work.work_id && accepted.result === work.result))) {
248
+ throw new AppError("reviewer_jobs_incomplete", "Review or repair results changed while the final checks were running", 2);
249
+ }
250
+ mergeRequest = prepareMerge?.() ?? null;
251
+ writeReport(root, report);
252
+ 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" });
253
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
254
+ if (nextAction === "start_merge")
255
+ advanceFlowRun(context, { settlementStage: "code-review", projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
256
+ else {
257
+ finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
258
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
259
+ }
260
+ refreshRunWorkProjection(context, project.id, run.id);
261
+ });
262
+ return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } } : {}) };
263
+ }
264
+ export function readCodeReviewDecision(file, accepted, explicit = false, prepared) {
265
+ if (!accepted)
266
+ return prepared ?? prepareJsonFile(file, "CODE-REVIEW decision").value;
267
+ if ((prepared !== undefined || (explicit && fs.existsSync(file))) && JSON.stringify(prepared ?? prepareJsonFile(file, "CODE-REVIEW decision").value) !== JSON.stringify(accepted)) {
268
+ throw new AppError("review_decision_changed", "This review cycle already accepted a different decision", 2, { decision_file: file });
204
269
  }
205
- refreshRunWorkProjection(context, project.id, run.id);
206
- return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } } : {}) };
270
+ return accepted;
207
271
  }
208
- function reviewGroups(home) {
272
+ function disabledReviewDecision() {
273
+ return { schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] };
274
+ }
275
+ /** Retained decisions remain authoritative after their caller file is gone. */
276
+ export function prepareCodeReviewDecision(runRoot, file) {
277
+ const root = path.join(runRoot, "06-code-review");
278
+ const context = readJson(path.join(root, "review-context.json"));
279
+ const source = path.resolve(file ?? path.join(root, "decision.json"));
280
+ if (context.accepted_decision && !fs.existsSync(source))
281
+ return { value: context.accepted_decision };
282
+ if (context.mode === "off" && !file && !fs.existsSync(source))
283
+ return { value: disabledReviewDecision() };
284
+ const prepared = prepareJsonFile(source, "CODE-REVIEW decision");
285
+ return { value: readCodeReviewDecision(source, context.accepted_decision, Boolean(file), prepared.value), text: prepared.text };
286
+ }
287
+ export function reviewGroups(home) {
209
288
  const expected = new Set(baselineAspects);
210
289
  const declared = [];
211
290
  for (const file of findFiles(path.join(home, "03-plan"), "aspect-map.json")) {
@@ -220,17 +299,35 @@ function reviewGroups(home) {
220
299
  if (!declared.length)
221
300
  return [{ key: "baseline", aspect_ids: [...expected].sort() }];
222
301
  const assigned = new Set();
302
+ const scoped = new Set();
223
303
  for (const group of declared)
224
304
  for (const aspectId of group.aspect_ids) {
225
- if (assigned.has(aspectId))
226
- throw new AppError("code_review_groups_invalid", "A CODE-REVIEW aspect appears in more than one accepted group", 2, { aspect_id: aspectId });
305
+ const assignment = `${group.key.slice(0, group.key.lastIndexOf("/"))}/${aspectId}`;
306
+ if (scoped.has(assignment))
307
+ throw new AppError("code_review_groups_invalid", "A CODE-REVIEW aspect appears in more than one group of the same protocol", 2, { aspect_id: aspectId });
308
+ scoped.add(assignment);
227
309
  assigned.add(aspectId);
228
310
  }
229
311
  const missing = [...expected].filter((aspectId) => !assigned.has(aspectId)).sort();
230
312
  return [...declared, ...(missing.length ? [{ key: "baseline", aspect_ids: missing }] : [])];
231
313
  }
232
- export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
233
- function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Each reviewer Work must run in one fresh child session. The coordinator must never claim a reviewer Work itself. Start each ready Work with its exact start_command from the work graph; it receives the task and result schema. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Finish only after every reviewer Work settles and you have written ${decision}: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Run it, then call the same Finish command again. Review is not repeated.", "If the post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then stop so the runner can dispatch the new repair. Do not edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
314
+ export function assertCodeReviewCoverage(groups, reviewers) {
315
+ const expected = new Map(groups.map(group => [group.key, [...group.aspect_ids].sort()]));
316
+ if (!expected.size || expected.size !== groups.length)
317
+ throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW expected group identities must be nonempty and unique", 2);
318
+ const seen = new Set();
319
+ for (const work of reviewers) {
320
+ const group = readJsonString(work.payload_json).group;
321
+ if (!group?.key || seen.has(group.key) || !expected.has(group.key) || !Array.isArray(group.aspect_ids) || JSON.stringify([...group.aspect_ids].sort()) !== JSON.stringify(expected.get(group.key)))
322
+ throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW Work does not match its expected group", 2, { work_id: work.work_id });
323
+ seen.add(group.key);
324
+ }
325
+ const missing = [...expected.keys()].filter(key => !seen.has(key));
326
+ if (missing.length || !expected.size)
327
+ throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
328
+ }
329
+ export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Verify that universal and exclusive constraints (only, any, all, never, whole) and stated exceptions were not narrowed. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Evidence closes a claim only when it exercises the named failure mechanism; a sequential negative test does not prove a concurrency race, and proof limits cannot waive an accepted obligation. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
330
+ function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", "Ready reviewer Works: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]), `Write the decision to this exact absolute path, not a relative look-alike: ${decision}`, `Finish only after every reviewer Work settles and that exact file exists: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Do not invoke that Work's start_command from the coordinator Session: end this Turn so the controller can launch a fresh child, then call the returned next.finish_command after it completes. Internal invocation IDs are runtime-owned and must not be copied or reused. Review is not repeated.", "If the aggregate gate fails with repair_required, its repair Work is already registered with all causal checks. End this Turn so the controller launches a fresh child. Do not copy receipt IDs, create another repair, invoke its start_command in the coordinator, or edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
234
331
  function canonicalReviewEvidence(decision, reviewers) {
235
332
  const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
236
333
  const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
@@ -279,8 +376,11 @@ function validateDecision(context, input) {
279
376
  const repairedBy = new Map();
280
377
  for (const work of input.repairs) {
281
378
  const result = readJsonString(work.result);
282
- for (const ref of Array.isArray(result.resolved_finding_refs) ? result.resolved_finding_refs.filter((value) => typeof value === "string") : [])
283
- repairedBy.set(ref, [...(repairedBy.get(ref) ?? []), work]);
379
+ for (const resolution of Array.isArray(result.resolutions) ? result.resolutions : []) {
380
+ const ref = typeof resolution === "object" && resolution !== null && typeof resolution.finding_ref === "string" ? resolution.finding_ref : null;
381
+ if (ref)
382
+ repairedBy.set(ref, [...(repairedBy.get(ref) ?? []), work]);
383
+ }
284
384
  }
285
385
  for (const { finding_ref: findingRef, finding } of findings) {
286
386
  const item = decisions.get(findingRef);
@@ -319,7 +419,7 @@ function validateReviewerResult(_context, work, _run) {
319
419
  if (result.aspects.some((aspect) => aspect.verdict === "blocked"))
320
420
  throw new AppError("review_blocked", "A blocked review aspect must be resolved before CODE-REVIEW can finish", 2, { work_id: work.work_id });
321
421
  }
322
- function reviewerWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const p = readJsonString(work.payload_json); return Boolean(p && p.kind === "code-review"); }); }
422
+ function reviewerWorks(context, projectId, runId, cycle) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const payload = readJsonString(work.payload_json); return Boolean(payload.kind === "code-review" && (typeof payload.review_cycle === "string" ? payload.review_cycle : "initial") === cycle); }); }
323
423
  function codeWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).filter((work) => readJsonString(work.payload_json).schema_id === "dd-flow/code-work-packet@5" && !reviewFindingIds(work).length); }
324
424
  function acceptedCodeChecks(context, projectId, runId) { return codeWorks(context, projectId, runId).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }); }
325
425
  function validateRepairChecks(context, input) { const known = new Set(acceptedCodeChecks(context, input.projectId, input.runId).map((check) => check.id)); const selected = {}; for (const item of input.decision.findings.filter((item) => item.disposition === "fix")) {
@@ -331,7 +431,7 @@ function validateRepairChecks(context, input) { const known = new Set(acceptedCo
331
431
  throw new AppError("review_check_reference_unknown", "CODE-REVIEW repair selects a check absent from the accepted CODE handoff", 2, { finding_ref: item.finding_ref, check_refs: unknown });
332
432
  selected[item.finding_ref] = refs;
333
433
  } return selected; }
334
- function reviewRepairWorks(context, projectId, runId, root) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => isCodeReviewStageRepair(work, root)); }
434
+ function reviewRepairWorks(context, projectId, runId, root, cycle) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const payload = readJsonString(work.payload_json); return isCodeReviewStageRepair(work, root) && (typeof payload.review_cycle === "string" ? payload.review_cycle : "initial") === cycle; }); }
335
435
  export function isCodeReviewStageRepair(work, root) {
336
436
  if (reviewFindingIds(work).length)
337
437
  return true;
@@ -345,26 +445,31 @@ export function isCodeReviewStageRepair(work, root) {
345
445
  return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
346
446
  }
347
447
  function reviewFindingIds(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
348
- return []; const ids = repair.review_finding_ids; return Array.isArray(ids) ? ids.filter((value) => typeof value === "string") : []; }
448
+ return []; const findings = repair.review_findings; return Array.isArray(findings) ? findings.flatMap((value) => typeof value === "object" && value !== null && typeof value.finding_ref === "string" ? [value.finding_ref] : []) : []; }
349
449
  function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
350
- return []; const refs = repair.review_check_refs; return Array.isArray(refs) ? refs.filter((value) => typeof value === "string") : []; }
450
+ return []; const findings = repair.review_findings; return Array.isArray(findings) ? [...new Set(findings.flatMap((value) => typeof value === "object" && value !== null && Array.isArray(value.check_refs) ? value.check_refs.filter((ref) => typeof ref === "string") : []))] : []; }
351
451
  function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
352
- function effectiveMode(home, run) { const index = JSON.parse(run.index_json); const configured = index.settings?.code_review; const requested = configured?.mode ?? "auto"; if (requested !== "auto")
452
+ function effectiveMode(home, run) { const index = JSON.parse(run.index_json); if (index.variables?.["merge.source_repair"])
453
+ return { mode: "standard", source: "project_policy", reason: "A failed integration gate requires independent review of its source repair." }; const configured = index.settings?.code_review; const requested = configured?.mode ?? "auto"; if (requested !== "auto")
353
454
  return { mode: requested, source: configured?.source === "user_instruction" ? "user_instruction" : "project_policy", reason: configured?.reason ?? "Frozen RUN configuration." }; const assessments = findFiles(path.join(home, "03-plan"), "plan.json").map((file) => readJson(file)); const deep = assessments.some((plan) => plan.assessment?.failure_impact?.level === "high" || plan.assessment?.solution_uncertainty?.level === "high"); return { mode: deep ? "deep" : "standard", source: "plan_assessment", reason: deep ? "Accepted PLAN assessment records high impact or uncertainty." : "Accepted PLAN assessment requires the standard independent review." }; }
354
455
  function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
355
- function changedPaths(workspaceRoot) { try {
356
- return execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: workspaceRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split(/\r?\n/).filter(Boolean).map((line) => line.slice(3).replace(/^.* -> /, ""));
357
- }
358
- catch {
359
- return [];
360
- } }
456
+ function changedPaths(workspaceRoot) { return workspaceChangedPaths(workspaceRoot); }
361
457
  function sha256File(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
458
+ function sha256Value(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
362
459
  function stageStatus(run, name) { try {
363
460
  return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === name)?.status ?? null;
364
461
  }
365
462
  catch {
366
463
  return null;
367
464
  } }
465
+ function reviewCycle(run) { try {
466
+ const value = JSON.parse(run.index_json);
467
+ const repair = value.variables?.["merge.source_repair"];
468
+ return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.cycle === "string" ? String(repair.cycle) : "initial";
469
+ }
470
+ catch {
471
+ return "initial";
472
+ } }
368
473
  function stageStartedAt(run, fallback) { try {
369
474
  return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === stage)?.started_at ?? fallback;
370
475
  }
@@ -378,7 +483,9 @@ function requireRun(context, projectId, runId) { const run = context.db.get("SEL
378
483
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
379
484
  function requireHome(run) { if (!run.run_root)
380
485
  throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
381
- function finishCommand(context, runId, projectRoot, decision) { return `${flowCommand(context)} stage finish ${runId} --stage code-review --decision-file ${JSON.stringify(decision)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`; }
486
+ export function finishCommand(context, runId, projectRoot, decision, retryCheckId) {
487
+ return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "code-review", "--decision-file", decision, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
488
+ }
382
489
  function findFiles(root, name) { if (!fs.existsSync(root))
383
490
  return []; const out = []; for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
384
491
  const file = path.join(root, entry.name);