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

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 (68) hide show
  1. package/dist/build-info.json +6 -6
  2. package/dist/cli/command-inputs.js +300 -0
  3. package/dist/cli/help.js +1 -1
  4. package/dist/cli/hook-ingress.js +82 -0
  5. package/dist/cli/input-preparation.js +115 -0
  6. package/dist/cli/run-cli.js +1015 -423
  7. package/dist/cli.js +6 -2
  8. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  9. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
  10. package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
  11. package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
  12. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
  13. package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
  14. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  15. package/dist/services/canon.js +11 -8
  16. package/dist/services/cleanup.js +49 -8
  17. package/dist/services/cli-operation-classifier.js +12 -24
  18. package/dist/services/codex-hook-delivery.js +28 -0
  19. package/dist/services/config.js +11 -8
  20. package/dist/services/controller-fanout.js +2 -2
  21. package/dist/services/dashboard.js +94 -31
  22. package/dist/services/engines.js +75 -61
  23. package/dist/services/eval-snapshots.js +138 -38
  24. package/dist/services/execution-policy.js +5 -12
  25. package/dist/services/hooks.js +226 -203
  26. package/dist/services/lanes.js +60 -52
  27. package/dist/services/lifecycle-command.js +13 -1
  28. package/dist/services/lifecycle-invocations.js +285 -94
  29. package/dist/services/managed-processes.js +158 -18
  30. package/dist/services/merge-queue.js +173 -98
  31. package/dist/services/migrations.js +13 -8
  32. package/dist/services/plan-runtime.js +19 -8
  33. package/dist/services/plans.js +28 -25
  34. package/dist/services/projects.js +11 -2
  35. package/dist/services/prompts.js +23 -16
  36. package/dist/services/protocols.js +54 -19
  37. package/dist/services/recovery-observation-budget.js +1 -1
  38. package/dist/services/run-controller-adapter.js +31 -20
  39. package/dist/services/run-controller-state.js +5 -0
  40. package/dist/services/run-controller.js +102 -56
  41. package/dist/services/run-fork.js +36 -22
  42. package/dist/services/run-recovery.js +32 -17
  43. package/dist/services/runs.js +110 -38
  44. package/dist/services/runtime-budget.js +120 -48
  45. package/dist/services/runtime-scope-capture.js +2 -2
  46. package/dist/services/runtime-scope-control.js +2 -2
  47. package/dist/services/runtime-scope-resume.js +34 -12
  48. package/dist/services/runtime-scope-worker.js +49 -16
  49. package/dist/services/runtime-service.js +8 -7
  50. package/dist/services/schema-validation.js +9 -7
  51. package/dist/services/sessions.js +35 -69
  52. package/dist/services/stage-blocker.js +17 -7
  53. package/dist/services/stage-context.js +42 -16
  54. package/dist/services/stage-lifecycle.js +82 -100
  55. package/dist/services/stage-pause.js +54 -24
  56. package/dist/services/vnext-code-review.js +82 -32
  57. package/dist/services/vnext-code.js +78 -45
  58. package/dist/services/vnext-fanout.js +13 -12
  59. package/dist/services/vnext-merge.js +68 -35
  60. package/dist/services/vnext-plan-review.js +84 -41
  61. package/dist/services/vnext-plan.js +91 -70
  62. package/dist/services/vnext-protocolize.js +116 -50
  63. package/dist/services/vnext-specify.js +70 -57
  64. package/dist/services/work-registry.js +195 -92
  65. package/dist/services/worktrees.js +85 -35
  66. package/dist/storage/database.js +104 -10
  67. package/dist/storage/paths.js +17 -4
  68. package/package.json +1 -1
@@ -10,17 +10,18 @@ import { validateSchema } from "./schema-validation.js";
10
10
  import { flowCommand } from "./stage-pause.js";
11
11
  import { assertStageStartHookEvent } from "./hooks.js";
12
12
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
13
- import { addVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
13
+ import { addVnextCodeRepair, prepareVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
14
14
  import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
15
- import { addWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
16
- import { withStageSettlement } from "./runs.js";
15
+ import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
16
+ import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
17
17
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
18
18
  import { writeStageReport } from "./stage-report-renderer.js";
19
- import { applyExternalStageContext } from "./stage-context.js";
19
+ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
20
20
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
21
21
  import { prepareVnextMergeRequest } from "./vnext-merge.js";
22
22
  import { validateVnextCodeHandoff } from "./vnext-plan.js";
23
23
  import { assertRunMutationAllowed } from "./run-recovery.js";
24
+ import { prepareJsonFile, prepareOutputFile } from "../cli/input-preparation.js";
24
25
  const stage = "code-review";
25
26
  const stageDir = vnextStageDirectory(stage);
26
27
  const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
@@ -28,34 +29,57 @@ export function isVnextCodeReviewRun(context, input) {
28
29
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
29
30
  return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
30
31
  }
31
- export function startVnextCodeReview(context, input) {
32
+ export function prepareVnextCodeReviewStart(context, input) {
32
33
  const projectRoot = resolveProjectRoot(input.projectRoot);
33
34
  const project = requireProjectByRoot(context, projectRoot);
34
35
  const run = requireRun(context, project.id, input.runId);
35
36
  const home = requireHome(run);
36
- assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
37
+ assertRunMutationAllowed(context, project.id, run.id);
37
38
  requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
38
39
  if (!fs.existsSync(path.join(home, "05-code", "stage-report.json")))
39
- throw new AppError("not_found", "CODE-REVIEW requires an accepted CODE report", 1);
40
+ throw new AppError("runtime_artifact_missing", "CODE-REVIEW requires an accepted CODE report", 1);
40
41
  const root = path.join(home, stageDir);
41
- fs.mkdirSync(root, { recursive: true });
42
42
  const rootWork = requireRootWork(context, project.id, run.id);
43
43
  const prior = stageStatus(run, stage);
44
44
  const existingPrompt = path.join(root, "stage-prompt.md");
45
- if (prior && fs.existsSync(existingPrompt)) {
45
+ if (input.externalContext)
46
+ prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
47
+ prepareOutputFile(existingPrompt);
48
+ const common = { projectRoot, project, run, home, root, rootWork, existingPrompt };
49
+ if (prior && fs.existsSync(existingPrompt))
50
+ return { ...common, resumed: true, prior, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(root) };
51
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
52
+ const mode = effectiveMode(home, run);
53
+ const cycle = reviewCycle(run);
54
+ const codeReportSha256 = sha256File(path.join(home, "05-code", "stage-report.json"));
55
+ const groups = mode.mode === "off" ? [] : reviewGroups(home);
56
+ const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", review_cycle: cycle, group, read_only: true } })) };
57
+ const items = mode.mode === "off" ? [] : prepareWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch });
58
+ const template = mode.mode === "off" ? "" : read(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md"));
59
+ const checks = mode.mode === "off" ? [] : acceptedCodeChecks(context, project.id, run.id).map(({ id, purpose }) => ({ id, purpose }));
60
+ for (const name of ["review-context.json", ...(mode.mode === "off" ? ["decision.json"] : ["review-work-batch.json", "fanout.json"])])
61
+ prepareOutputFile(path.join(root, name));
62
+ return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks };
63
+ }
64
+ export function startVnextCodeReview(context, input, prepared = prepareVnextCodeReviewStart(context, input)) {
65
+ const { projectRoot, project, run, root, rootWork, existingPrompt } = prepared;
66
+ assertRunMutationAllowed(context, project.id, run.id);
67
+ assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
68
+ fs.mkdirSync(root, { recursive: true });
69
+ if (prepared.resumed) {
46
70
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
47
71
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
48
72
  const prompt = fs.readFileSync(existingPrompt, "utf8");
49
- const orchestration = readFanoutDescriptor(root);
50
- return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
73
+ const orchestration = prepared.orchestration;
74
+ return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
51
75
  }
52
- const mode = effectiveMode(home, run);
53
- const cycle = reviewCycle(run);
76
+ const { mode, cycle, groups, batch, items, template, checks, codeReportSha256 } = prepared;
54
77
  freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
55
78
  const promptPath = path.join(root, "stage-prompt.md");
56
79
  const reviewContextPath = path.join(root, "review-context.json");
57
- fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id } } : {}), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
80
+ fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id } } : {}), code_report_sha256: codeReportSha256 }, null, 2)}\n`);
58
81
  if (mode.mode === "off") {
82
+ fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
59
83
  const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n`;
60
84
  fs.writeFileSync(promptPath, prompt);
61
85
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
@@ -63,22 +87,23 @@ export function startVnextCodeReview(context, input) {
63
87
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
64
88
  return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
65
89
  }
66
- const groups = reviewGroups(home);
67
- const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", review_cycle: cycle, group, read_only: true } })) };
68
90
  const batchFile = path.join(root, "review-work-batch.json");
69
91
  fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
70
- const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups });
92
+ const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks });
71
93
  fs.writeFileSync(promptPath, prompt);
72
94
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
73
95
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
74
- const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, file: batchFile });
96
+ const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items });
75
97
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
76
98
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
77
99
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
78
100
  return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
79
101
  }
80
- /** Create a narrow CODE repair from accepted independent-review evidence. */
81
- export function addVnextCodeReviewRepair(context, input) {
102
+ export function prepareVnextCodeReviewRepair(context, input) {
103
+ if (!Array.isArray(input.findingIds) || !input.findingIds.length || input.findingIds.some(id => typeof id !== "string" || !id.trim()))
104
+ throw new AppError("validation", "Repair requires nonempty finding IDs", 2);
105
+ if (!input.checkRefsByFinding || typeof input.checkRefsByFinding !== "object" || Array.isArray(input.checkRefsByFinding) || Object.entries(input.checkRefsByFinding).some(([id, refs]) => !input.findingIds.includes(id) || !Array.isArray(refs) || refs.some(ref => typeof ref !== "string" || !ref.trim())))
106
+ throw new AppError("validation", "Repair check references must map selected findings to check IDs", 2);
82
107
  const projectRoot = resolveProjectRoot(input.projectRoot);
83
108
  const project = requireProjectByRoot(context, projectRoot);
84
109
  const run = requireRun(context, project.id, input.runId);
@@ -103,7 +128,7 @@ export function addVnextCodeReviewRepair(context, input) {
103
128
  if (unknown.length)
104
129
  throw new AppError("review_check_reference_unknown", "CODE-REVIEW finding cites a check absent from the accepted CODE handoff", 2, { check_refs: unknown });
105
130
  const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref));
106
- return addVnextCodeRepair(context, {
131
+ const repairInput = {
107
132
  projectRoot,
108
133
  runId: run.id,
109
134
  reviewFindings: selected.map(({ finding_ref, finding }) => ({ finding_ref, priority: finding.priority, problem: finding.problem, impact: finding.impact, required_outcome: finding.required_outcome, evidence_refs: finding.evidence_refs, obligation_refs: finding.obligation_refs, decision_reason: input.decisionReasonsByFinding?.[finding_ref] ?? input.objective, check_refs: input.checkRefsByFinding[finding_ref] ?? [] })),
@@ -111,7 +136,11 @@ export function addVnextCodeReviewRepair(context, input) {
111
136
  originWorkIds: origins,
112
137
  objective: input.objective,
113
138
  reviewCycle: reviewCycle(run)
114
- });
139
+ };
140
+ return { input: repairInput, value: prepareVnextCodeRepair(context, repairInput) };
141
+ }
142
+ export function addVnextCodeReviewRepair(context, input, prepared = prepareVnextCodeReviewRepair(context, input)) {
143
+ return addVnextCodeRepair(context, prepared.input, prepared.value);
115
144
  }
116
145
  export async function finishVnextCodeReview(context, input) {
117
146
  const projectRoot = resolveProjectRoot(input.projectRoot);
@@ -140,10 +169,14 @@ export async function finishVnextCodeReview(context, input) {
140
169
  const coordinator = context.db.get("SELECT session_id, result_path FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [rootWork.work_id]);
141
170
  if (!coordinator || path.resolve(coordinator.result_path ?? "") !== path.resolve(reportPath))
142
171
  throw new AppError("code_review_session_binding_conflict", "CODE-REVIEW can finish only after its successful stage start bound the coordinator Session and report path", 1, { run_id: run.id, expected_result_path: reportPath, actual_result_path: coordinator?.result_path ?? null });
143
- const decisionFile = input.decisionFile ?? path.join(root, "decision.json");
144
- if (!fs.existsSync(decisionFile) && mode === "off")
145
- fs.writeFileSync(decisionFile, `${JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] }, null, 2)}\n`);
146
- const decision = readCodeReviewDecision(decisionFile, reviewContext.accepted_decision, Boolean(input.decisionFile));
172
+ const decisionFile = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
173
+ const disabledDecision = disabledReviewDecision();
174
+ // The implicit review-off decision is the one deliberate no-file path. An
175
+ // explicitly supplied --decision-file is still caller input and must be
176
+ // readable before any lifecycle work can proceed.
177
+ const decision = mode === "off" && !input.decision && !input.decisionFile && !fs.existsSync(decisionFile)
178
+ ? disabledDecision
179
+ : readCodeReviewDecision(decisionFile, reviewContext.accepted_decision, Boolean(input.decisionFile), input.decision);
147
180
  validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
148
181
  const cycle = reviewContext.cycle ?? "initial";
149
182
  const reviewers = reviewerWorks(context, project.id, run.id, cycle);
@@ -158,7 +191,8 @@ export async function finishVnextCodeReview(context, input) {
158
191
  const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
159
192
  let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
160
193
  const frozenShaFile = path.join(root, ".decision-sha256");
161
- if (!reviewContext.accepted_decision && fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== sha256File(decisionFile))
194
+ const decisionHash = sha256Value(input.decisionText ?? JSON.stringify(decision));
195
+ if (!reviewContext.accepted_decision && fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== decisionHash)
162
196
  throw new AppError("review_decision_changed", "The accepted CODE-REVIEW decision cannot change during repair", 2, { decision_file: decisionFile });
163
197
  const fixIds = canonical.fix_ids;
164
198
  if (fixIds.length && repairs.length === 0) {
@@ -168,7 +202,7 @@ export async function finishVnextCodeReview(context, input) {
168
202
  fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
169
203
  fs.renameSync(temporary, contextFile);
170
204
  const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, decisionReasonsByFinding: Object.fromEntries(canonical.decision.findings.map((item) => [item.finding_ref, item.reason])), objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
171
- return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, instruction: "Run the returned repair Work in one fresh child session. When it completes, invoke next.finish_command; this review cycle uses the decision already accepted by the runtime.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
205
+ return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, instruction: "The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. When it completes, invoke next.finish_command; this review cycle uses the decision already accepted by the runtime.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
172
206
  }
173
207
  repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
174
208
  const incompleteRepairs = repairs.filter((work) => work.status !== "completed");
@@ -226,14 +260,29 @@ export async function finishVnextCodeReview(context, input) {
226
260
  });
227
261
  return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`) } } : {}) };
228
262
  }
229
- export function readCodeReviewDecision(file, accepted, explicit = false) {
263
+ export function readCodeReviewDecision(file, accepted, explicit = false, prepared) {
230
264
  if (!accepted)
231
- return readJson(file);
232
- if (explicit && fs.existsSync(file) && JSON.stringify(readJson(file)) !== JSON.stringify(accepted)) {
265
+ return prepared ?? prepareJsonFile(file, "CODE-REVIEW decision").value;
266
+ if ((prepared !== undefined || (explicit && fs.existsSync(file))) && JSON.stringify(prepared ?? prepareJsonFile(file, "CODE-REVIEW decision").value) !== JSON.stringify(accepted)) {
233
267
  throw new AppError("review_decision_changed", "This review cycle already accepted a different decision", 2, { decision_file: file });
234
268
  }
235
269
  return accepted;
236
270
  }
271
+ function disabledReviewDecision() {
272
+ return { schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] };
273
+ }
274
+ /** Retained decisions remain authoritative after their caller file is gone. */
275
+ export function prepareCodeReviewDecision(runRoot, file) {
276
+ const root = path.join(runRoot, "06-code-review");
277
+ const context = readJson(path.join(root, "review-context.json"));
278
+ const source = path.resolve(file ?? path.join(root, "decision.json"));
279
+ if (context.accepted_decision && !fs.existsSync(source))
280
+ return { value: context.accepted_decision };
281
+ if (context.mode === "off" && !file && !fs.existsSync(source))
282
+ return { value: disabledReviewDecision() };
283
+ const prepared = prepareJsonFile(source, "CODE-REVIEW decision");
284
+ return { value: readCodeReviewDecision(source, context.accepted_decision, Boolean(file), prepared.value), text: prepared.text };
285
+ }
237
286
  export function reviewGroups(home) {
238
287
  const expected = new Set(baselineAspects);
239
288
  const declared = [];
@@ -277,7 +326,7 @@ export function assertCodeReviewCoverage(groups, reviewers) {
277
326
  throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
278
327
  }
279
328
  export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Verify that universal and exclusive constraints (only, any, all, never, whole) and stated exceptions were not narrowed. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Evidence closes a claim only when it exercises the named failure mechanism; a sequential negative test does not prove a concurrency race, and proof limits cannot waive an accepted obligation. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
280
- function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. The shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract at the Work-graph boundary. Each reviewer Work must run in one fresh child session; the coordinator must never claim a reviewer Work itself. Start each ready Work with its exact start_command from the work graph. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Finish only after every reviewer Work settles and you have written ${decision}: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Run it, then call the returned next.finish_command; do not reuse an earlier invocation-id. Review is not repeated.", "If the post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then stop so the runner can dispatch the new repair. Do not edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
329
+ function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Write the decision to this exact absolute path, not a relative look-alike: ${decision}`, `Finish only after every reviewer Work settles and that exact file exists: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Do not invoke that Work's start_command from the coordinator Session: end this Turn so the controller can launch a fresh child, then call the returned next.finish_command after it completes. Internal invocation IDs are runtime-owned and must not be copied or reused. Review is not repeated.", "If the post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then end the Turn so the runner can dispatch the new repair. The repair command creates Work only; do not invoke its start_command in the coordinator Session. Do not edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
281
330
  function canonicalReviewEvidence(decision, reviewers) {
282
331
  const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
283
332
  const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
@@ -405,6 +454,7 @@ function effectiveMode(home, run) { const index = JSON.parse(run.index_json); if
405
454
  function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
406
455
  function changedPaths(workspaceRoot) { return workspaceChangedPaths(workspaceRoot); }
407
456
  function sha256File(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
457
+ function sha256Value(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
408
458
  function stageStatus(run, name) { try {
409
459
  return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === name)?.status ?? null;
410
460
  }
@@ -1,5 +1,4 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
- import crypto from "node:crypto";
3
2
  import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
4
3
  import fs from "node:fs";
5
4
  import path from "node:path";
@@ -7,20 +6,21 @@ import { AppError } from "../shared/errors.js";
7
6
  import { resolveProjectRoot } from "../storage/paths.js";
8
7
  import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
9
8
  import { requireProjectByRoot } from "./projects.js";
10
- import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
9
+ import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
11
10
  import { validateSchema } from "./schema-validation.js";
12
11
  import { flowCommand } from "./stage-pause.js";
13
12
  import { validateVnextCodeHandoff } from "./vnext-plan.js";
14
13
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
15
14
  import { readVnextSpecifyResult } from "./vnext-specify.js";
16
- import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
15
+ import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
17
16
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
18
17
  import { writeStageReport } from "./stage-report-renderer.js";
19
18
  import { prepareVnextMergeRequest } from "./vnext-merge.js";
20
19
  import { assertStageStartHookEvent } from "./hooks.js";
21
- import { applyExternalStageContext } from "./stage-context.js";
20
+ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
22
21
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
23
22
  import { assertRunMutationAllowed } from "./run-recovery.js";
23
+ import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
24
24
  const stage = "code";
25
25
  const stageDir = vnextStageDirectory("code");
26
26
  /** A controller prepares slow, deterministic project tooling before it asks an
@@ -39,25 +39,28 @@ export async function prepareVnextCodeWorkspace(context, input) {
39
39
  fs.mkdirSync(root, { recursive: true });
40
40
  return ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress, { force: input.stage !== "code-review" });
41
41
  }
42
- export async function startVnextCode(context, input) {
42
+ export function prepareVnextCodeStart(context, input) {
43
43
  const projectRoot = resolveProjectRoot(input.projectRoot);
44
44
  const project = requireProjectByRoot(context, projectRoot);
45
45
  const run = requireRun(context, project.id, input.runId);
46
- assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
46
+ assertRunMutationAllowed(context, project.id, run.id);
47
47
  const home = requireHome(run);
48
48
  requireVnextWorkspaceRoute({ projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
49
49
  const existingRoot = path.join(home, stageDir);
50
50
  const existingPrompt = path.join(existingRoot, "stage-prompt.md");
51
51
  const existingStage = stageStatus(run, stage);
52
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 };
53
59
  if (existingStage && fs.existsSync(existingPrompt)) {
54
- const externalContext = applyExternalStageContext({ stageRoot: existingRoot, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
55
- const prompt = fs.readFileSync(existingPrompt, "utf8");
56
- const rootWork = requireRootWork(context, project.id, input.runId);
57
- bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(existingRoot, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
58
- const orchestration = readFanoutDescriptor(existingRoot) ?? writeFanoutDescriptor(existingRoot, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
59
- 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) };
60
61
  }
62
+ if (!sourceRepairAttempt)
63
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
61
64
  const batch = path.join(home, "03-plan", "code-work-batch.json");
62
65
  const handoffFailures = validateVnextCodeHandoff(context, {
63
66
  projectRoot,
@@ -77,12 +80,25 @@ export async function startVnextCode(context, input) {
77
80
  runRoot: home
78
81
  });
79
82
  validateWorkBatchFile(batch);
80
- 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
+ }
81
97
  fs.mkdirSync(root, { recursive: true });
82
98
  const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
83
- const rootWork = requireRootWork(context, project.id, input.runId);
84
- const promptPath = path.join(root, "stage-prompt.md");
85
- 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 });
86
102
  fs.writeFileSync(promptPath, prompt);
87
103
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
88
104
  const binding = bindStageCoordinatorWork(context, {
@@ -103,7 +119,6 @@ export async function startVnextCode(context, input) {
103
119
  dataSchemaId: "dd-flow/stage-report@1"
104
120
  });
105
121
  }
106
- const graph = codeWorkGraph(context, project.id, input.runId);
107
122
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
108
123
  appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
109
124
  return {
@@ -159,7 +174,8 @@ export async function finishVnextCode(context, input) {
159
174
  throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
160
175
  }
161
176
  // Reject a malformed semantic receipt before running the expensive gate.
162
- 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 });
163
179
  if (verification.verdict === "blocked") {
164
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." };
165
181
  }
@@ -173,9 +189,10 @@ export async function finishVnextCode(context, input) {
173
189
  originWorkIds: works.filter((work) => work.status === "completed").map((work) => work.work_id),
174
190
  semanticUnresolved: verification.unresolved.length ? verification.unresolved : [verification.summary],
175
191
  verificationPath: input.verificationFile,
192
+ verification,
176
193
  objective: verification.summary
177
194
  });
178
- 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." };
179
196
  }
180
197
  const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
181
198
  if (input.retryCheckId) {
@@ -292,21 +309,41 @@ export async function finishVnextCode(context, input) {
292
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`) } } : {})
293
310
  };
294
311
  }
295
- export function addVnextCodeRepair(context, input) {
312
+ export function prepareVnextCodeRepair(context, input) {
296
313
  const projectRoot = resolveProjectRoot(input.projectRoot);
297
314
  const project = requireProjectByRoot(context, projectRoot);
298
315
  const run = requireRun(context, project.id, input.runId);
299
316
  const home = requireHome(run);
300
- if (!input.objective.trim())
301
- throw new AppError("validation", "Repair objective must not be empty", 2);
302
317
  if (input.reviewCycle) {
303
318
  const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle);
304
319
  if (previous)
305
- return { ok: true, run_id: run.id, repair_work_id: previous.work_id, start_command: workStartCommand(context, previous), reused: true };
320
+ return { kind: "replay", project, run, previous };
306
321
  }
322
+ if (typeof input.objective !== "string" || !input.objective.trim())
323
+ throw new AppError("validation", "Repair objective must not be empty", 2);
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);
307
332
  if (!input.checkReceiptId && !input.reviewFindings?.length && !input.semanticUnresolved?.length) {
308
333
  throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
309
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
+ }
310
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 = ?", [
311
348
  project.id,
312
349
  input.checkReceiptId
@@ -387,23 +424,23 @@ export function addVnextCodeRepair(context, input) {
387
424
  result_schema: "dd-flow/code-work-result@3",
388
425
  ...(input.reviewCycle ? { review_cycle: input.reviewCycle } : {})
389
426
  };
390
- const file = path.join(home, stageDir, `.repair-${crypto.randomUUID()}.json`);
391
- fs.mkdirSync(path.dirname(file), { recursive: true });
392
- fs.writeFileSync(file, `${JSON.stringify({ works: [repair] }, null, 2)}\n`);
393
- let registered;
394
- try {
395
- registered = addWorkBatch(context, { parentWorkId: requireRootWork(context, project.id, run.id).work_id, file });
396
- }
397
- finally {
398
- fs.rmSync(file, { force: true });
399
- }
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 });
400
437
  const id = String(registered.work_ids[key]);
401
438
  const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]);
402
439
  appendFlowRunTimelineEvent(context, project.id, run.id, {
403
440
  type: "code_repair_created",
404
441
  work_id: id,
405
- origin_work_ids: input.originWorkIds,
406
- ...(receipt ? { check_receipt_id: receipt.id } : input.reviewFindings?.length ? { review_finding_ids: input.reviewFindings.map((finding) => finding.finding_ref) } : { 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 })
407
444
  });
408
445
  refreshRunWorkProjection(context, project.id, run.id);
409
446
  return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
@@ -439,10 +476,7 @@ function receiptRepairPaths(workspaceRoot, receipt) {
439
476
  return [...paths].sort();
440
477
  }
441
478
  function coordinatorPrompt(context, input) {
442
- const graph = codeWorkGraph(context, input.run.project_id, input.run.id);
443
- const variables = getFlowRunVariables(context, { projectRoot: input.projectRoot, runId: input.run.id });
444
- const capacity = variables.variables?.["runtime.subagent.available_slots"];
445
- const template = fs.readFileSync(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
479
+ const { graph, capacity, template } = input;
446
480
  return [
447
481
  "<stage_identity>",
448
482
  `- RUN: ${input.run.id}`,
@@ -464,8 +498,8 @@ function coordinatorPrompt(context, input) {
464
498
  "</code_graph>",
465
499
  "",
466
500
  "<execution_commands>",
467
- "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 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, 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`,
468
- "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. Read that returned error and run its repair command; 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.",
469
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.`,
470
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"))}`,
471
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.",
@@ -658,10 +692,9 @@ export function finishCommand(context, runId, projectRoot, verificationFile, ret
658
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}`);
659
693
  }
660
694
  function readVerification(context, input) {
661
- const file = path.resolve(input.file);
662
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
663
- validateSchema({ schemaName: "code-verification", file, data, projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot });
664
- return data;
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;
665
698
  }
666
699
  function verificationForFinish(context, input) {
667
700
  return readVerification(context, input);
@@ -59,8 +59,10 @@ export function getVnextFanoutStatus(context, input) {
59
59
  const projectRoot = resolveProjectRoot(input.projectRoot);
60
60
  const project = requireProjectByRoot(context, projectRoot);
61
61
  const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
62
- if (!run?.run_root)
63
- throw new AppError("not_found", "RUN artifact root is unavailable", 1, { run_id: input.runId });
62
+ if (!run)
63
+ throw new AppError("not_found", "RUN is not registered", 1, { run_id: input.runId });
64
+ if (!run.run_root)
65
+ throw new AppError("runtime_artifact_missing", "RUN artifact root is unavailable", 1, { run_id: input.runId });
64
66
  const stageRoot = path.join(run.run_root, vnextStageDirectory(input.stage));
65
67
  const descriptor = readFanoutDescriptor(stageRoot);
66
68
  if (!descriptor)
@@ -93,15 +95,10 @@ export function getVnextFanoutStatus(context, input) {
93
95
  * Sessions need a provider execution identity: without one we report ambiguity
94
96
  * instead of applying a late event to the newest Work attempt. */
95
97
  export async function reconcileVnextFanout(context, input) {
98
+ const value = prepareNativeObservations(input.observations);
96
99
  const status = getVnextFanoutStatus(context, input);
97
100
  if (!status.orchestration)
98
101
  return { ok: true, issues: [], reconciled: [] };
99
- const value = input.observations;
100
- if (!value || typeof value.harness_id !== "string" || !value.harness_id || typeof value.parent_session_id !== "string" || !Array.isArray(value.children)
101
- || value.children.some(child => !child || typeof child.session_id !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(String(child.status))))
102
- throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
103
- if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
104
- throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
105
102
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
106
103
  const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
107
104
  const issues = [], reconciled = [];
@@ -157,13 +154,17 @@ export async function reconcileVnextFanout(context, input) {
157
154
  * evidence for an earlier Work, while an unbound terminal child is retained
158
155
  * as an inconsistency instead of being rediscovered in a later review wave.
159
156
  */
160
- export function observeVnextNativeChildren(context, input) {
161
- const value = input.observations;
162
- if (!value || typeof value.harness_id !== "string" || !value.harness_id || typeof value.parent_session_id !== "string" || !Array.isArray(value.children)
163
- || value.children.some(child => !child || typeof child.session_id !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(String(child.status))))
157
+ export function prepareNativeObservations(observations) {
158
+ const value = observations;
159
+ if (!value || typeof value.harness_id !== "string" || !value.harness_id.trim() || typeof value.parent_session_id !== "string" || !value.parent_session_id.trim() || !Array.isArray(value.children)
160
+ || value.children.some(child => !child || typeof child.session_id !== "string" || !child.session_id.trim() || typeof child.status !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(child.status)))
164
161
  throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
165
162
  if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
166
163
  throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
164
+ return { harness_id: value.harness_id, parent_session_id: value.parent_session_id, children: value.children };
165
+ }
166
+ export function observeVnextNativeChildren(context, input) {
167
+ const value = prepareNativeObservations(input.observations);
167
168
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
168
169
  const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
169
170
  const issues = [], history = [], pending = [];