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

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 (123) hide show
  1. package/CHANGELOG.md +404 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/help.js +81 -9
  5. package/dist/cli/run-cli.js +442 -46
  6. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  7. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  8. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  9. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  10. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  11. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  12. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  13. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  14. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  15. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
  16. package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
  17. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  18. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  19. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  20. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  21. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  22. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  23. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  24. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  25. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  26. package/dist/harness-runtime/lib/delegation-instructions.mjs +121 -0
  27. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  28. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  29. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  30. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  31. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  32. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  33. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  34. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  35. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  36. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  37. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  38. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  39. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  40. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  41. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  42. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  43. package/dist/runtime/context.js +3 -1
  44. package/dist/schemas/agent-profile.schema.json +1 -1
  45. package/dist/schemas/code-review-result.schema.json +2 -2
  46. package/dist/schemas/code-work-batch.schema.json +4 -3
  47. package/dist/schemas/code-work-result.schema.json +4 -4
  48. package/dist/schemas/harness-config.schema.json +23 -0
  49. package/dist/schemas/plan-review-result.schema.json +2 -2
  50. package/dist/schemas/run-control-receipt.schema.json +99 -0
  51. package/dist/schemas/run-control-request.schema.json +36 -0
  52. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  53. package/dist/services/cleanup.js +8 -5
  54. package/dist/services/cli-operation-classifier.js +11 -1
  55. package/dist/services/code-checks.js +543 -96
  56. package/dist/services/controller-fanout.js +116 -0
  57. package/dist/services/dashboard.js +9 -0
  58. package/dist/services/engines.js +50 -19
  59. package/dist/services/eval-snapshots.js +639 -60
  60. package/dist/services/execution-policy.js +177 -0
  61. package/dist/services/external-work-launch.js +110 -0
  62. package/dist/services/harness-adapter.js +147 -24
  63. package/dist/services/harness-config.js +68 -0
  64. package/dist/services/hooks.js +413 -168
  65. package/dist/services/lanes.js +1 -1
  66. package/dist/services/lifecycle-command.js +74 -6
  67. package/dist/services/lifecycle-invocations.js +637 -0
  68. package/dist/services/managed-daemon-binding.js +38 -0
  69. package/dist/services/managed-processes.js +138 -22
  70. package/dist/services/merge-queue.js +6 -6
  71. package/dist/services/merge-server.js +75 -29
  72. package/dist/services/migrations.js +1 -1
  73. package/dist/services/native-daemon-history.js +49 -0
  74. package/dist/services/native-session-control.js +39 -0
  75. package/dist/services/projects.js +7 -3
  76. package/dist/services/prompts.js +4 -2
  77. package/dist/services/protocols.js +11 -2
  78. package/dist/services/recovery-observation-budget.js +61 -0
  79. package/dist/services/recovery-snapshot-database.js +107 -0
  80. package/dist/services/run-control-receipt.js +56 -0
  81. package/dist/services/run-control-worker.js +363 -0
  82. package/dist/services/run-control.js +876 -0
  83. package/dist/services/run-controller-adapter.js +211 -0
  84. package/dist/services/run-controller-capture.js +134 -0
  85. package/dist/services/run-controller-process.js +189 -0
  86. package/dist/services/run-controller-recovery.js +222 -0
  87. package/dist/services/run-controller-state.js +31 -0
  88. package/dist/services/run-controller.js +733 -0
  89. package/dist/services/run-engine-bindings.js +20 -62
  90. package/dist/services/run-fork.js +124 -0
  91. package/dist/services/run-observations.js +112 -0
  92. package/dist/services/run-recovery-runtime.js +69 -0
  93. package/dist/services/run-recovery.js +327 -0
  94. package/dist/services/runs.js +280 -40
  95. package/dist/services/runtime-budget.js +362 -0
  96. package/dist/services/runtime-scope-capture.js +50 -0
  97. package/dist/services/runtime-scope-control.js +450 -0
  98. package/dist/services/runtime-scope-resume.js +543 -0
  99. package/dist/services/runtime-scope-stop.js +99 -0
  100. package/dist/services/runtime-scope-worker.js +210 -0
  101. package/dist/services/runtime-service.js +96 -0
  102. package/dist/services/schema-validation.js +12 -12
  103. package/dist/services/stage-context.js +2 -2
  104. package/dist/services/stage-lifecycle.js +32 -10
  105. package/dist/services/stage-pause.js +25 -13
  106. package/dist/services/usage.js +37 -93
  107. package/dist/services/vnext-code-review.js +149 -89
  108. package/dist/services/vnext-code.js +224 -216
  109. package/dist/services/vnext-execution-profile.js +5 -3
  110. package/dist/services/vnext-fanout.js +135 -21
  111. package/dist/services/vnext-merge.js +258 -89
  112. package/dist/services/vnext-plan-review.js +75 -64
  113. package/dist/services/vnext-plan.js +77 -29
  114. package/dist/services/vnext-protocolize.js +21 -14
  115. package/dist/services/vnext-specify.js +26 -16
  116. package/dist/services/work-registry.js +605 -157
  117. package/dist/services/workspace-bootstrap.js +76 -0
  118. package/dist/shared/errors.js +12 -0
  119. package/dist/storage/database.js +341 -31
  120. package/dist/storage/writer-contract.js +77 -0
  121. package/dist/storage/writer-migration.js +102 -0
  122. package/package.json +20 -13
  123. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,24 +1,26 @@
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
- import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
8
+ import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
9
9
  import { validateSchema } from "./schema-validation.js";
10
10
  import { flowCommand } from "./stage-pause.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, resumeAcceptedCodeStage } from "./vnext-code.js";
14
+ import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
15
+ import { addWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
16
+ import { withStageSettlement } from "./runs.js";
16
17
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
17
18
  import { writeStageReport } from "./stage-report-renderer.js";
18
19
  import { applyExternalStageContext } from "./stage-context.js";
19
20
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
20
- import { ensureVnextMergeRequest } from "./vnext-merge.js";
21
+ import { prepareVnextMergeRequest } from "./vnext-merge.js";
21
22
  import { validateVnextCodeHandoff } from "./vnext-plan.js";
23
+ import { assertRunMutationAllowed } from "./run-recovery.js";
22
24
  const stage = "code-review";
23
25
  const stageDir = vnextStageDirectory(stage);
24
26
  const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
@@ -47,38 +49,40 @@ export function startVnextCodeReview(context, input) {
47
49
  const orchestration = readFanoutDescriptor(root);
48
50
  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")) } };
49
51
  }
50
- const mode = effectiveMode(run, acceptedCodeChangedPaths(home));
52
+ const mode = effectiveMode(home, run);
53
+ const cycle = reviewCycle(run);
54
+ freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
51
55
  const promptPath = path.join(root, "stage-prompt.md");
52
56
  const reviewContextPath = path.join(root, "review-context.json");
53
- fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", mode, workspace_fingerprint: workspaceFingerprint(run.workspace_root), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
54
- if (mode === "off") {
57
+ 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: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
58
+ if (mode.mode === "off") {
55
59
  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`;
56
60
  fs.writeFileSync(promptPath, prompt);
57
61
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
58
62
  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 } : {}) });
59
63
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
60
- return { ok: true, run_id: run.id, stage, mode, 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")) } };
64
+ 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")) } };
61
65
  }
62
- const groups = reviewGroups(run, home, mode);
63
- 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 } })) };
66
+ const groups = reviewGroups(home);
67
+ 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 } })) };
64
68
  const batchFile = path.join(root, "review-work-batch.json");
65
69
  fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
66
- const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode, groups });
70
+ const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups });
67
71
  fs.writeFileSync(promptPath, prompt);
68
72
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
69
73
  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 } : {}) });
70
74
  const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, file: batchFile });
71
75
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
72
76
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
73
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode, groups, registered });
74
- return { ok: true, run_id: run.id, stage, mode, 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")) } };
77
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
78
+ 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")) } };
75
79
  }
76
80
  /** Create a narrow CODE repair from accepted independent-review evidence. */
77
81
  export function addVnextCodeReviewRepair(context, input) {
78
82
  const projectRoot = resolveProjectRoot(input.projectRoot);
79
83
  const project = requireProjectByRoot(context, projectRoot);
80
84
  const run = requireRun(context, project.id, input.runId);
81
- const reviewers = reviewerWorks(context, project.id, run.id);
85
+ const reviewers = reviewerWorks(context, project.id, run.id, reviewCycle(run));
82
86
  const findings = canonicalCodeFindings(reviewers);
83
87
  const selected = findings.filter((finding) => input.findingIds.includes(finding.finding_ref));
84
88
  if (selected.length !== new Set(input.findingIds).size)
@@ -98,18 +102,15 @@ export function addVnextCodeReviewRepair(context, input) {
98
102
  const unknown = requestedCheckRefs.filter((ref) => !checks.has(ref));
99
103
  if (unknown.length)
100
104
  throw new AppError("review_check_reference_unknown", "CODE-REVIEW finding cites a check absent from the accepted CODE handoff", 2, { check_refs: unknown });
101
- const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref)).filter((check) => check.run_at !== "external");
102
- if (!reviewChecks.length)
103
- 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
+ const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref));
104
106
  return addVnextCodeRepair(context, {
105
107
  projectRoot,
106
108
  runId: run.id,
107
- reviewFindingIds: selected.map((finding) => finding.finding_ref),
108
- reviewEvidenceRefs: selected.flatMap(({ finding }) => finding.evidence_refs),
109
+ 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] ?? [] })),
109
110
  reviewChecks,
110
- reviewCheckRefs: requestedCheckRefs,
111
111
  originWorkIds: origins,
112
- objective: input.objective
112
+ objective: input.objective,
113
+ reviewCycle: reviewCycle(run)
113
114
  });
114
115
  }
115
116
  export async function finishVnextCodeReview(context, input) {
@@ -118,6 +119,14 @@ export async function finishVnextCodeReview(context, input) {
118
119
  const run = requireRun(context, project.id, input.runId);
119
120
  const home = requireHome(run);
120
121
  const root = path.join(home, stageDir);
122
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
123
+ const reportPath = path.join(root, "stage-report.json");
124
+ if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
125
+ const report = readJson(reportPath);
126
+ const next = report.semantic?.next_action ?? "code_review_completed";
127
+ resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next);
128
+ return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: next };
129
+ }
121
130
  const reviewContext = readJson(path.join(root, "review-context.json"));
122
131
  const mode = reviewContext.mode;
123
132
  if (!mode)
@@ -127,11 +136,6 @@ export async function finishVnextCodeReview(context, input) {
127
136
  const handoffFailures = validateVnextCodeHandoff(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home });
128
137
  if (handoffFailures.length)
129
138
  throw new AppError("validation", "CODE-REVIEW handoff no longer matches the accepted PLAN", 2, { errors: handoffFailures });
130
- const reportPath = path.join(root, "stage-report.json");
131
- if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
132
- const report = readJson(reportPath);
133
- return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: report.semantic?.next_action ?? "code_review_completed" };
134
- }
135
139
  const rootWork = requireRootWork(context, project.id, run.id);
136
140
  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]);
137
141
  if (!coordinator || path.resolve(coordinator.result_path ?? "") !== path.resolve(reportPath))
@@ -139,10 +143,12 @@ export async function finishVnextCodeReview(context, input) {
139
143
  const decisionFile = input.decisionFile ?? path.join(root, "decision.json");
140
144
  if (!fs.existsSync(decisionFile) && mode === "off")
141
145
  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`);
142
- validateSchema({ schemaName: "code-review-decision", file: decisionFile, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id });
143
- const decision = readJson(decisionFile);
144
- const reviewers = reviewerWorks(context, project.id, run.id);
146
+ const decision = readCodeReviewDecision(decisionFile, reviewContext.accepted_decision, Boolean(input.decisionFile));
147
+ validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
148
+ const cycle = reviewContext.cycle ?? "initial";
149
+ const reviewers = reviewerWorks(context, project.id, run.id, cycle);
145
150
  if (mode !== "off") {
151
+ assertCodeReviewCoverage(reviewGroups(home), reviewers);
146
152
  const incomplete = reviewers.filter((work) => work.status !== "completed");
147
153
  if (incomplete.length)
148
154
  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 })) });
@@ -150,30 +156,34 @@ export async function finishVnextCodeReview(context, input) {
150
156
  validateReviewerResult(context, work, run);
151
157
  const canonical = reviewDecisionForRepair(decision, reviewers);
152
158
  const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
153
- let repairs = reviewRepairWorks(context, project.id, run.id, root);
154
- const decisionSha = sha256File(decisionFile);
159
+ let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
155
160
  const frozenShaFile = path.join(root, ".decision-sha256");
156
- if (fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== decisionSha)
161
+ if (!reviewContext.accepted_decision && fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== sha256File(decisionFile))
157
162
  throw new AppError("review_decision_changed", "The accepted CODE-REVIEW decision cannot change during repair", 2, { decision_file: decisionFile });
158
163
  const fixIds = canonical.fix_ids;
159
164
  if (fixIds.length && repairs.length === 0) {
160
- const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
161
- fs.writeFileSync(frozenShaFile, `${decisionSha}\n`);
162
- 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) } };
165
+ // Accept the value once. Repairs consume this decision, not a mutable
166
+ // operator file. Reuse the existing context artifact and review cycle.
167
+ const contextFile = path.join(root, "review-context.json"), temporary = `${contextFile}.tmp-${crypto.randomUUID()}`;
168
+ fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
169
+ fs.renameSync(temporary, contextFile);
170
+ 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(", ")}` });
171
+ return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, instruction: "Run the returned repair Work in one 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) } };
163
172
  }
164
- repairs = reviewRepairWorks(context, project.id, run.id, root);
173
+ repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
165
174
  const incompleteRepairs = repairs.filter((work) => work.status !== "completed");
166
175
  if (incompleteRepairs.length)
167
176
  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 })) });
168
177
  validateDecision(context, { decision: canonical.decision, reviewers: canonical.reviewers, repairs, workspaceRoot: run.workspace_root });
169
178
  }
170
179
  const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
171
- const repairs = reviewRepairWorks(context, project.id, run.id, root);
180
+ const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
172
181
  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 : []; }));
173
- const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: finalChecks });
174
- if (unchangedFailures.length)
175
- 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 });
176
- 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 } : {}) });
182
+ if (input.retryCheckId) {
183
+ validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: finalChecks, reason: input.retryReason });
184
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
185
+ }
186
+ 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 } : {}) });
177
187
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
178
188
  if (failed.length)
179
189
  throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
@@ -181,7 +191,8 @@ export async function finishVnextCodeReview(context, input) {
181
191
  outcome: "repair_required",
182
192
  retry_after_workspace_change: true,
183
193
  workspace_fingerprint: failed[0].workspace_fingerprint,
184
- 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`
194
+ 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`,
195
+ retry_command: finishCommand(context, run.id, projectRoot, decisionFile, failed[0].id)
185
196
  });
186
197
  const stopTarget = executionStopTarget(run);
187
198
  const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
@@ -191,34 +202,82 @@ export async function finishVnextCodeReview(context, input) {
191
202
  // The request must exist before CODE-REVIEW becomes terminal. Otherwise a
192
203
  // merge-gate error creates an irrecoverable `done` report with no MRG-*.
193
204
  const acceptedPaths = changedPaths(run.workspace_root);
194
- const mergeRequest = nextAction === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
195
- writeReport(root, report);
196
- 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" });
197
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
198
- if (nextAction === "start_merge")
199
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
200
- else {
201
- await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
202
- completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
205
+ let mergeRequest = null;
206
+ assertRunMutationAllowed(context, project.id, run.id, generation);
207
+ const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
208
+ withStageSettlement(context, () => {
209
+ assertRunMutationAllowed(context, project.id, run.id, generation);
210
+ const currentReviewers = reviewerWorks(context, project.id, run.id, cycle);
211
+ const currentRepairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
212
+ 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))) {
213
+ throw new AppError("reviewer_jobs_incomplete", "Review or repair results changed while the final checks were running", 2);
214
+ }
215
+ mergeRequest = prepareMerge?.() ?? null;
216
+ writeReport(root, report);
217
+ 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" });
218
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
219
+ if (nextAction === "start_merge")
220
+ advanceFlowRun(context, { settlementStage: "code-review", projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
221
+ else {
222
+ finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
223
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
224
+ }
225
+ refreshRunWorkProjection(context, project.id, run.id);
226
+ });
227
+ 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, `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`) } } : {}) };
228
+ }
229
+ export function readCodeReviewDecision(file, accepted, explicit = false) {
230
+ if (!accepted)
231
+ return readJson(file);
232
+ if (explicit && fs.existsSync(file) && JSON.stringify(readJson(file)) !== JSON.stringify(accepted)) {
233
+ throw new AppError("review_decision_changed", "This review cycle already accepted a different decision", 2, { decision_file: file });
203
234
  }
204
- refreshRunWorkProjection(context, project.id, run.id);
205
- 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` } } : {}) };
235
+ return accepted;
206
236
  }
207
- function reviewGroups(run, home, mode) {
208
- const aspectMapFiles = findFiles(path.join(home, "03-plan"), "aspect-map.json");
209
- const ids = new Set(baselineAspects);
210
- for (const file of aspectMapFiles) {
237
+ export function reviewGroups(home) {
238
+ const expected = new Set(baselineAspects);
239
+ const declared = [];
240
+ for (const file of findFiles(path.join(home, "03-plan"), "aspect-map.json")) {
211
241
  const map = readJson(file);
212
242
  for (const aspect of map.aspects ?? [])
213
243
  if (aspect.applicability === "applicable" && aspect.aspect_id)
214
- ids.add(aspect.aspect_id);
244
+ expected.add(aspect.aspect_id);
245
+ for (const group of map.review_groups ?? [])
246
+ if (group.id && group.aspect_ids?.length)
247
+ declared.push({ key: `${map.protocol_id ?? path.basename(path.dirname(file))}/${group.id}`, aspect_ids: [...new Set(group.aspect_ids)] });
215
248
  }
216
- const all = [...ids];
217
- const chunk = mode === "deep" ? 1 : 3;
218
- return Array.from({ length: Math.ceil(all.length / chunk) }, (_, index) => ({ key: `group-${index + 1}`, aspect_ids: all.slice(index * chunk, (index + 1) * chunk) }));
249
+ if (!declared.length)
250
+ return [{ key: "baseline", aspect_ids: [...expected].sort() }];
251
+ const assigned = new Set();
252
+ const scoped = new Set();
253
+ for (const group of declared)
254
+ for (const aspectId of group.aspect_ids) {
255
+ const assignment = `${group.key.slice(0, group.key.lastIndexOf("/"))}/${aspectId}`;
256
+ if (scoped.has(assignment))
257
+ 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 });
258
+ scoped.add(assignment);
259
+ assigned.add(aspectId);
260
+ }
261
+ const missing = [...expected].filter((aspectId) => !assigned.has(aspectId)).sort();
262
+ return [...declared, ...(missing.length ? [{ key: "baseline", aspect_ids: missing }] : [])];
263
+ }
264
+ export function assertCodeReviewCoverage(groups, reviewers) {
265
+ const expected = new Map(groups.map(group => [group.key, [...group.aspect_ids].sort()]));
266
+ if (!expected.size || expected.size !== groups.length)
267
+ throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW expected group identities must be nonempty and unique", 2);
268
+ const seen = new Set();
269
+ for (const work of reviewers) {
270
+ const group = readJsonString(work.payload_json).group;
271
+ 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)))
272
+ throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW Work does not match its expected group", 2, { work_id: work.work_id });
273
+ seen.add(group.key);
274
+ }
275
+ const missing = [...expected.keys()].filter(key => !seen.has(key));
276
+ if (missing.length || !expected.size)
277
+ throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
219
278
  }
220
- 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")}. 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.`; }
221
- 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_home_path, "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"); }
279
+ 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.`; }
280
+ 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>", "Do not choose a provider delegation tool from this stage prompt. The shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract at the Work-graph boundary. 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. 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 returned next.finish_command; do not reuse an earlier invocation-id. 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"); }
222
281
  function canonicalReviewEvidence(decision, reviewers) {
223
282
  const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
224
283
  const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
@@ -267,8 +326,11 @@ function validateDecision(context, input) {
267
326
  const repairedBy = new Map();
268
327
  for (const work of input.repairs) {
269
328
  const result = readJsonString(work.result);
270
- for (const ref of Array.isArray(result.resolved_finding_refs) ? result.resolved_finding_refs.filter((value) => typeof value === "string") : [])
271
- repairedBy.set(ref, [...(repairedBy.get(ref) ?? []), work]);
329
+ for (const resolution of Array.isArray(result.resolutions) ? result.resolutions : []) {
330
+ const ref = typeof resolution === "object" && resolution !== null && typeof resolution.finding_ref === "string" ? resolution.finding_ref : null;
331
+ if (ref)
332
+ repairedBy.set(ref, [...(repairedBy.get(ref) ?? []), work]);
333
+ }
272
334
  }
273
335
  for (const { finding_ref: findingRef, finding } of findings) {
274
336
  const item = decisions.get(findingRef);
@@ -307,7 +369,7 @@ function validateReviewerResult(_context, work, _run) {
307
369
  if (result.aspects.some((aspect) => aspect.verdict === "blocked"))
308
370
  throw new AppError("review_blocked", "A blocked review aspect must be resolved before CODE-REVIEW can finish", 2, { work_id: work.work_id });
309
371
  }
310
- 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"); }); }
372
+ 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); }); }
311
373
  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); }
312
374
  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 : []; }); }
313
375
  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")) {
@@ -319,7 +381,7 @@ function validateRepairChecks(context, input) { const known = new Set(acceptedCo
319
381
  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 });
320
382
  selected[item.finding_ref] = refs;
321
383
  } return selected; }
322
- 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)); }
384
+ 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; }); }
323
385
  export function isCodeReviewStageRepair(work, root) {
324
386
  if (reviewFindingIds(work).length)
325
387
  return true;
@@ -333,25 +395,15 @@ export function isCodeReviewStageRepair(work, root) {
333
395
  return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
334
396
  }
335
397
  function reviewFindingIds(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
336
- return []; const ids = repair.review_finding_ids; return Array.isArray(ids) ? ids.filter((value) => typeof value === "string") : []; }
398
+ 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] : []) : []; }
337
399
  function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
338
- return []; const refs = repair.review_check_refs; return Array.isArray(refs) ? refs.filter((value) => typeof value === "string") : []; }
400
+ 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") : []))] : []; }
339
401
  function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
340
- function effectiveMode(run, paths) { const index = JSON.parse(run.index_json); const requested = index.settings?.code_review?.mode ?? "auto"; return requested === "auto" ? (paths.length ? "standard" : "off") : requested; }
341
- function acceptedCodeChangedPaths(home) { try {
342
- const report = readJson(path.join(home, "05-code", "stage-report.json"));
343
- return Array.isArray(report.semantic?.changed_files) ? report.semantic.changed_files.filter((item) => typeof item === "string") : [];
344
- }
345
- catch {
346
- return [];
347
- } }
402
+ function effectiveMode(home, run) { const index = JSON.parse(run.index_json); if (index.variables?.["merge.source_repair"])
403
+ 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")
404
+ 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." }; }
348
405
  function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
349
- function changedPaths(workspaceRoot) { try {
350
- 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(/^.* -> /, ""));
351
- }
352
- catch {
353
- return [];
354
- } }
406
+ function changedPaths(workspaceRoot) { return workspaceChangedPaths(workspaceRoot); }
355
407
  function sha256File(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
356
408
  function stageStatus(run, name) { try {
357
409
  return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === name)?.status ?? null;
@@ -359,6 +411,14 @@ function stageStatus(run, name) { try {
359
411
  catch {
360
412
  return null;
361
413
  } }
414
+ function reviewCycle(run) { try {
415
+ const value = JSON.parse(run.index_json);
416
+ const repair = value.variables?.["merge.source_repair"];
417
+ return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.cycle === "string" ? String(repair.cycle) : "initial";
418
+ }
419
+ catch {
420
+ return "initial";
421
+ } }
362
422
  function stageStartedAt(run, fallback) { try {
363
423
  return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === stage)?.started_at ?? fallback;
364
424
  }
@@ -368,11 +428,11 @@ catch {
368
428
  function runRef(runId, home, file) { const relative = path.relative(home, file).split(path.sep).join("/"); return relative && !relative.startsWith("../") ? `run://${runId}/${relative}` : file; }
369
429
  function requireRootWork(context, projectId, runId) { const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!work || work.status !== "running")
370
430
  throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
371
- function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, workspace_root, run_home_path, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
431
+ function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
372
432
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
373
- function requireHome(run) { if (!run.run_home_path)
374
- throw new AppError("runtime_missing", "RUN workspace is unavailable", 1); return run.run_home_path; }
375
- 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`; }
433
+ function requireHome(run) { if (!run.run_root)
434
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
435
+ export function finishCommand(context, runId, projectRoot, decision, retryCheckId) { const retry = retryCheckId ? ` --retry-check ${retryCheckId} --reason "<environment recovery evidence>"` : ""; return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage code-review --decision-file ${JSON.stringify(decision)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`); }
376
436
  function findFiles(root, name) { if (!fs.existsSync(root))
377
437
  return []; const out = []; for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
378
438
  const file = path.join(root, entry.name);