@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.110

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 (162) hide show
  1. package/CHANGELOG.md +520 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +89 -13
  6. package/dist/cli/hook-ingress.js +77 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/native-hook-ingress.js +48 -0
  9. package/dist/cli/run-cli.js +1465 -384
  10. package/dist/cli.js +8 -2
  11. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  12. package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
  13. package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
  14. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  15. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  16. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  17. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  18. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  19. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  20. package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
  21. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  22. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
  23. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  24. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  25. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
  26. package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
  27. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  28. package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +388 -0
  30. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  31. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
  32. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
  35. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  36. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  37. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  38. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  39. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  40. package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
  41. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  42. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
  43. package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
  44. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  45. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  46. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  47. package/dist/harness-runtime/lib/process-json.mjs +71 -0
  48. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  49. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  50. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  51. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  52. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  53. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  54. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  55. package/dist/protocol/local-files.js +18 -9
  56. package/dist/runtime/context.js +2 -2
  57. package/dist/schemas/agent-profile.schema.json +1 -1
  58. package/dist/schemas/code-review-result.schema.json +1 -1
  59. package/dist/schemas/code-work-batch.schema.json +3 -3
  60. package/dist/schemas/code-work-result.schema.json +3 -3
  61. package/dist/schemas/plan-review-result.schema.json +2 -2
  62. package/dist/schemas/run-control-receipt.schema.json +99 -0
  63. package/dist/schemas/run-control-request.schema.json +36 -0
  64. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  65. package/dist/services/canon.js +11 -8
  66. package/dist/services/cleanup.js +132 -39
  67. package/dist/services/cli-operation-classifier.js +22 -24
  68. package/dist/services/code-checks.js +589 -106
  69. package/dist/services/codex-hook-delivery.js +28 -0
  70. package/dist/services/command-context.js +80 -0
  71. package/dist/services/config.js +11 -8
  72. package/dist/services/continuation-outcome.js +20 -0
  73. package/dist/services/controller-fanout.js +173 -0
  74. package/dist/services/dashboard.js +103 -31
  75. package/dist/services/engines.js +121 -76
  76. package/dist/services/eval-snapshots.js +994 -97
  77. package/dist/services/execution-policy.js +181 -0
  78. package/dist/services/external-work-launch.js +166 -0
  79. package/dist/services/harness-adapter.js +147 -24
  80. package/dist/services/harness-config.js +2 -0
  81. package/dist/services/hooks.js +702 -327
  82. package/dist/services/lanes.js +78 -65
  83. package/dist/services/lifecycle-command.js +113 -18
  84. package/dist/services/lifecycle-contract.js +30 -0
  85. package/dist/services/lifecycle-invocations.js +1379 -0
  86. package/dist/services/managed-daemon-binding.js +38 -0
  87. package/dist/services/managed-processes.js +302 -34
  88. package/dist/services/merge-queue.js +228 -149
  89. package/dist/services/merge-server.js +66 -28
  90. package/dist/services/migrations.js +37 -16
  91. package/dist/services/native-daemon-history.js +49 -0
  92. package/dist/services/native-session-control.js +39 -0
  93. package/dist/services/plan-runtime.js +26 -11
  94. package/dist/services/plans.js +28 -25
  95. package/dist/services/projects.js +20 -15
  96. package/dist/services/prompts.js +25 -16
  97. package/dist/services/protocols.js +160 -86
  98. package/dist/services/recovery-observation-budget.js +61 -0
  99. package/dist/services/recovery-snapshot-database.js +142 -0
  100. package/dist/services/repair-intents.js +40 -0
  101. package/dist/services/review-copy.js +136 -0
  102. package/dist/services/run-control-receipt.js +56 -0
  103. package/dist/services/run-control-worker.js +351 -0
  104. package/dist/services/run-control.js +888 -0
  105. package/dist/services/run-controller-adapter.js +231 -0
  106. package/dist/services/run-controller-capture.js +167 -0
  107. package/dist/services/run-controller-process.js +180 -0
  108. package/dist/services/run-controller-recovery.js +221 -0
  109. package/dist/services/run-controller-state.js +36 -0
  110. package/dist/services/run-controller.js +798 -0
  111. package/dist/services/run-engine-bindings.js +1 -1
  112. package/dist/services/run-fork.js +142 -0
  113. package/dist/services/run-observations.js +112 -0
  114. package/dist/services/run-projection.js +22 -64
  115. package/dist/services/run-recovery-runtime.js +80 -0
  116. package/dist/services/run-recovery.js +345 -0
  117. package/dist/services/runs.js +536 -181
  118. package/dist/services/runtime-budget.js +413 -0
  119. package/dist/services/runtime-command.js +43 -0
  120. package/dist/services/runtime-scope-capture.js +52 -0
  121. package/dist/services/runtime-scope-control.js +460 -0
  122. package/dist/services/runtime-scope-resume.js +577 -0
  123. package/dist/services/runtime-scope-stop.js +159 -0
  124. package/dist/services/runtime-scope-worker.js +242 -0
  125. package/dist/services/runtime-service.js +97 -0
  126. package/dist/services/schema-validation.js +10 -8
  127. package/dist/services/scope-writer-cleanup.js +26 -0
  128. package/dist/services/sessions.js +35 -69
  129. package/dist/services/stage-blocker.js +17 -7
  130. package/dist/services/stage-context.js +50 -19
  131. package/dist/services/stage-lifecycle.js +100 -103
  132. package/dist/services/stage-pause.js +88 -75
  133. package/dist/services/stage-report-renderer.js +116 -6
  134. package/dist/services/stage-work-graph.js +35 -0
  135. package/dist/services/usage.js +35 -98
  136. package/dist/services/vnext-code-review.js +197 -84
  137. package/dist/services/vnext-code.js +278 -251
  138. package/dist/services/vnext-execution-profile.js +5 -3
  139. package/dist/services/vnext-fanout.js +172 -17
  140. package/dist/services/vnext-merge.js +345 -111
  141. package/dist/services/vnext-plan-review.js +171 -111
  142. package/dist/services/vnext-plan.js +180 -87
  143. package/dist/services/vnext-protocolize.js +320 -74
  144. package/dist/services/vnext-specify.js +115 -77
  145. package/dist/services/work-registry.js +1001 -187
  146. package/dist/services/workspace-bootstrap.js +76 -0
  147. package/dist/services/workspace-files.js +83 -0
  148. package/dist/services/worktrees.js +88 -36
  149. package/dist/shared/entity-references.js +8 -0
  150. package/dist/shared/errors.js +12 -0
  151. package/dist/storage/database.js +692 -35
  152. package/dist/storage/foreign-key-transaction.js +39 -0
  153. package/dist/storage/immutable-input.js +45 -0
  154. package/dist/storage/import-guard.js +102 -0
  155. package/dist/storage/native-cleanup.js +52 -0
  156. package/dist/storage/paths.js +17 -4
  157. package/dist/storage/projection-lock.js +73 -0
  158. package/dist/storage/work-references.js +23 -0
  159. package/dist/storage/writer-contract.js +77 -0
  160. package/dist/storage/writer-migration.js +85 -0
  161. package/package.json +24 -13
  162. package/tools/repair-paused-run-status.mjs +84 -0
@@ -1,43 +1,69 @@
1
- import crypto from "node:crypto";
2
- import { spawn } from "node:child_process";
1
+ import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
3
+ import { ensureAggregateRepair, ensureRepairIntent } from "./repair-intents.js";
4
+ import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
3
5
  import fs from "node:fs";
4
6
  import path from "node:path";
5
7
  import { AppError } from "../shared/errors.js";
6
8
  import { resolveProjectRoot } from "../storage/paths.js";
7
- import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
9
+ import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
8
10
  import { requireProjectByRoot } from "./projects.js";
9
- import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
11
+ import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, prepareFlowRunStageCompletion, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
10
12
  import { validateSchema } from "./schema-validation.js";
11
- import { flowCommand } from "./stage-pause.js";
13
+ import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
14
+ import { stageCycle, stageGraphScope } from "./stage-work-graph.js";
12
15
  import { validateVnextCodeHandoff } from "./vnext-plan.js";
13
16
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
14
17
  import { readVnextSpecifyResult } from "./vnext-specify.js";
15
- import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
18
+ import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
16
19
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
17
- import { writeStageReport } from "./stage-report-renderer.js";
18
- import { ensureVnextMergeRequest } from "./vnext-merge.js";
20
+ import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
21
+ import { prepareVnextMergeRequest } from "./vnext-merge.js";
19
22
  import { assertStageStartHookEvent } from "./hooks.js";
20
- import { applyExternalStageContext } from "./stage-context.js";
23
+ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
21
24
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
25
+ import { assertRunMutationAllowed } from "./run-recovery.js";
26
+ import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
22
27
  const stage = "code";
23
28
  const stageDir = vnextStageDirectory("code");
24
- export async function startVnextCode(context, input) {
29
+ /** A controller prepares slow, deterministic project tooling before it asks an
30
+ * agent to enter CODE or a restored CODE_REVIEW. The subsequent lifecycle tool call remains the sole
31
+ * authority for attaching and binding the Stage. */
32
+ export async function prepareVnextCodeWorkspace(context, input) {
33
+ if (context.env.DD_FLOW_CODE_READINESS_OWNER !== input.ownerId) {
34
+ throw new AppError("code_readiness_owner_mismatch", "CODE readiness preparation must use its controller's frozen owner", 1, { owner_id: input.ownerId });
35
+ }
25
36
  const projectRoot = resolveProjectRoot(input.projectRoot);
26
37
  const project = requireProjectByRoot(context, projectRoot);
27
38
  const run = requireRun(context, project.id, input.runId);
28
- assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
39
+ assertRunMutationAllowed(context, project.id, run.id);
40
+ requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, stage: input.stage ?? stage });
41
+ const root = path.join(requireHome(run), stageDir);
42
+ fs.mkdirSync(root, { recursive: true });
43
+ return ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress, { force: input.stage !== "code-review" });
44
+ }
45
+ export function prepareVnextCodeStart(context, input) {
46
+ const projectRoot = resolveProjectRoot(input.projectRoot);
47
+ const project = requireProjectByRoot(context, projectRoot);
48
+ const run = requireRun(context, project.id, input.runId);
49
+ assertRunMutationAllowed(context, project.id, run.id);
29
50
  const home = requireHome(run);
30
51
  requireVnextWorkspaceRoute({ projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
31
52
  const existingRoot = path.join(home, stageDir);
32
53
  const existingPrompt = path.join(existingRoot, "stage-prompt.md");
33
54
  const existingStage = stageStatus(run, stage);
55
+ const sourceRepairAttempt = isMergeSourceRepairAttempt(run);
56
+ const rootWork = requireRootWork(context, project.id, input.runId);
57
+ if (input.externalContext)
58
+ prepareExternalStageContext({ stageRoot: existingRoot, loaded: input.externalContext });
59
+ prepareOutputFile(existingPrompt);
60
+ prepareOutputFile(path.join(existingRoot, "fanout.json"));
61
+ const common = { projectRoot, project, run, home, root: existingRoot, promptPath: existingPrompt, rootWork, sourceRepairAttempt };
34
62
  if (existingStage && fs.existsSync(existingPrompt)) {
35
- const externalContext = applyExternalStageContext({ stageRoot: existingRoot, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
36
- const prompt = fs.readFileSync(existingPrompt, "utf8");
37
- const rootWork = requireRootWork(context, project.id, input.runId);
38
- const orchestration = readFanoutDescriptor(existingRoot) ?? writeFanoutDescriptor(existingRoot, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
39
- return { ok: true, resumed: true, run_id: run.id, stage, stage_status: existingStage, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: `${flowCommand(context)} work ls --run ${run.id} --ready --project-root ${JSON.stringify(projectRoot)} --json`, finish_command: finishCommand(context, input.runId, projectRoot, path.join(existingRoot, "code-verification.json")) } };
63
+ return { ...common, resumed: true, existingStage, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(existingRoot) };
40
64
  }
65
+ if (!sourceRepairAttempt)
66
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
41
67
  const batch = path.join(home, "03-plan", "code-work-batch.json");
42
68
  const handoffFailures = validateVnextCodeHandoff(context, {
43
69
  projectRoot,
@@ -57,12 +83,25 @@ export async function startVnextCode(context, input) {
57
83
  runRoot: home
58
84
  });
59
85
  validateWorkBatchFile(batch);
60
- const root = path.join(home, stageDir);
86
+ const template = fs.readFileSync(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
87
+ const variables = getFlowRunVariables(context, { projectRoot, runId: run.id });
88
+ return { ...common, resumed: false, template, capacity: variables.variables?.["runtime.subagent.available_slots"] };
89
+ }
90
+ export async function startVnextCode(context, input, prepared = prepareVnextCodeStart(context, input)) {
91
+ const { projectRoot, project, run, root, promptPath, rootWork, sourceRepairAttempt } = prepared;
92
+ assertRunMutationAllowed(context, project.id, run.id);
93
+ assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
94
+ if (prepared.resumed) {
95
+ const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
96
+ bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
97
+ const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
98
+ return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) } };
99
+ }
61
100
  fs.mkdirSync(root, { recursive: true });
62
101
  const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
63
- const rootWork = requireRootWork(context, project.id, input.runId);
64
- const promptPath = path.join(root, "stage-prompt.md");
65
- const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness });
102
+ // Graph rendering may issue managed child commands; it belongs to execution.
103
+ const graph = codeWorkGraph(context, project.id, run.id);
104
+ const prompt = `${coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph })}\n${stageLifecycleInstruction}\n`;
66
105
  fs.writeFileSync(promptPath, prompt);
67
106
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
68
107
  const binding = bindStageCoordinatorWork(context, {
@@ -73,16 +112,17 @@ export async function startVnextCode(context, input) {
73
112
  resultPath: path.join(root, "stage-report.json"),
74
113
  ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
75
114
  });
76
- attachFlowRunStage(context, {
77
- projectRoot,
78
- runId: input.runId,
79
- stage,
80
- dir: stageDir,
81
- status: "running",
82
- dataSchemaId: "dd-flow/stage-report@1"
83
- });
84
- const graph = codeWorkGraph(context, project.id, input.runId);
85
- const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
115
+ if (!sourceRepairAttempt) {
116
+ attachFlowRunStage(context, {
117
+ projectRoot,
118
+ runId: input.runId,
119
+ stage,
120
+ dir: stageDir,
121
+ status: "running",
122
+ dataSchemaId: "dd-flow/stage-report@1"
123
+ });
124
+ }
125
+ const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
86
126
  appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
87
127
  return {
88
128
  ok: true,
@@ -96,7 +136,7 @@ export async function startVnextCode(context, input) {
96
136
  graph,
97
137
  orchestration,
98
138
  readiness,
99
- next: { refresh_graph_command: `${flowCommand(context)} work ls --run ${run.id} --ready --project-root ${JSON.stringify(projectRoot)} --json`, finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) }
139
+ next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) }
100
140
  };
101
141
  }
102
142
  export async function finishVnextCode(context, input) {
@@ -105,8 +145,10 @@ export async function finishVnextCode(context, input) {
105
145
  const run = requireRun(context, project.id, input.runId);
106
146
  const home = requireHome(run);
107
147
  const root = path.join(home, stageDir);
148
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
108
149
  requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
109
150
  if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
151
+ resumeAcceptedCodeStage(context, run, projectRoot, stage, JSON.parse(fs.readFileSync(path.join(root, "stage-report.json"), "utf8")), nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))));
110
152
  return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: path.join(root, "stage-report.json"), next_action: nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))) };
111
153
  }
112
154
  const handoffFailures = validateVnextCodeHandoff(context, {
@@ -135,7 +177,8 @@ export async function finishVnextCode(context, input) {
135
177
  throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
136
178
  }
137
179
  // Reject a malformed semantic receipt before running the expensive gate.
138
- const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
180
+ const verification = input.verification ?? verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
181
+ validateSchema({ schemaName: "code-verification", file: input.verificationFile, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
139
182
  if (verification.verdict === "blocked") {
140
183
  return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, instruction: "CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again." };
141
184
  }
@@ -149,58 +192,44 @@ export async function finishVnextCode(context, input) {
149
192
  originWorkIds: works.filter((work) => work.status === "completed").map((work) => work.work_id),
150
193
  semanticUnresolved: verification.unresolved.length ? verification.unresolved : [verification.summary],
151
194
  verificationPath: input.verificationFile,
195
+ verification,
152
196
  objective: verification.summary
153
197
  });
154
- return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted. Run the returned repair Work, then update code-verification.json and invoke this same stage finish command again." };
198
+ return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted. 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. After that Work completes, update code-verification.json and invoke this same stage finish command again." };
155
199
  }
156
200
  const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
157
- const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: checks });
158
- if (unchangedFailures.length)
159
- throw new AppError("code_gate_repair_required", "CODE 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 });
160
- let receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks });
161
- if (!receipts) {
162
- const release = acquireAggregateGateLock(root);
163
- try {
164
- // A prior caller may have completed the slow gate while this caller was
165
- // waiting to acquire the lock. Reuse that immutable evidence.
166
- receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks }) ?? await runCodeChecks(context, {
167
- projectId: project.id,
168
- runId: run.id,
169
- runHome: home,
170
- workspaceRoot: run.workspace_root,
171
- scope: "aggregate",
172
- checks,
173
- ...(input.progress ? { progress: input.progress } : {})
174
- });
175
- }
176
- finally {
177
- release();
178
- }
179
- }
201
+ if (input.retryCheckId) {
202
+ validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
203
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
204
+ }
205
+ const receipts = await runCodeChecks(context, {
206
+ projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, scope: "aggregate", checks,
207
+ ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}),
208
+ ...(input.progress ? { progress: input.progress } : {})
209
+ });
180
210
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
181
211
  if (failed.length) {
212
+ const continuation = registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks, generation });
182
213
  throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
183
214
  failures: failed,
184
215
  outcome: "repair_required",
185
216
  retry_after_workspace_change: true,
186
217
  workspace_fingerprint: failed[0].workspace_fingerprint,
187
- 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`
218
+ continuation,
219
+ instruction: "Repair Work is already registered. End this coordinator Turn; the controller will launch a fresh child. After it completes, update semantic verification and finish the Stage."
188
220
  });
189
221
  }
190
- const allReceipts = checkReceipts(context, { projectId: project.id, runId: run.id });
191
- const finalReceipts = latestReceiptsByCommand(allReceipts);
192
- const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: allReceipts, runId: run.id, runHome: home });
222
+ const finalReceipts = receipts;
223
+ const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: finalReceipts, runId: run.id, runHome: home });
193
224
  const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
194
225
  if (unresolvedAcceptance.length)
195
226
  throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
196
227
  const now = context.now();
197
228
  const timing = stageTiming(home, stage, now);
198
- const reportedPaths = [...new Set(works.flatMap((work) => resultPaths(work.result)))];
199
- const observedChangedPaths = workspaceChangedPaths(run.workspace_root);
200
- const changedPaths = observedChangedPaths ?? reportedPaths;
201
- const missingReportedPaths = observedChangedPaths === null ? [] : reportedPaths.filter((item) => !observedChangedPaths.includes(item));
202
- if (missingReportedPaths.length)
203
- throw new AppError("changed_path_not_materialized", "CODE Work reported paths that are not changed in the accepted workspace", 2, { paths: missingReportedPaths });
229
+ // Work result paths are provenance, not a mutable filesystem oracle. The
230
+ // stage reports one shared Git observation; ignored machine repairs are
231
+ // rejected before a Work can become completed.
232
+ const changedPaths = workspaceChangedPaths(run.workspace_root);
204
233
  const next = nextAction(run, changedPaths);
205
234
  const report = {
206
235
  schema_id: "dd-flow/stage-report@1",
@@ -230,8 +259,8 @@ export async function finishVnextCode(context, input) {
230
259
  wall_clock_ms: timing.wall_clock_ms,
231
260
  git: gitFacts(run.workspace_root),
232
261
  graph: codeWorkGraph(context, project.id, run.id),
233
- session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`,
234
- usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`
262
+ session_stats_command: runtimeCommand(context, ["stat", "run", "sessions", "ls", "--run", run.id, "--project-root", projectRoot, "--json"]),
263
+ usage_stats_command: runtimeCommand(context, ["stat", "usage", "--run", run.id, "--project-root", projectRoot, "--json"])
235
264
  },
236
265
  artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" },
237
266
  validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" }
@@ -239,31 +268,35 @@ export async function finishVnextCode(context, input) {
239
268
  // Creating the MERGE request is part of accepting this terminal handoff.
240
269
  // Do it before materialising a terminal stage report or changing RUN state,
241
270
  // so an invalid merge policy leaves CODE safely running and retryable.
242
- const mergeRequest = next === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
243
- writeReport(root, report);
244
- completeFlowRunStage(context, {
245
- projectRoot,
246
- runId: run.id,
247
- stage,
248
- status: "done",
249
- data: "stage-report.json",
250
- dataSchemaId: "dd-flow/stage-report@1",
251
- report: "stage-report.md",
252
- stageReport: "stage-report.html"
253
- });
254
- const rootWork = requireRootWork(context, project.id, run.id);
255
- appendFlowRunTimelineEvent(context, project.id, run.id, {
256
- type: "code_accepted",
257
- work_id: rootWork.work_id,
258
- checks: finalReceipts.map((receipt) => receipt.id)
271
+ let mergeRequest = null;
272
+ assertRunMutationAllowed(context, project.id, run.id, generation);
273
+ const prepareMerge = next === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
274
+ const preparedReport = prepareStageReport(report);
275
+ const completionInput = { 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", dataSha256: preparedReport.sha256, git: report.mechanical.git };
276
+ withStageSettlement(context, () => {
277
+ assertRunMutationAllowed(context, project.id, run.id, generation);
278
+ const currentWorks = codeWorks(context, project.id, run.id);
279
+ if (currentWorks.length !== works.length || currentWorks.some(work => work.status !== "completed" || !works.some(accepted => accepted.work_id === work.work_id && accepted.result === work.result))) {
280
+ throw new AppError("code_graph_incomplete", "CODE results changed while the final checks were running", 2);
281
+ }
282
+ mergeRequest = prepareMerge?.() ?? null;
283
+ const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
284
+ acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
285
+ completeFlowRunStage(context, completionInput, preparedCompletion);
286
+ const rootWork = requireRootWork(context, project.id, run.id);
287
+ appendFlowRunTimelineEvent(context, project.id, run.id, {
288
+ type: "code_accepted",
289
+ work_id: rootWork.work_id,
290
+ checks: finalReceipts.map((receipt) => receipt.id)
291
+ });
292
+ if (next === "code_completed") {
293
+ finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
294
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
295
+ }
296
+ else {
297
+ advanceFlowRun(context, { settlementStage: "code", projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
298
+ }
259
299
  });
260
- if (next === "code_completed") {
261
- await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
262
- completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
263
- }
264
- else {
265
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
266
- }
267
300
  return {
268
301
  ok: true,
269
302
  run_id: run.id,
@@ -271,19 +304,58 @@ export async function finishVnextCode(context, input) {
271
304
  outcome: "accepted",
272
305
  report_path: path.join(root, "stage-report.json"),
273
306
  next_action: next,
274
- ...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: `${flowCommand(context)} stage start ${run.id} --stage code-review --project-root ${JSON.stringify(projectRoot)} --json` } } : next === "start_merge" ? { 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` } } : {})
307
+ ...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "code-review", "--project-root", projectRoot, "--json"])) } } : next === "start_merge" ? { 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"])) } } : {})
275
308
  };
276
309
  }
277
- export function addVnextCodeRepair(context, input) {
310
+ export function registerAggregateCodeRepair(context, input) {
311
+ const project = requireProjectByRoot(context, input.projectRoot);
312
+ const origins = codeWorks(context, project.id, input.runId).filter(work => !packet(work)?.repair && work.status === "completed");
313
+ const failedDeclarations = new Set(input.failures.flatMap(receipt => [receipt.declaration_id, ...receipt.check_refs]));
314
+ const objective = ["Investigate and repair all retained aggregate check failures while preserving accepted requirements and non-goals. If evidence proves an external blocker or a contradiction, report it; do not invent a product change.", ...input.failures.map(receipt => `Receipt: ${receipt.receipt_path}\nstdout: ${receipt.stdout_path}\nstderr: ${receipt.stderr_path}`)].join("\n\n");
315
+ try {
316
+ return ensureAggregateRepair(context, { projectId: project.id, runId: input.runId, stage: input.stage, failures: input.failures, origins: origins.map(work => work.work_id), ...(input.generation !== undefined ? { generation: input.generation } : {}) }, () => String(addVnextCodeRepair(context, { registrationOwned: true, projectRoot: input.projectRoot, runId: input.runId, stage: input.stage, ...(input.stage === "code-review" ? { reviewCycle: stageCycle(context, project.id, input.runId, input.stage) } : {}), checkReceiptId: input.failures[0].id, originWorkIds: origins.map(work => work.work_id), objective, reviewChecks: input.checks.filter(check => failedDeclarations.has(check.id) || failedDeclarations.has(check.canonical_ref ?? "")) }).repair_work_id));
317
+ }
318
+ catch (error) {
319
+ if (!(error instanceof AppError))
320
+ throw error;
321
+ throw new AppError(error.code, error.message, error.exitCode, { ...error.details, stage: input.stage, run_id: input.runId, failures: input.failures, cause: { code: input.stage === "code" ? "code_gate_failed" : "code_review_gate_failed", message: "Aggregate checks failed; repair registration was rejected" } });
322
+ }
323
+ }
324
+ export function prepareVnextCodeRepair(context, input) {
278
325
  const projectRoot = resolveProjectRoot(input.projectRoot);
279
326
  const project = requireProjectByRoot(context, projectRoot);
280
327
  const run = requireRun(context, project.id, input.runId);
281
328
  const home = requireHome(run);
282
- if (!input.objective.trim())
329
+ if (input.reviewCycle) {
330
+ const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle && (input.checkReceiptId ? packet(work)?.repair?.check_receipt_id === input.checkReceiptId : Boolean(packet(work)?.repair?.review_findings?.length)));
331
+ if (previous)
332
+ return { kind: "replay", project, run, previous };
333
+ }
334
+ if (typeof input.objective !== "string" || !input.objective.trim())
283
335
  throw new AppError("validation", "Repair objective must not be empty", 2);
336
+ if (input.semanticUnresolved !== undefined && (!Array.isArray(input.semanticUnresolved) || input.semanticUnresolved.some(value => typeof value !== "string" || !value.trim())))
337
+ throw new AppError("validation", "Semantic unresolved items must be nonempty strings", 2);
338
+ if (input.reviewFindings !== undefined && !Array.isArray(input.reviewFindings))
339
+ throw new AppError("validation", "Review findings must be an array", 2);
340
+ if (input.reviewChecks !== undefined && !Array.isArray(input.reviewChecks))
341
+ throw new AppError("validation", "Review checks must be an array", 2);
342
+ if (!Array.isArray(input.originWorkIds) || !input.originWorkIds.length || input.originWorkIds.some(id => typeof id !== "string" || !id.trim()))
343
+ throw new AppError("validation", "Repair requires at least one origin Work", 2);
284
344
  if (!input.checkReceiptId && !input.reviewFindings?.length && !input.semanticUnresolved?.length) {
285
345
  throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
286
346
  }
347
+ if ([Boolean(input.checkReceiptId), Boolean(input.reviewFindings?.length), Boolean(input.semanticUnresolved?.length)].filter(Boolean).length !== 1)
348
+ throw new AppError("validation", "Repair requires exactly one evidence source", 2);
349
+ if (input.semanticUnresolved?.length) {
350
+ if (!input.verificationPath)
351
+ throw new AppError("usage", "Semantic repair requires a verification file", 2);
352
+ const verification = input.verification ?? readVerification(context, { file: input.verificationPath, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
353
+ if (input.verification !== undefined)
354
+ validateSchema({ schemaName: "code-verification", file: input.verificationPath, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
355
+ const unresolved = verification.unresolved.length ? verification.unresolved : [verification.summary];
356
+ if (verification.verdict !== "needs_repair" || input.semanticUnresolved.some(value => !unresolved.includes(value)))
357
+ throw new AppError("validation", "Semantic repair must reference unresolved items from a needs_repair verification", 2);
358
+ }
287
359
  const receipt = input.checkReceiptId ? context.db.get("SELECT id, declaration_id, run_id, command, status, receipt_path, stdout_path, stderr_path FROM check_receipts WHERE project_id = ? AND id = ?", [
288
360
  project.id,
289
361
  input.checkReceiptId
@@ -304,10 +376,10 @@ export function addVnextCodeRepair(context, input) {
304
376
  // repair that fixes one failed aggregate check can make a second, already
305
377
  // declared aggregate failure impossible to repair. The immutable source
306
378
  // of repair eligibility is the original CODE graph.
307
- const declaredChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
379
+ const declaredChecks = effectiveCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
308
380
  .map((work) => packet(work))
309
381
  .filter((value) => !value.repair)
310
- .flatMap((value) => value.checks));
382
+ .flatMap((value) => value.checks), ["work", "code", "readiness", "merge"]);
311
383
  const failedCheck = receipt ? declaredChecks.find((check) => check.id === receipt.declaration_id) : undefined;
312
384
  if (receipt && !failedCheck) {
313
385
  throw new AppError("repair_check_declaration_missing", "Repair receipt is not backed by an accepted CODE check declaration", 2, {
@@ -327,7 +399,11 @@ export function addVnextCodeRepair(context, input) {
327
399
  origin_work_ids: origins.map((work) => work.work_id),
328
400
  ...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
329
401
  ...(input.reviewFindings?.length ? { review_findings: input.reviewFindings } : {}),
330
- ...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {})
402
+ ...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {}),
403
+ // `run_at` remains the accepted aggregate placement. This separate
404
+ // repair obligation is deliberately executed before this repair Work is
405
+ // accepted, so a local fix cannot close without re-proving its cause.
406
+ verification_check_refs: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) })
331
407
  },
332
408
  task: input.objective,
333
409
  semantic_spine: {
@@ -357,35 +433,36 @@ export function addVnextCodeRepair(context, input) {
357
433
  ...(receipt ? ["Stop and report the blocker if the proposed repair contradicts an accepted requirement or non-goal."] : ["Do not modify accepted PLAN artifacts, code-work-batch.json, or review decisions. Stop and report if the finding cannot be repaired within accepted scope."])
358
434
  ]),
359
435
  depends_on: origins.map((work) => work.work_id),
360
- result_schema: "dd-flow/code-work-result@2"
436
+ result_schema: "dd-flow/code-work-result@3",
437
+ ...(input.reviewCycle ? { review_cycle: input.reviewCycle } : {})
361
438
  };
362
- const file = path.join(home, stageDir, `.repair-${crypto.randomUUID()}.json`);
363
- fs.mkdirSync(path.dirname(file), { recursive: true });
364
- fs.writeFileSync(file, `${JSON.stringify({ works: [repair] }, null, 2)}\n`);
365
- let registered;
366
- try {
367
- registered = addWorkBatch(context, { parentWorkId: requireRootWork(context, project.id, run.id).work_id, file });
368
- }
369
- finally {
370
- fs.rmSync(file, { force: true });
371
- }
372
- const id = String(registered.work_ids[key]);
439
+ const parentWorkId = requireRootWork(context, project.id, run.id).work_id;
440
+ const items = prepareWorkBatch(context, { parentWorkId, value: { works: [repair] } });
441
+ return { kind: "create", project, run, input, parentWorkId, items, key, receipt };
442
+ }
443
+ export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRepair(context, input)) {
444
+ const { project, run } = prepared;
445
+ if (prepared.kind === "replay")
446
+ return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous), reused: true };
447
+ const { parentWorkId, items, key, receipt } = prepared;
448
+ const create = () => String(addWorkBatch(context, { parentWorkId, prepared: items, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"), ...(input.reviewCycle ? { cycle: input.reviewCycle } : {}) }).work_ids[key]);
449
+ const id = input.registrationOwned ? create() : ensureRepairIntent(context, {
450
+ projectId: project.id, runId: run.id, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"),
451
+ causes: input.checkReceiptId ? { receipt: input.checkReceiptId, origins: [...input.originWorkIds].sort() } : input.reviewCycle ? { review_cycle: input.reviewCycle, findings: input.reviewFindings } : { verification: input.verification, unresolved: input.semanticUnresolved, origins: [...input.originWorkIds].sort() }
452
+ }, create).work_id;
373
453
  const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]);
374
454
  appendFlowRunTimelineEvent(context, project.id, run.id, {
375
455
  type: "code_repair_created",
376
456
  work_id: id,
377
- origin_work_ids: input.originWorkIds,
378
- ...(receipt ? { check_receipt_id: receipt.id } : input.reviewFindings?.length ? { review_finding_ids: input.reviewFindings.map((finding) => finding.finding_ref) } : { semantic_unresolved: input.semanticUnresolved })
457
+ origin_work_ids: prepared.input.originWorkIds,
458
+ ...(receipt ? { check_receipt_id: receipt.id } : prepared.input.reviewFindings?.length ? { review_finding_ids: prepared.input.reviewFindings.map((finding) => finding.finding_ref) } : { semantic_unresolved: prepared.input.semanticUnresolved })
379
459
  });
380
460
  refreshRunWorkProjection(context, project.id, run.id);
381
461
  return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
382
462
  }
383
- /** Retain the causal declaration for worker context; run_at keeps aggregate gates at stage scope. */
463
+ /** Retain the causal declaration for worker context; run_at still records its original gate. */
384
464
  export function selectRepairChecks(input) {
385
- // Gate placement belongs to the accepted PLAN. `runCodeChecks` executes only
386
- // work-scoped entries, while retaining the aggregate declaration tells the
387
- // worker exactly what will be rerun by the coordinator.
388
- const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
465
+ const candidates = input.failedCheck ? [input.failedCheck, ...(input.reviewChecks ?? [])] : input.reviewChecks ?? input.semanticChecks ?? [];
389
466
  const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
390
467
  return uniqueBy(checks, (check) => check.id);
391
468
  }
@@ -414,15 +491,13 @@ function receiptRepairPaths(workspaceRoot, receipt) {
414
491
  return [...paths].sort();
415
492
  }
416
493
  function coordinatorPrompt(context, input) {
417
- const graph = codeWorkGraph(context, input.run.project_id, input.run.id);
418
- const variables = getFlowRunVariables(context, { projectRoot: input.projectRoot, runId: input.run.id });
419
- const capacity = variables.variables?.["runtime.subagent.available_slots"];
420
- const template = fs.readFileSync(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
494
+ const { graph, capacity, template } = input;
421
495
  return [
422
496
  "<stage_identity>",
423
497
  `- RUN: ${input.run.id}`,
424
498
  `- root Work: ${input.rootWork.work_id}`,
425
499
  "- stage: code",
500
+ "Before any project edit, change into the immutable write workspace shown below with cd --. Write only that workspace and the named CODE verification artifacts under the stage workspace; never another RUN/native home or Git/worktree control data.",
426
501
  "</stage_identity>",
427
502
  "",
428
503
  "<trusted_runtime_context>",
@@ -439,17 +514,18 @@ function coordinatorPrompt(context, input) {
439
514
  "</code_graph>",
440
515
  "",
441
516
  "<execution_commands>",
442
- "Launch only entries listed in graph.ready. Use at most the known available slot count. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain; the coordinator owns dispatch and the stage conclusion, not implementation Work. Every child starts with its exact start_command and receives its complete packet from dd-flow. After a Work finishes, use the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " + `${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`,
443
- "A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return: do not inspect its PID, start a second finish command, or infer failure from quiet output. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
444
- `A repairable engine, harness, or environment failure is not a user question. Record it without finishing CODE: ${flowCommand(context)} stage block ${input.run.id} --stage code --work ${input.rootWork.work_id} --kind <engine|harness|environment> --code <stable-code> --summary-stdin --retryable --project-root ${JSON.stringify(input.projectRoot)} --json. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
517
+ "Do not choose a provider delegation tool from this stage prompt. At each work_fanout boundary, stop at the Work-graph boundary; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. It launches only entries listed in graph.ready and uses at most the qualified capacity. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain. The coordinator launches those children only through the controller-supplied native delegation instruction; it must never invoke a Work start_command itself. Each child receives its complete packet from dd-flow and runs its own exact start_command. After a Work finishes, the controller uses the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]),
518
+ "A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return once: a completed command with a non-zero exit and structured `code_gate_failed` output is its terminal result, not a reason to keep waiting. A repair_required continuation already registered the repair: end this Turn. Do not invoke that Work's start_command from the coordinator Session, inspect its PID, start a second finish command, or infer failure from quiet output. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
519
+ `A repairable engine, harness, or environment failure is not a user question. Write the evidence summary to ${path.join(requireHome(input.run), "works", input.rootWork.work_id, "block-summary.md")}, then record it with a separate command: ${runtimeCommand(context, ["stage", "block", input.run.id, "--stage", "code", "--work", input.rootWork.work_id, "--kind", "<engine|harness|environment>", "--code", "<stable-code>", "--summary-file", `@run/works/${input.rootWork.work_id}/block-summary.md`, "--retryable", "--project-root", input.projectRoot, "--json"])}. Choose kind and stable-code from evidence; this example is not an executable continuation. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
445
520
  `When every CODE and repair Work is completed, write ${path.join(input.root, "code-verification.json")} using the exact contract below. Mark passed only when all accepted requirements and current-gate acceptance criteria are implemented or explicitly evidenced; list every remaining issue in unresolved. Every evidence_refs item must already exist as a relative workspace path or run://${input.run.id}/ path. Do not claim a browser or other check receipt that was not retained. Then finish: ${finishCommand(context, input.run.id, input.projectRoot, path.join(input.root, "code-verification.json"))}`,
446
- "If the aggregate gate fails, do not stop after `code_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use its returned repair command with the relevant completed origin Work IDs and a concise repair objective, then stop so the runner can dispatch the newly declared repair. Do not edit invisibly in the root orchestrator.",
521
+ "A code_gate_failed response with continuation.kind=repair_required has already registered one repair Work with all failed receipts. End this Turn so the controller can dispatch it in a fresh child. Do not recreate the repair, copy origin IDs, write a repair task file or execute the child start command yourself. After repair, update semantic verification and finish again.",
447
522
  "</execution_commands>",
448
523
  "",
449
524
  "<verification_contract>",
450
525
  "```json",
451
- JSON.stringify({ schema_id: "dd-flow/code-verification@2", verdict: "passed | needs_repair | blocked", summary: "Evidence-backed conclusion.", unresolved: [], deviations: [] }, null, 2),
526
+ JSON.stringify({ schema_id: "dd-flow/code-verification@2", verdict: "passed", summary: "Evidence-backed conclusion.", unresolved: [], deviations: [] }, null, 2),
452
527
  "```",
528
+ "Allowed verdict values are passed, needs_repair, and blocked. The example is valid JSON using passed; choose exactly one value.",
453
529
  "The CLI verifies graph coverage and executes all deterministic checks. Do not duplicate its report; record only your semantic conclusion and evidence.",
454
530
  "</verification_contract>",
455
531
  "",
@@ -462,76 +538,6 @@ function coordinatorPrompt(context, input) {
462
538
  export function profileCommands(workspaceRoot) {
463
539
  return aggregateCheckDeclarations(workspaceRoot).map((check) => check.command);
464
540
  }
465
- function latestReceiptsByCommand(receipts) {
466
- const latest = new Map();
467
- for (const receipt of receipts)
468
- for (const ref of receipt.check_refs)
469
- latest.set(ref, receipt);
470
- return [...new Set(latest.values())];
471
- }
472
- /**
473
- * A CODE finish may be retried after a client loses the long command's output.
474
- * Reusing complete, same-workspace receipts is correct; rerunning them is both
475
- * expensive and can race the still-settling original invocation.
476
- */
477
- export function reusableAggregateGateReceipts(context, input) {
478
- const expected = new Set(input.checks.map((check) => check.id));
479
- const current = workspaceFingerprint(input.workspaceRoot);
480
- const latest = new Map();
481
- for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId })) {
482
- if (receipt.scope === "aggregate")
483
- for (const ref of receipt.check_refs)
484
- if (expected.has(ref))
485
- latest.set(ref, receipt);
486
- }
487
- if (latest.size !== expected.size)
488
- return null;
489
- const receipts = input.checks.map((check) => latest.get(check.id));
490
- return receipts.every((receipt) => receipt.status === "passed" && receipt.workspace_fingerprint === current) ? receipts : null;
491
- }
492
- /** One aggregate gate per RUN; a second invocation must wait, never duplicate it. */
493
- function acquireAggregateGateLock(root) {
494
- const directory = path.join(root, "checks");
495
- const lock = path.join(directory, ".aggregate-gate.lock");
496
- fs.mkdirSync(directory, { recursive: true });
497
- try {
498
- const descriptor = fs.openSync(lock, "wx");
499
- fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
500
- fs.closeSync(descriptor);
501
- }
502
- catch (error) {
503
- if (!isExistingLock(error))
504
- throw error;
505
- const activePid = lockPid(lock);
506
- if (activePid && processAlive(activePid)) {
507
- throw new AppError("code_gate_in_progress", "CODE aggregate gate is already running; wait for that exact stage finish command to return", 1, { pid: activePid, lock });
508
- }
509
- fs.rmSync(lock, { force: true });
510
- return acquireAggregateGateLock(root);
511
- }
512
- return () => fs.rmSync(lock, { force: true });
513
- }
514
- function isExistingLock(error) {
515
- return Boolean(error && typeof error === "object" && error.code === "EEXIST");
516
- }
517
- function lockPid(lock) {
518
- try {
519
- const parsed = JSON.parse(fs.readFileSync(lock, "utf8"));
520
- return Number.isInteger(parsed.pid) && Number(parsed.pid) > 0 ? Number(parsed.pid) : null;
521
- }
522
- catch {
523
- return null;
524
- }
525
- }
526
- function processAlive(pid) {
527
- try {
528
- process.kill(pid, 0);
529
- return true;
530
- }
531
- catch {
532
- return false;
533
- }
534
- }
535
541
  /**
536
542
  * The immutable Work result is a repair history. The stage report is a current
537
543
  * acceptance projection, so a registered receipt from an older check epoch
@@ -545,8 +551,8 @@ export function verificationProjection(works, receipts, options = {}) {
545
551
  for (const work of works) {
546
552
  const result = readJsonResult(work.result);
547
553
  for (const item of result.evidence ?? [])
548
- if (item.criterion_id)
549
- rawEvidence.set(item.criterion_id, unique([...(rawEvidence.get(item.criterion_id) ?? []), ...(item.refs ?? [])]));
554
+ if (item.obligation_ref)
555
+ rawEvidence.set(item.obligation_ref, unique([...(rawEvidence.get(item.obligation_ref) ?? []), ...(item.refs ?? [])]));
550
556
  }
551
557
  const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
552
558
  const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
@@ -561,18 +567,16 @@ export function verificationProjection(works, receipts, options = {}) {
561
567
  return [item.criterion_id, { evidenceRefs, historical }];
562
568
  }));
563
569
  const hasEvidence = (criterionId) => (acceptanceEvidence.get(criterionId)?.evidenceRefs.length ?? 0) > 0;
564
- const isPassed = (checkId, criterionId) => receiptByDeclaration.get(checkId)?.status === "passed" || (declarations.find((check) => check.id === checkId)?.run_at === "external" && hasEvidence(criterionId));
570
+ const isPassed = (checkId) => receiptByDeclaration.get(checkId)?.status === "passed";
565
571
  return {
566
572
  checks: declarations.map((check) => {
567
573
  const receipt = receiptByDeclaration.get(check.id);
568
- const externallyEvidenced = check.run_at === "external" && acceptance.some((item) => item.check_refs.includes(check.id) && hasEvidence(item.criterion_id));
569
- return { id: check.id, run_at: check.run_at, status: receipt?.status ?? (externallyEvidenced ? "evidence_recorded" : ["work", "code", "readiness"].includes(check.run_at) ? "failed" : "not_due"), receipt_ref: receipt?.receipt_path ?? null };
574
+ return { id: check.id, run_at: check.run_at, status: receipt?.status ?? (["work", "code", "readiness"].includes(check.run_at) ? "failed" : "not_due"), receipt_ref: receipt?.receipt_path ?? null };
570
575
  }),
571
576
  acceptance: acceptance.map((item) => {
572
- const external = item.check_refs.some((id) => declarations.find((check) => check.id === id)?.run_at === "external");
573
- const confirmed = item.check_refs.every((id) => isPassed(id, item.criterion_id)) && hasEvidence(item.criterion_id);
577
+ const confirmed = item.check_refs.every((id) => isPassed(id)) && hasEvidence(item.criterion_id);
574
578
  const evidence = acceptanceEvidence.get(item.criterion_id) ?? { evidenceRefs: [], historical: [] };
575
- return { criterion_id: item.criterion_id, gate: item.gate, status: confirmed ? (external ? "confirmed_with_external_evidence" : "confirmed") : ["work", "code", "readiness"].includes(item.gate) ? "unresolved" : "not_due", check_refs: item.check_refs, evidence_refs: evidence.evidenceRefs, ...(evidence.historical.length ? { reported_evidence_refs: evidence.historical } : {}) };
579
+ return { criterion_id: item.criterion_id, gate: item.gate, status: confirmed ? "confirmed" : ["work", "code", "readiness"].includes(item.gate) ? "unresolved" : "not_due", check_refs: item.check_refs, evidence_refs: evidence.evidenceRefs, ...(evidence.historical.length ? { reported_evidence_refs: evidence.historical } : {}) };
576
580
  })
577
581
  };
578
582
  }
@@ -642,69 +646,71 @@ function requireRun(context, projectId, runId) {
642
646
  return run;
643
647
  }
644
648
  /** CODE, not PROTOCOLIZE, makes a checkout ready for project tooling. */
645
- async function ensureCodeWorkspaceReady(context, projectId, run, root, readinessChecks, progress) {
649
+ export function isReusablePreparedCodeReadiness(value, expected) {
650
+ if (!value || typeof value !== "object" || Array.isArray(value))
651
+ return false;
652
+ const receipt = value;
653
+ return receipt.schema_id === "dd-flow/workspace-readiness@2"
654
+ && receipt.status === "passed"
655
+ && receipt.prepared_by === expected.ownerId
656
+ && receipt.workspace_root === expected.workspaceRoot
657
+ && receipt.command === expected.command
658
+ && receipt.policy_ref === expected.policyRef;
659
+ }
660
+ function readPreparedReadiness(file) {
661
+ try {
662
+ return JSON.parse(fs.readFileSync(file, "utf8"));
663
+ }
664
+ catch {
665
+ return null;
666
+ }
667
+ }
668
+ async function ensureCodeWorkspaceReady(context, projectId, run, root, readinessChecks, progress, options = {}) {
646
669
  const profile = JSON.parse(run.index_json);
647
670
  const bootstrap = profile.execution_profile?.settings?.code_bootstrap;
648
671
  if (!bootstrap || typeof bootstrap.command !== "string" || typeof bootstrap.policy_ref !== "string") {
649
672
  throw new AppError("execution_profile_invalid", "CODE requires a frozen bootstrap command in the RUN execution profile", 1, { run_id: run.id });
650
673
  }
651
674
  const receiptPath = path.join(root, "workspace-readiness.json");
652
- const prior = readReadiness(receiptPath);
653
- if (prior?.workspace_root === run.workspace_root && prior.command === bootstrap.command && prior.status === "passed" && JSON.stringify(prior.check_declarations ?? []) === JSON.stringify(readinessChecks))
654
- return { ...prior, reused: true };
675
+ const ownerId = context.env.DD_FLOW_CODE_READINESS_OWNER;
676
+ if (!options.force && ownerId && isReusablePreparedCodeReadiness(readPreparedReadiness(receiptPath), {
677
+ ownerId,
678
+ workspaceRoot: run.workspace_root,
679
+ command: bootstrap.command,
680
+ policyRef: bootstrap.policy_ref
681
+ })) {
682
+ progress?.("workspace bootstrap already prepared by this controller");
683
+ return readPreparedReadiness(receiptPath);
684
+ }
685
+ // Unowned and stale receipts are never trusted: a direct stage entry still
686
+ // performs its own readiness check. Only the same controller can hand a
687
+ // freshly prepared workspace to its immediately following lifecycle call.
655
688
  const startedAt = new Date().toISOString();
656
689
  progress?.(`workspace bootstrap started: ${bootstrap.command}`);
657
- const result = await runBootstrap(bootstrap.command, run.workspace_root, codeExecutionEnvironment(run.workspace_root), progress);
690
+ const result = await runWorkspaceBootstrap(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, command: bootstrap.command, artifactDir: "05-code/readiness", progress });
658
691
  const bootstrapPassed = result.exit_code === 0 && !result.error;
659
692
  const checks = bootstrapPassed && readinessChecks.length
660
693
  ? await runCodeChecks(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, scope: "aggregate", artifactDir: "05-code/readiness", checks: readinessChecks, ...(progress ? { progress } : {}) })
661
694
  : [];
662
- const receipt = { schema_id: "dd-flow/workspace-readiness@2", workspace_root: run.workspace_root, command: bootstrap.command, policy_ref: bootstrap.policy_ref, started_at: startedAt, finished_at: new Date().toISOString(), status: bootstrapPassed && checks.every((check) => check.status === "passed") ? "passed" : "failed", exit_code: result.exit_code, stdout: result.stdout, stderr: result.stderr, check_declarations: readinessChecks, checks: checks.map((check) => ({ id: check.id, status: check.status, receipt_path: check.receipt_path })), ...(result.error ? { error: result.error } : {}), receipt_path: receiptPath };
695
+ const receipt = { schema_id: "dd-flow/workspace-readiness@2", workspace_root: run.workspace_root, command: bootstrap.command, policy_ref: bootstrap.policy_ref, started_at: startedAt, finished_at: new Date().toISOString(), status: bootstrapPassed && checks.every((check) => check.status === "passed") ? "passed" : "failed", ...(ownerId ? { prepared_by: ownerId } : {}), process_id: result.process_id, stdout_path: result.stdout_path, stderr_path: result.stderr_path, completion_path: result.completion_path, exit_code: result.exit_code, stdout: result.stdout, stderr: result.stderr, check_declarations: readinessChecks, checks: checks.map((check) => ({ id: check.id, status: check.status, receipt_path: check.receipt_path })), ...(result.error ? { error: result.error } : {}), receipt_path: receiptPath };
663
696
  fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
664
697
  if (receipt.status !== "passed")
665
698
  throw new AppError("workspace_readiness_failed", "CODE workspace bootstrap failed", 1, receipt);
666
699
  progress?.("workspace bootstrap passed");
667
700
  return receipt;
668
701
  }
669
- function runBootstrap(command, cwd, environment, progress) {
670
- return new Promise((resolve) => {
671
- const started = Date.now();
672
- const child = spawn("/bin/sh", ["-lc", command], { cwd, env: environment, stdio: ["ignore", "pipe", "pipe"] });
673
- let stdout = "";
674
- let stderr = "";
675
- let error = null;
676
- child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
677
- child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
678
- const heartbeat = setInterval(() => progress?.(`workspace bootstrap still running (${Math.floor((Date.now() - started) / 1000)}s)`), 15_000);
679
- const timeout = setTimeout(() => child.kill("SIGTERM"), 15 * 60 * 1000);
680
- child.once("error", (value) => { error = value.message; });
681
- child.once("close", (exitCode, signal) => {
682
- clearInterval(heartbeat);
683
- clearTimeout(timeout);
684
- resolve({ exit_code: exitCode, stdout, stderr, error: error ?? (signal ? `terminated by ${signal}` : null) });
685
- });
686
- });
687
- }
688
- function readReadiness(file) {
689
- try {
690
- return JSON.parse(fs.readFileSync(file, "utf8"));
691
- }
692
- catch {
693
- return null;
694
- }
695
- }
696
702
  function requireHome(run) {
697
703
  if (!run.run_root)
698
704
  throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1);
699
705
  return run.run_root;
700
706
  }
701
- function finishCommand(context, runId, projectRoot, verificationFile) {
702
- return `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`;
707
+ export function finishCommand(context, runId, projectRoot, verificationFile, retryCheckId) {
708
+ return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "code", "--verification-file", verificationFile, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
703
709
  }
704
710
  function readVerification(context, input) {
705
- const file = path.resolve(input.file);
706
- validateSchema({ schemaName: "code-verification", file, projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot });
707
- return JSON.parse(fs.readFileSync(file, "utf8"));
711
+ return prepareJsonSchema(input.file, "CODE verification", {
712
+ schemaName: "code-verification", projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot
713
+ }).value;
708
714
  }
709
715
  function verificationForFinish(context, input) {
710
716
  return readVerification(context, input);
@@ -726,6 +732,8 @@ export function writeProtocolFlowStatus(workspaceRoot, runHome, runId, status) {
726
732
  }
727
733
  function nextAction(run, changedPaths) {
728
734
  const index = JSON.parse(run.index_json);
735
+ if (index.variables?.["merge.source_repair"])
736
+ return "start_code_review";
729
737
  const requested = index.settings?.code_review?.mode ?? "auto";
730
738
  const enabled = requested === "off" ? false : requested === "auto" ? changedPaths.length > 0 : true;
731
739
  const stopTarget = index.execution_profile?.settings?.stop_target;
@@ -741,19 +749,19 @@ function stageStatus(run, name) {
741
749
  return null;
742
750
  }
743
751
  }
744
- function changedPathsFromReport(file) {
752
+ function isMergeSourceRepairAttempt(run) {
745
753
  try {
746
- const value = JSON.parse(fs.readFileSync(file, "utf8"));
747
- return Array.isArray(value.semantic?.changed_files) ? value.semantic.changed_files.filter((item) => typeof item === "string") : [];
754
+ const value = JSON.parse(run.index_json);
755
+ return Boolean(value.variables?.["merge.source_repair"]);
748
756
  }
749
757
  catch {
750
- return [];
758
+ return false;
751
759
  }
752
760
  }
753
- function resultPaths(result) {
761
+ function changedPathsFromReport(file) {
754
762
  try {
755
- const value = JSON.parse(result ?? "");
756
- return Array.isArray(value.changed_paths) ? value.changed_paths.filter((item) => typeof item === "string") : [];
763
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
764
+ return Array.isArray(value.semantic?.changed_files) ? value.semantic.changed_files.filter((item) => typeof item === "string") : [];
757
765
  }
758
766
  catch {
759
767
  return [];
@@ -772,6 +780,25 @@ function uniqueBy(values, key) {
772
780
  return true;
773
781
  });
774
782
  }
775
- function writeReport(root, report) {
776
- writeStageReport(root, report);
783
+ /** Repair pre-atomic terminal stages without repeating accepted checks or rewinding a successor. */
784
+ export function resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next) {
785
+ const index = JSON.parse(run.index_json);
786
+ if ((index.current_stage ?? index.stage_runs.at(-1)?.stage) !== stage)
787
+ return;
788
+ const terminal = next === "code_completed" || next === "code_review_completed";
789
+ if (!terminal && index.next_action === next)
790
+ return;
791
+ withStageSettlement(context, () => {
792
+ if (!terminal) {
793
+ advanceFlowRun(context, { settlementStage: stage, projectRoot, runId: run.id, status: "running", verdict: `${stage.replaceAll("-", "_")}_completed`, nextAction: next });
794
+ return;
795
+ }
796
+ 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", [run.project_id, run.id]);
797
+ if (!work)
798
+ throw new AppError("runtime_missing", "Accepted stage has no root coordinator", 1);
799
+ if (work.status !== "completed")
800
+ finishStageCoordinatorWork(context, work.work_id, JSON.stringify(report, null, 2));
801
+ if (index.status !== "done")
802
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: next });
803
+ });
777
804
  }