@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81

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 (138) hide show
  1. package/CHANGELOG.md +402 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +300 -0
  5. package/dist/cli/help.js +81 -9
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +115 -0
  8. package/dist/cli/run-cli.js +1344 -376
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  11. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  12. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  13. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  14. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  15. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  16. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  17. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  18. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  19. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  21. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  22. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  23. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  24. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  25. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  26. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  27. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  28. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  29. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  30. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  32. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  33. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  34. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  36. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  37. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  38. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  39. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  40. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  41. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  42. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  43. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  44. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  45. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  46. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  47. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  48. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  49. package/dist/schemas/agent-profile.schema.json +1 -1
  50. package/dist/schemas/code-review-result.schema.json +2 -2
  51. package/dist/schemas/code-work-batch.schema.json +4 -3
  52. package/dist/schemas/code-work-result.schema.json +4 -4
  53. package/dist/schemas/plan-review-result.schema.json +2 -2
  54. package/dist/schemas/run-control-receipt.schema.json +99 -0
  55. package/dist/schemas/run-control-request.schema.json +36 -0
  56. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  57. package/dist/services/canon.js +11 -8
  58. package/dist/services/cleanup.js +57 -13
  59. package/dist/services/cli-operation-classifier.js +22 -24
  60. package/dist/services/code-checks.js +543 -96
  61. package/dist/services/codex-hook-delivery.js +28 -0
  62. package/dist/services/config.js +11 -8
  63. package/dist/services/controller-fanout.js +116 -0
  64. package/dist/services/dashboard.js +103 -31
  65. package/dist/services/engines.js +121 -76
  66. package/dist/services/eval-snapshots.js +750 -73
  67. package/dist/services/execution-policy.js +170 -0
  68. package/dist/services/external-work-launch.js +110 -0
  69. package/dist/services/harness-adapter.js +147 -24
  70. package/dist/services/harness-config.js +2 -0
  71. package/dist/services/hooks.js +575 -293
  72. package/dist/services/lanes.js +61 -53
  73. package/dist/services/lifecycle-command.js +87 -7
  74. package/dist/services/lifecycle-invocations.js +835 -0
  75. package/dist/services/managed-daemon-binding.js +38 -0
  76. package/dist/services/managed-processes.js +283 -27
  77. package/dist/services/merge-queue.js +179 -104
  78. package/dist/services/merge-server.js +73 -28
  79. package/dist/services/migrations.js +14 -9
  80. package/dist/services/native-daemon-history.js +49 -0
  81. package/dist/services/native-session-control.js +39 -0
  82. package/dist/services/plan-runtime.js +19 -8
  83. package/dist/services/plans.js +28 -25
  84. package/dist/services/projects.js +18 -5
  85. package/dist/services/prompts.js +23 -16
  86. package/dist/services/protocols.js +64 -20
  87. package/dist/services/recovery-observation-budget.js +61 -0
  88. package/dist/services/recovery-snapshot-database.js +107 -0
  89. package/dist/services/run-control-receipt.js +56 -0
  90. package/dist/services/run-control-worker.js +363 -0
  91. package/dist/services/run-control.js +876 -0
  92. package/dist/services/run-controller-adapter.js +222 -0
  93. package/dist/services/run-controller-capture.js +134 -0
  94. package/dist/services/run-controller-process.js +189 -0
  95. package/dist/services/run-controller-recovery.js +222 -0
  96. package/dist/services/run-controller-state.js +36 -0
  97. package/dist/services/run-controller.js +779 -0
  98. package/dist/services/run-engine-bindings.js +1 -1
  99. package/dist/services/run-fork.js +138 -0
  100. package/dist/services/run-observations.js +112 -0
  101. package/dist/services/run-recovery-runtime.js +69 -0
  102. package/dist/services/run-recovery.js +343 -0
  103. package/dist/services/runs.js +368 -74
  104. package/dist/services/runtime-budget.js +434 -0
  105. package/dist/services/runtime-scope-capture.js +50 -0
  106. package/dist/services/runtime-scope-control.js +450 -0
  107. package/dist/services/runtime-scope-resume.js +565 -0
  108. package/dist/services/runtime-scope-stop.js +99 -0
  109. package/dist/services/runtime-scope-worker.js +243 -0
  110. package/dist/services/runtime-service.js +97 -0
  111. package/dist/services/schema-validation.js +10 -8
  112. package/dist/services/sessions.js +35 -69
  113. package/dist/services/stage-blocker.js +17 -7
  114. package/dist/services/stage-context.js +44 -18
  115. package/dist/services/stage-lifecycle.js +99 -102
  116. package/dist/services/stage-pause.js +69 -28
  117. package/dist/services/usage.js +37 -93
  118. package/dist/services/vnext-code-review.js +189 -85
  119. package/dist/services/vnext-code.js +257 -243
  120. package/dist/services/vnext-execution-profile.js +5 -3
  121. package/dist/services/vnext-fanout.js +134 -19
  122. package/dist/services/vnext-merge.js +285 -102
  123. package/dist/services/vnext-plan-review.js +148 -94
  124. package/dist/services/vnext-plan.js +141 -72
  125. package/dist/services/vnext-protocolize.js +129 -56
  126. package/dist/services/vnext-specify.js +87 -64
  127. package/dist/services/work-registry.js +701 -164
  128. package/dist/services/workspace-bootstrap.js +76 -0
  129. package/dist/services/worktrees.js +85 -35
  130. package/dist/shared/entity-references.js +8 -0
  131. package/dist/shared/errors.js +12 -0
  132. package/dist/storage/database.js +444 -32
  133. package/dist/storage/paths.js +17 -4
  134. package/dist/storage/work-references.js +23 -0
  135. package/dist/storage/writer-contract.js +77 -0
  136. package/dist/storage/writer-migration.js +102 -0
  137. package/package.json +11 -4
  138. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,43 +1,66 @@
1
- import crypto from "node:crypto";
2
- import { spawn } from "node:child_process";
1
+ import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { AppError } from "../shared/errors.js";
6
6
  import { resolveProjectRoot } from "../storage/paths.js";
7
- import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
7
+ import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
8
8
  import { requireProjectByRoot } from "./projects.js";
9
- import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
9
+ import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
10
10
  import { validateSchema } from "./schema-validation.js";
11
11
  import { flowCommand } from "./stage-pause.js";
12
12
  import { validateVnextCodeHandoff } from "./vnext-plan.js";
13
13
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
14
14
  import { readVnextSpecifyResult } from "./vnext-specify.js";
15
- import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
15
+ import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
16
16
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
17
17
  import { writeStageReport } from "./stage-report-renderer.js";
18
- import { ensureVnextMergeRequest } from "./vnext-merge.js";
18
+ import { prepareVnextMergeRequest } from "./vnext-merge.js";
19
19
  import { assertStageStartHookEvent } from "./hooks.js";
20
- import { applyExternalStageContext } from "./stage-context.js";
20
+ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
21
21
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
22
+ import { assertRunMutationAllowed } from "./run-recovery.js";
23
+ import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
22
24
  const stage = "code";
23
25
  const stageDir = vnextStageDirectory("code");
24
- export async function startVnextCode(context, input) {
26
+ /** A controller prepares slow, deterministic project tooling before it asks an
27
+ * agent to enter CODE or a restored CODE_REVIEW. The subsequent lifecycle tool call remains the sole
28
+ * authority for attaching and binding the Stage. */
29
+ export async function prepareVnextCodeWorkspace(context, input) {
30
+ if (context.env.DD_FLOW_CODE_READINESS_OWNER !== input.ownerId) {
31
+ throw new AppError("code_readiness_owner_mismatch", "CODE readiness preparation must use its controller's frozen owner", 1, { owner_id: input.ownerId });
32
+ }
25
33
  const projectRoot = resolveProjectRoot(input.projectRoot);
26
34
  const project = requireProjectByRoot(context, projectRoot);
27
35
  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 } : {}) });
36
+ assertRunMutationAllowed(context, project.id, run.id);
37
+ requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, stage: input.stage ?? stage });
38
+ const root = path.join(requireHome(run), stageDir);
39
+ fs.mkdirSync(root, { recursive: true });
40
+ return ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress, { force: input.stage !== "code-review" });
41
+ }
42
+ export function prepareVnextCodeStart(context, input) {
43
+ const projectRoot = resolveProjectRoot(input.projectRoot);
44
+ const project = requireProjectByRoot(context, projectRoot);
45
+ const run = requireRun(context, project.id, input.runId);
46
+ assertRunMutationAllowed(context, project.id, run.id);
29
47
  const home = requireHome(run);
30
48
  requireVnextWorkspaceRoute({ projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
31
49
  const existingRoot = path.join(home, stageDir);
32
50
  const existingPrompt = path.join(existingRoot, "stage-prompt.md");
33
51
  const existingStage = stageStatus(run, stage);
52
+ const sourceRepairAttempt = isMergeSourceRepairAttempt(run);
53
+ const rootWork = requireRootWork(context, project.id, input.runId);
54
+ if (input.externalContext)
55
+ prepareExternalStageContext({ stageRoot: existingRoot, loaded: input.externalContext });
56
+ prepareOutputFile(existingPrompt);
57
+ prepareOutputFile(path.join(existingRoot, "fanout.json"));
58
+ const common = { projectRoot, project, run, home, root: existingRoot, promptPath: existingPrompt, rootWork, sourceRepairAttempt };
34
59
  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")) } };
60
+ return { ...common, resumed: true, existingStage, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(existingRoot) };
40
61
  }
62
+ if (!sourceRepairAttempt)
63
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
41
64
  const batch = path.join(home, "03-plan", "code-work-batch.json");
42
65
  const handoffFailures = validateVnextCodeHandoff(context, {
43
66
  projectRoot,
@@ -57,12 +80,25 @@ export async function startVnextCode(context, input) {
57
80
  runRoot: home
58
81
  });
59
82
  validateWorkBatchFile(batch);
60
- const root = path.join(home, stageDir);
83
+ const template = fs.readFileSync(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
84
+ const variables = getFlowRunVariables(context, { projectRoot, runId: run.id });
85
+ return { ...common, resumed: false, template, capacity: variables.variables?.["runtime.subagent.available_slots"] };
86
+ }
87
+ export async function startVnextCode(context, input, prepared = prepareVnextCodeStart(context, input)) {
88
+ const { projectRoot, project, run, root, promptPath, rootWork, sourceRepairAttempt } = prepared;
89
+ assertRunMutationAllowed(context, project.id, run.id);
90
+ assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
91
+ if (prepared.resumed) {
92
+ const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
93
+ bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
94
+ const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
95
+ 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: `${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")) } };
96
+ }
61
97
  fs.mkdirSync(root, { recursive: true });
62
98
  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 });
99
+ // Graph rendering may issue managed child commands; it belongs to execution.
100
+ const graph = codeWorkGraph(context, project.id, run.id);
101
+ const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph });
66
102
  fs.writeFileSync(promptPath, prompt);
67
103
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
68
104
  const binding = bindStageCoordinatorWork(context, {
@@ -73,15 +109,16 @@ export async function startVnextCode(context, input) {
73
109
  resultPath: path.join(root, "stage-report.json"),
74
110
  ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
75
111
  });
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);
112
+ if (!sourceRepairAttempt) {
113
+ attachFlowRunStage(context, {
114
+ projectRoot,
115
+ runId: input.runId,
116
+ stage,
117
+ dir: stageDir,
118
+ status: "running",
119
+ dataSchemaId: "dd-flow/stage-report@1"
120
+ });
121
+ }
85
122
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
86
123
  appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
87
124
  return {
@@ -105,8 +142,10 @@ export async function finishVnextCode(context, input) {
105
142
  const run = requireRun(context, project.id, input.runId);
106
143
  const home = requireHome(run);
107
144
  const root = path.join(home, stageDir);
145
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
108
146
  requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
109
147
  if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
148
+ 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
149
  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
150
  }
112
151
  const handoffFailures = validateVnextCodeHandoff(context, {
@@ -135,7 +174,8 @@ export async function finishVnextCode(context, input) {
135
174
  throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
136
175
  }
137
176
  // 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 });
177
+ const verification = input.verification ?? verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
178
+ validateSchema({ schemaName: "code-verification", file: input.verificationFile, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
139
179
  if (verification.verdict === "blocked") {
140
180
  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
181
  }
@@ -149,34 +189,21 @@ export async function finishVnextCode(context, input) {
149
189
  originWorkIds: works.filter((work) => work.status === "completed").map((work) => work.work_id),
150
190
  semanticUnresolved: verification.unresolved.length ? verification.unresolved : [verification.summary],
151
191
  verificationPath: input.verificationFile,
192
+ verification,
152
193
  objective: verification.summary
153
194
  });
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." };
195
+ 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
196
  }
156
197
  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
- }
198
+ if (input.retryCheckId) {
199
+ validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
200
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
201
+ }
202
+ const receipts = await runCodeChecks(context, {
203
+ projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, scope: "aggregate", checks,
204
+ ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}),
205
+ ...(input.progress ? { progress: input.progress } : {})
206
+ });
180
207
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
181
208
  if (failed.length) {
182
209
  throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
@@ -184,23 +211,21 @@ export async function finishVnextCode(context, input) {
184
211
  outcome: "repair_required",
185
212
  retry_after_workspace_change: true,
186
213
  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`
214
+ 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`,
215
+ retry_command: finishCommand(context, run.id, projectRoot, input.verificationFile, failed[0].id)
188
216
  });
189
217
  }
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 });
218
+ const finalReceipts = receipts;
219
+ const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: finalReceipts, runId: run.id, runHome: home });
193
220
  const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
194
221
  if (unresolvedAcceptance.length)
195
222
  throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
196
223
  const now = context.now();
197
224
  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 });
225
+ // Work result paths are provenance, not a mutable filesystem oracle. The
226
+ // stage reports one shared Git observation; ignored machine repairs are
227
+ // rejected before a Work can become completed.
228
+ const changedPaths = workspaceChangedPaths(run.workspace_root);
204
229
  const next = nextAction(run, changedPaths);
205
230
  const report = {
206
231
  schema_id: "dd-flow/stage-report@1",
@@ -239,31 +264,41 @@ export async function finishVnextCode(context, input) {
239
264
  // Creating the MERGE request is part of accepting this terminal handoff.
240
265
  // Do it before materialising a terminal stage report or changing RUN state,
241
266
  // 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)
267
+ let mergeRequest = null;
268
+ assertRunMutationAllowed(context, project.id, run.id, generation);
269
+ const prepareMerge = next === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
270
+ withStageSettlement(context, () => {
271
+ assertRunMutationAllowed(context, project.id, run.id, generation);
272
+ const currentWorks = codeWorks(context, project.id, run.id);
273
+ if (currentWorks.length !== works.length || currentWorks.some(work => work.status !== "completed" || !works.some(accepted => accepted.work_id === work.work_id && accepted.result === work.result))) {
274
+ throw new AppError("code_graph_incomplete", "CODE results changed while the final checks were running", 2);
275
+ }
276
+ mergeRequest = prepareMerge?.() ?? null;
277
+ writeReport(root, report);
278
+ completeFlowRunStage(context, {
279
+ projectRoot,
280
+ runId: run.id,
281
+ stage,
282
+ status: "done",
283
+ data: "stage-report.json",
284
+ dataSchemaId: "dd-flow/stage-report@1",
285
+ report: "stage-report.md",
286
+ stageReport: "stage-report.html"
287
+ });
288
+ const rootWork = requireRootWork(context, project.id, run.id);
289
+ appendFlowRunTimelineEvent(context, project.id, run.id, {
290
+ type: "code_accepted",
291
+ work_id: rootWork.work_id,
292
+ checks: finalReceipts.map((receipt) => receipt.id)
293
+ });
294
+ if (next === "code_completed") {
295
+ finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
296
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
297
+ }
298
+ else {
299
+ advanceFlowRun(context, { settlementStage: "code", projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
300
+ }
259
301
  });
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
302
  return {
268
303
  ok: true,
269
304
  run_id: run.id,
@@ -271,19 +306,44 @@ export async function finishVnextCode(context, input) {
271
306
  outcome: "accepted",
272
307
  report_path: path.join(root, "stage-report.json"),
273
308
  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` } } : {})
309
+ ...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context, `${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: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`) } } : {})
275
310
  };
276
311
  }
277
- export function addVnextCodeRepair(context, input) {
312
+ export function prepareVnextCodeRepair(context, input) {
278
313
  const projectRoot = resolveProjectRoot(input.projectRoot);
279
314
  const project = requireProjectByRoot(context, projectRoot);
280
315
  const run = requireRun(context, project.id, input.runId);
281
316
  const home = requireHome(run);
282
- if (!input.objective.trim())
317
+ if (input.reviewCycle) {
318
+ const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle);
319
+ if (previous)
320
+ return { kind: "replay", project, run, previous };
321
+ }
322
+ if (typeof input.objective !== "string" || !input.objective.trim())
283
323
  throw new AppError("validation", "Repair objective must not be empty", 2);
284
- if (!input.checkReceiptId && !input.reviewFindingIds?.length && !input.semanticUnresolved?.length) {
324
+ if (input.semanticUnresolved !== undefined && (!Array.isArray(input.semanticUnresolved) || input.semanticUnresolved.some(value => typeof value !== "string" || !value.trim())))
325
+ throw new AppError("validation", "Semantic unresolved items must be nonempty strings", 2);
326
+ if (input.reviewFindings !== undefined && !Array.isArray(input.reviewFindings))
327
+ throw new AppError("validation", "Review findings must be an array", 2);
328
+ if (input.reviewChecks !== undefined && !Array.isArray(input.reviewChecks))
329
+ throw new AppError("validation", "Review checks must be an array", 2);
330
+ if (!Array.isArray(input.originWorkIds) || !input.originWorkIds.length || input.originWorkIds.some(id => typeof id !== "string" || !id.trim()))
331
+ throw new AppError("validation", "Repair requires at least one origin Work", 2);
332
+ if (!input.checkReceiptId && !input.reviewFindings?.length && !input.semanticUnresolved?.length) {
285
333
  throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
286
334
  }
335
+ if ([Boolean(input.checkReceiptId), Boolean(input.reviewFindings?.length), Boolean(input.semanticUnresolved?.length)].filter(Boolean).length !== 1)
336
+ throw new AppError("validation", "Repair requires exactly one evidence source", 2);
337
+ if (input.semanticUnresolved?.length) {
338
+ if (!input.verificationPath)
339
+ throw new AppError("usage", "Semantic repair requires a verification file", 2);
340
+ const verification = input.verification ?? readVerification(context, { file: input.verificationPath, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
341
+ if (input.verification !== undefined)
342
+ validateSchema({ schemaName: "code-verification", file: input.verificationPath, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
343
+ const unresolved = verification.unresolved.length ? verification.unresolved : [verification.summary];
344
+ if (verification.verdict !== "needs_repair" || input.semanticUnresolved.some(value => !unresolved.includes(value)))
345
+ throw new AppError("validation", "Semantic repair must reference unresolved items from a needs_repair verification", 2);
346
+ }
287
347
  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
348
  project.id,
289
349
  input.checkReceiptId
@@ -304,10 +364,10 @@ export function addVnextCodeRepair(context, input) {
304
364
  // repair that fixes one failed aggregate check can make a second, already
305
365
  // declared aggregate failure impossible to repair. The immutable source
306
366
  // of repair eligibility is the original CODE graph.
307
- const declaredChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
367
+ const declaredChecks = effectiveCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
308
368
  .map((work) => packet(work))
309
369
  .filter((value) => !value.repair)
310
- .flatMap((value) => value.checks));
370
+ .flatMap((value) => value.checks), ["work", "code", "readiness", "merge"]);
311
371
  const failedCheck = receipt ? declaredChecks.find((check) => check.id === receipt.declaration_id) : undefined;
312
372
  if (receipt && !failedCheck) {
313
373
  throw new AppError("repair_check_declaration_missing", "Repair receipt is not backed by an accepted CODE check declaration", 2, {
@@ -316,7 +376,7 @@ export function addVnextCodeRepair(context, input) {
316
376
  });
317
377
  }
318
378
  const semanticRepair = Boolean(input.semanticUnresolved?.length);
319
- const key = input.reviewFindingIds?.length ? "code-review-repair" : semanticRepair ? "code-verification-repair" : "code-gate-repair";
379
+ const key = input.reviewFindings?.length ? "code-review-repair" : semanticRepair ? "code-verification-repair" : "code-gate-repair";
320
380
  const receiptWriteScope = receipt ? receiptRepairPaths(run.workspace_root, receipt) : [];
321
381
  const repair = {
322
382
  schema_id: "dd-flow/code-work-packet@5",
@@ -326,8 +386,12 @@ export function addVnextCodeRepair(context, input) {
326
386
  repair: {
327
387
  origin_work_ids: origins.map((work) => work.work_id),
328
388
  ...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
329
- ...(input.reviewFindingIds?.length ? { review_finding_ids: unique(input.reviewFindingIds), review_evidence_refs: unique(input.reviewEvidenceRefs ?? []), review_check_refs: unique(input.reviewCheckRefs ?? (input.reviewChecks ?? []).map((check) => check.id)) } : {}),
330
- ...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {})
389
+ ...(input.reviewFindings?.length ? { review_findings: input.reviewFindings } : {}),
390
+ ...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {}),
391
+ // `run_at` remains the accepted aggregate placement. This separate
392
+ // repair obligation is deliberately executed before this repair Work is
393
+ // accepted, so a local fix cannot close without re-proving its cause.
394
+ verification_check_refs: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) })
331
395
  },
332
396
  task: input.objective,
333
397
  semantic_spine: {
@@ -335,14 +399,14 @@ export function addVnextCodeRepair(context, input) {
335
399
  component_responsibility: "Diagnose and repair the failed accepted CODE result without changing unrelated behavior.",
336
400
  must_preserve: unique(invariantPackets.flatMap((value) => value.semantic_spine.must_preserve)),
337
401
  non_goals: unique(invariantPackets.flatMap((value) => value.semantic_spine.non_goals)),
338
- acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : semanticRepair ? `Resolve CODE verification gap(s): ${input.semanticUnresolved.join("; ")}` : `Resolve CODE-REVIEW finding(s): ${input.reviewFindingIds.join(", ")}`
402
+ acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : semanticRepair ? `Resolve CODE verification gap(s): ${input.semanticUnresolved.join("; ")}` : `Resolve CODE-REVIEW finding(s): ${input.reviewFindings.map((finding) => finding.finding_ref).join(", ")}`
339
403
  },
340
404
  requirements: uniqueBy(invariantPackets.flatMap((value) => value.requirements), (value) => value.id),
341
405
  acceptance: uniqueBy(invariantPackets.flatMap((value) => value.acceptance), (value) => JSON.stringify(value)),
342
406
  // A CODE-REVIEW repair changes delivered code/evidence, never the
343
407
  // already accepted PLAN or its ownership declaration.
344
408
  document_updates: [],
345
- required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewEvidenceRefs ?? []), ...(semanticRepair && input.verificationPath ? [input.verificationPath] : []), ...packets.flatMap((value) => value.required_read)]),
409
+ required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewFindings?.flatMap((finding) => finding.evidence_refs) ?? []), ...(semanticRepair && input.verificationPath ? [input.verificationPath] : []), ...packets.flatMap((value) => value.required_read)]),
346
410
  discovery_boundary: unique(packets.flatMap((value) => value.discovery_boundary)),
347
411
  // These are collision-avoidance hints. Receipt paths enrich the coordinator
348
412
  // picture but never restrict the repair's project-local edits.
@@ -357,34 +421,32 @@ export function addVnextCodeRepair(context, input) {
357
421
  ...(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
422
  ]),
359
423
  depends_on: origins.map((work) => work.work_id),
360
- result_schema: "dd-flow/code-work-result@2"
424
+ result_schema: "dd-flow/code-work-result@3",
425
+ ...(input.reviewCycle ? { review_cycle: input.reviewCycle } : {})
361
426
  };
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
- }
427
+ const parentWorkId = requireRootWork(context, project.id, run.id).work_id;
428
+ const items = prepareWorkBatch(context, { parentWorkId, value: { works: [repair] } });
429
+ return { kind: "create", project, run, input, parentWorkId, items, key, receipt };
430
+ }
431
+ export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRepair(context, input)) {
432
+ const { project, run } = prepared;
433
+ if (prepared.kind === "replay")
434
+ return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous), reused: true };
435
+ const { parentWorkId, items, key, receipt } = prepared;
436
+ const registered = addWorkBatch(context, { parentWorkId, prepared: items });
372
437
  const id = String(registered.work_ids[key]);
373
438
  const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]);
374
439
  appendFlowRunTimelineEvent(context, project.id, run.id, {
375
440
  type: "code_repair_created",
376
441
  work_id: id,
377
- origin_work_ids: input.originWorkIds,
378
- ...(receipt ? { check_receipt_id: receipt.id } : input.reviewFindingIds?.length ? { review_finding_ids: input.reviewFindingIds } : { semantic_unresolved: input.semanticUnresolved })
442
+ origin_work_ids: prepared.input.originWorkIds,
443
+ ...(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
444
  });
380
445
  refreshRunWorkProjection(context, project.id, run.id);
381
446
  return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
382
447
  }
383
- /** Retain the causal declaration for worker context; run_at keeps aggregate gates at stage scope. */
448
+ /** Retain the causal declaration for worker context; run_at still records its original gate. */
384
449
  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
450
  const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
389
451
  const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
390
452
  return uniqueBy(checks, (check) => check.id);
@@ -414,10 +476,7 @@ function receiptRepairPaths(workspaceRoot, receipt) {
414
476
  return [...paths].sort();
415
477
  }
416
478
  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");
479
+ const { graph, capacity, template } = input;
421
480
  return [
422
481
  "<stage_identity>",
423
482
  `- RUN: ${input.run.id}`,
@@ -439,8 +498,8 @@ function coordinatorPrompt(context, input) {
439
498
  "</code_graph>",
440
499
  "",
441
500
  "<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.",
501
+ "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: " + `${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`,
502
+ "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. Its repair_command only creates the repair Work; use it with the required origin Work IDs, then end the Turn. Do not invoke that new 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.",
444
503
  `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.`,
445
504
  `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
505
  "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.",
@@ -462,76 +521,6 @@ function coordinatorPrompt(context, input) {
462
521
  export function profileCommands(workspaceRoot) {
463
522
  return aggregateCheckDeclarations(workspaceRoot).map((check) => check.command);
464
523
  }
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
524
  /**
536
525
  * The immutable Work result is a repair history. The stage report is a current
537
526
  * acceptance projection, so a registered receipt from an older check epoch
@@ -545,8 +534,8 @@ export function verificationProjection(works, receipts, options = {}) {
545
534
  for (const work of works) {
546
535
  const result = readJsonResult(work.result);
547
536
  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 ?? [])]));
537
+ if (item.obligation_ref)
538
+ rawEvidence.set(item.obligation_ref, unique([...(rawEvidence.get(item.obligation_ref) ?? []), ...(item.refs ?? [])]));
550
539
  }
551
540
  const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
552
541
  const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
@@ -561,18 +550,16 @@ export function verificationProjection(works, receipts, options = {}) {
561
550
  return [item.criterion_id, { evidenceRefs, historical }];
562
551
  }));
563
552
  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));
553
+ const isPassed = (checkId) => receiptByDeclaration.get(checkId)?.status === "passed";
565
554
  return {
566
555
  checks: declarations.map((check) => {
567
556
  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 };
557
+ 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
558
  }),
571
559
  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);
560
+ const confirmed = item.check_refs.every((id) => isPassed(id)) && hasEvidence(item.criterion_id);
574
561
  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 } : {}) };
562
+ 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
563
  })
577
564
  };
578
565
  }
@@ -642,69 +629,72 @@ function requireRun(context, projectId, runId) {
642
629
  return run;
643
630
  }
644
631
  /** CODE, not PROTOCOLIZE, makes a checkout ready for project tooling. */
645
- async function ensureCodeWorkspaceReady(context, projectId, run, root, readinessChecks, progress) {
632
+ export function isReusablePreparedCodeReadiness(value, expected) {
633
+ if (!value || typeof value !== "object" || Array.isArray(value))
634
+ return false;
635
+ const receipt = value;
636
+ return receipt.schema_id === "dd-flow/workspace-readiness@2"
637
+ && receipt.status === "passed"
638
+ && receipt.prepared_by === expected.ownerId
639
+ && receipt.workspace_root === expected.workspaceRoot
640
+ && receipt.command === expected.command
641
+ && receipt.policy_ref === expected.policyRef;
642
+ }
643
+ function readPreparedReadiness(file) {
644
+ try {
645
+ return JSON.parse(fs.readFileSync(file, "utf8"));
646
+ }
647
+ catch {
648
+ return null;
649
+ }
650
+ }
651
+ async function ensureCodeWorkspaceReady(context, projectId, run, root, readinessChecks, progress, options = {}) {
646
652
  const profile = JSON.parse(run.index_json);
647
653
  const bootstrap = profile.execution_profile?.settings?.code_bootstrap;
648
654
  if (!bootstrap || typeof bootstrap.command !== "string" || typeof bootstrap.policy_ref !== "string") {
649
655
  throw new AppError("execution_profile_invalid", "CODE requires a frozen bootstrap command in the RUN execution profile", 1, { run_id: run.id });
650
656
  }
651
657
  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 };
658
+ const ownerId = context.env.DD_FLOW_CODE_READINESS_OWNER;
659
+ if (!options.force && ownerId && isReusablePreparedCodeReadiness(readPreparedReadiness(receiptPath), {
660
+ ownerId,
661
+ workspaceRoot: run.workspace_root,
662
+ command: bootstrap.command,
663
+ policyRef: bootstrap.policy_ref
664
+ })) {
665
+ progress?.("workspace bootstrap already prepared by this controller");
666
+ return readPreparedReadiness(receiptPath);
667
+ }
668
+ // Unowned and stale receipts are never trusted: a direct stage entry still
669
+ // performs its own readiness check. Only the same controller can hand a
670
+ // freshly prepared workspace to its immediately following lifecycle call.
655
671
  const startedAt = new Date().toISOString();
656
672
  progress?.(`workspace bootstrap started: ${bootstrap.command}`);
657
- const result = await runBootstrap(bootstrap.command, run.workspace_root, codeExecutionEnvironment(run.workspace_root), progress);
673
+ 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
674
  const bootstrapPassed = result.exit_code === 0 && !result.error;
659
675
  const checks = bootstrapPassed && readinessChecks.length
660
676
  ? 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
677
  : [];
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 };
678
+ 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
679
  fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
664
680
  if (receipt.status !== "passed")
665
681
  throw new AppError("workspace_readiness_failed", "CODE workspace bootstrap failed", 1, receipt);
666
682
  progress?.("workspace bootstrap passed");
667
683
  return receipt;
668
684
  }
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
685
  function requireHome(run) {
697
686
  if (!run.run_root)
698
687
  throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1);
699
688
  return run.run_root;
700
689
  }
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`;
690
+ export function finishCommand(context, runId, projectRoot, verificationFile, retryCheckId) {
691
+ const retry = retryCheckId ? ` --retry-check ${retryCheckId} --reason "<environment recovery evidence>"` : "";
692
+ return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`);
703
693
  }
704
694
  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"));
695
+ return prepareJsonSchema(input.file, "CODE verification", {
696
+ schemaName: "code-verification", projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot
697
+ }).value;
708
698
  }
709
699
  function verificationForFinish(context, input) {
710
700
  return readVerification(context, input);
@@ -726,6 +716,8 @@ export function writeProtocolFlowStatus(workspaceRoot, runHome, runId, status) {
726
716
  }
727
717
  function nextAction(run, changedPaths) {
728
718
  const index = JSON.parse(run.index_json);
719
+ if (index.variables?.["merge.source_repair"])
720
+ return "start_code_review";
729
721
  const requested = index.settings?.code_review?.mode ?? "auto";
730
722
  const enabled = requested === "off" ? false : requested === "auto" ? changedPaths.length > 0 : true;
731
723
  const stopTarget = index.execution_profile?.settings?.stop_target;
@@ -741,19 +733,19 @@ function stageStatus(run, name) {
741
733
  return null;
742
734
  }
743
735
  }
744
- function changedPathsFromReport(file) {
736
+ function isMergeSourceRepairAttempt(run) {
745
737
  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") : [];
738
+ const value = JSON.parse(run.index_json);
739
+ return Boolean(value.variables?.["merge.source_repair"]);
748
740
  }
749
741
  catch {
750
- return [];
742
+ return false;
751
743
  }
752
744
  }
753
- function resultPaths(result) {
745
+ function changedPathsFromReport(file) {
754
746
  try {
755
- const value = JSON.parse(result ?? "");
756
- return Array.isArray(value.changed_paths) ? value.changed_paths.filter((item) => typeof item === "string") : [];
747
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
748
+ return Array.isArray(value.semantic?.changed_files) ? value.semantic.changed_files.filter((item) => typeof item === "string") : [];
757
749
  }
758
750
  catch {
759
751
  return [];
@@ -772,6 +764,28 @@ function uniqueBy(values, key) {
772
764
  return true;
773
765
  });
774
766
  }
767
+ /** Repair pre-atomic terminal stages without repeating accepted checks or rewinding a successor. */
768
+ export function resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next) {
769
+ const index = JSON.parse(run.index_json);
770
+ if ((index.current_stage ?? index.stage_runs.at(-1)?.stage) !== stage)
771
+ return;
772
+ const terminal = next === "code_completed" || next === "code_review_completed";
773
+ if (!terminal && index.next_action === next)
774
+ return;
775
+ withStageSettlement(context, () => {
776
+ if (!terminal) {
777
+ advanceFlowRun(context, { settlementStage: stage, projectRoot, runId: run.id, status: "running", verdict: `${stage.replaceAll("-", "_")}_completed`, nextAction: next });
778
+ return;
779
+ }
780
+ 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]);
781
+ if (!work)
782
+ throw new AppError("runtime_missing", "Accepted stage has no root coordinator", 1);
783
+ if (work.status !== "completed")
784
+ finishStageCoordinatorWork(context, work.work_id, JSON.stringify(report, null, 2));
785
+ if (index.status !== "done")
786
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: next });
787
+ });
788
+ }
775
789
  function writeReport(root, report) {
776
790
  writeStageReport(root, report);
777
791
  }