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

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.
@@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process";
5
5
  import { AppError } from "../shared/errors.js";
6
6
  import { resolveProjectRoot } from "../storage/paths.js";
7
7
  import { requireProjectByRoot } from "./projects.js";
8
- import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
8
+ import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
9
9
  import { validateSchema } from "./schema-validation.js";
10
10
  import { flowCommand } from "./stage-pause.js";
11
11
  import { assertStageStartHookEvent } from "./hooks.js";
@@ -47,31 +47,32 @@ export function startVnextCodeReview(context, input) {
47
47
  const orchestration = readFanoutDescriptor(root);
48
48
  return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
49
49
  }
50
- const mode = effectiveMode(run, acceptedCodeChangedPaths(home));
50
+ const mode = effectiveMode(home, run);
51
+ freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
51
52
  const promptPath = path.join(root, "stage-prompt.md");
52
53
  const reviewContextPath = path.join(root, "review-context.json");
53
- fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", mode, workspace_fingerprint: workspaceFingerprint(run.workspace_root), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
54
- if (mode === "off") {
54
+ fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, workspace_fingerprint: workspaceFingerprint(run.workspace_root), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
55
+ if (mode.mode === "off") {
55
56
  const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n`;
56
57
  fs.writeFileSync(promptPath, prompt);
57
58
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
58
59
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
59
60
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
60
- return { ok: true, run_id: run.id, stage, mode, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
61
+ return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
61
62
  }
62
- const groups = reviewGroups(run, home, mode);
63
+ const groups = reviewGroups(home);
63
64
  const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", group, read_only: true } })) };
64
65
  const batchFile = path.join(root, "review-work-batch.json");
65
66
  fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
66
- const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode, groups });
67
+ const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups });
67
68
  fs.writeFileSync(promptPath, prompt);
68
69
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
69
70
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
70
71
  const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, file: batchFile });
71
72
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
72
73
  attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
73
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode, groups, registered });
74
- return { ok: true, run_id: run.id, stage, mode, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
74
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
75
+ return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
75
76
  }
76
77
  /** Create a narrow CODE repair from accepted independent-review evidence. */
77
78
  export function addVnextCodeReviewRepair(context, input) {
@@ -139,7 +140,7 @@ export async function finishVnextCodeReview(context, input) {
139
140
  const decisionFile = input.decisionFile ?? path.join(root, "decision.json");
140
141
  if (!fs.existsSync(decisionFile) && mode === "off")
141
142
  fs.writeFileSync(decisionFile, `${JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] }, null, 2)}\n`);
142
- validateSchema({ schemaName: "code-review-decision", file: decisionFile, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id });
143
+ validateSchema({ schemaName: "code-review-decision", file: decisionFile, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
143
144
  const decision = readJson(decisionFile);
144
145
  const reviewers = reviewerWorks(context, project.id, run.id);
145
146
  if (mode !== "off") {
@@ -204,21 +205,32 @@ export async function finishVnextCodeReview(context, input) {
204
205
  refreshRunWorkProjection(context, project.id, run.id);
205
206
  return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } } : {}) };
206
207
  }
207
- function reviewGroups(run, home, mode) {
208
- const aspectMapFiles = findFiles(path.join(home, "03-plan"), "aspect-map.json");
209
- const ids = new Set(baselineAspects);
210
- for (const file of aspectMapFiles) {
208
+ function reviewGroups(home) {
209
+ const expected = new Set(baselineAspects);
210
+ const declared = [];
211
+ for (const file of findFiles(path.join(home, "03-plan"), "aspect-map.json")) {
211
212
  const map = readJson(file);
212
213
  for (const aspect of map.aspects ?? [])
213
214
  if (aspect.applicability === "applicable" && aspect.aspect_id)
214
- ids.add(aspect.aspect_id);
215
+ expected.add(aspect.aspect_id);
216
+ for (const group of map.review_groups ?? [])
217
+ if (group.id && group.aspect_ids?.length)
218
+ declared.push({ key: `${map.protocol_id ?? path.basename(path.dirname(file))}/${group.id}`, aspect_ids: [...new Set(group.aspect_ids)] });
215
219
  }
216
- const all = [...ids];
217
- const chunk = mode === "deep" ? 1 : 3;
218
- return Array.from({ length: Math.ceil(all.length / chunk) }, (_, index) => ({ key: `group-${index + 1}`, aspect_ids: all.slice(index * chunk, (index + 1) * chunk) }));
220
+ if (!declared.length)
221
+ return [{ key: "baseline", aspect_ids: [...expected].sort() }];
222
+ const assigned = new Set();
223
+ for (const group of declared)
224
+ for (const aspectId of group.aspect_ids) {
225
+ if (assigned.has(aspectId))
226
+ throw new AppError("code_review_groups_invalid", "A CODE-REVIEW aspect appears in more than one accepted group", 2, { aspect_id: aspectId });
227
+ assigned.add(aspectId);
228
+ }
229
+ const missing = [...expected].filter((aspectId) => !assigned.has(aspectId)).sort();
230
+ return [...declared, ...(missing.length ? [{ key: "baseline", aspect_ids: missing }] : [])];
219
231
  }
220
- function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
221
- function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_home_path, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Each reviewer Work must run in one fresh child session. The coordinator must never claim a reviewer Work itself. Start each ready Work with its exact start_command from the work graph; it receives the task and result schema. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Finish only after every reviewer Work settles and you have written ${decision}: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Run it, then call the same Finish command again. Review is not repeated.", "If the post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then stop so the runner can dispatch the new repair. Do not edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
232
+ export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
233
+ function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Each reviewer Work must run in one fresh child session. The coordinator must never claim a reviewer Work itself. Start each ready Work with its exact start_command from the work graph; it receives the task and result schema. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Finish only after every reviewer Work settles and you have written ${decision}: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Run it, then call the same Finish command again. Review is not repeated.", "If the post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then stop so the runner can dispatch the new repair. Do not edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
222
234
  function canonicalReviewEvidence(decision, reviewers) {
223
235
  const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
224
236
  const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
@@ -337,14 +349,8 @@ function reviewFindingIds(work) { const repair = readJsonString(work.payload_jso
337
349
  function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
338
350
  return []; const refs = repair.review_check_refs; return Array.isArray(refs) ? refs.filter((value) => typeof value === "string") : []; }
339
351
  function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
340
- function effectiveMode(run, paths) { const index = JSON.parse(run.index_json); const requested = index.settings?.code_review?.mode ?? "auto"; return requested === "auto" ? (paths.length ? "standard" : "off") : requested; }
341
- function acceptedCodeChangedPaths(home) { try {
342
- const report = readJson(path.join(home, "05-code", "stage-report.json"));
343
- return Array.isArray(report.semantic?.changed_files) ? report.semantic.changed_files.filter((item) => typeof item === "string") : [];
344
- }
345
- catch {
346
- return [];
347
- } }
352
+ function effectiveMode(home, run) { const index = JSON.parse(run.index_json); const configured = index.settings?.code_review; const requested = configured?.mode ?? "auto"; if (requested !== "auto")
353
+ return { mode: requested, source: configured?.source === "user_instruction" ? "user_instruction" : "project_policy", reason: configured?.reason ?? "Frozen RUN configuration." }; const assessments = findFiles(path.join(home, "03-plan"), "plan.json").map((file) => readJson(file)); const deep = assessments.some((plan) => plan.assessment?.failure_impact?.level === "high" || plan.assessment?.solution_uncertainty?.level === "high"); return { mode: deep ? "deep" : "standard", source: "plan_assessment", reason: deep ? "Accepted PLAN assessment records high impact or uncertainty." : "Accepted PLAN assessment requires the standard independent review." }; }
348
354
  function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
349
355
  function changedPaths(workspaceRoot) { try {
350
356
  return execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: workspaceRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split(/\r?\n/).filter(Boolean).map((line) => line.slice(3).replace(/^.* -> /, ""));
@@ -368,10 +374,10 @@ catch {
368
374
  function runRef(runId, home, file) { const relative = path.relative(home, file).split(path.sep).join("/"); return relative && !relative.startsWith("../") ? `run://${runId}/${relative}` : file; }
369
375
  function requireRootWork(context, projectId, runId) { const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!work || work.status !== "running")
370
376
  throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
371
- function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, workspace_root, run_home_path, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
377
+ function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
372
378
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
373
- function requireHome(run) { if (!run.run_home_path)
374
- throw new AppError("runtime_missing", "RUN workspace is unavailable", 1); return run.run_home_path; }
379
+ function requireHome(run) { if (!run.run_root)
380
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
375
381
  function finishCommand(context, runId, projectRoot, decision) { return `${flowCommand(context)} stage finish ${runId} --stage code-review --decision-file ${JSON.stringify(decision)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`; }
376
382
  function findFiles(root, name) { if (!fs.existsSync(root))
377
383
  return []; const out = []; for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
@@ -53,7 +53,8 @@ export async function startVnextCode(context, input) {
53
53
  file: batch,
54
54
  projectRoot: run.workspace_root,
55
55
  ddFlowHome: context.ddFlowHome,
56
- runId: input.runId
56
+ runId: input.runId,
57
+ runRoot: home
57
58
  });
58
59
  validateWorkBatchFile(batch);
59
60
  const root = path.join(home, stageDir);
@@ -134,7 +135,7 @@ export async function finishVnextCode(context, input) {
134
135
  throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
135
136
  }
136
137
  // Reject a malformed semantic receipt before running the expensive gate.
137
- const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id });
138
+ const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
138
139
  if (verification.verdict === "blocked") {
139
140
  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." };
140
141
  }
@@ -188,7 +189,7 @@ export async function finishVnextCode(context, input) {
188
189
  }
189
190
  const allReceipts = checkReceipts(context, { projectId: project.id, runId: run.id });
190
191
  const finalReceipts = latestReceiptsByCommand(allReceipts);
191
- const projectedVerification = verificationProjection(works, finalReceipts);
192
+ const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: allReceipts, runId: run.id, runHome: home });
192
193
  const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
193
194
  if (unresolvedAcceptance.length)
194
195
  throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
@@ -531,19 +532,35 @@ function processAlive(pid) {
531
532
  return false;
532
533
  }
533
534
  }
534
- export function verificationProjection(works, receipts) {
535
+ /**
536
+ * The immutable Work result is a repair history. The stage report is a current
537
+ * acceptance projection, so a registered receipt from an older check epoch
538
+ * must never stand in for the receipt that currently proves the declaration.
539
+ */
540
+ export function verificationProjection(works, receipts, options = {}) {
535
541
  const packets = works.map((work) => packet(work)).filter((value) => value !== null);
536
542
  const declarations = uniqueBy(packets.flatMap((value) => value.checks), (value) => value.id);
537
543
  const receiptByDeclaration = new Map(receipts.flatMap((receipt) => receipt.check_refs.map((ref) => [ref, receipt])));
538
- const evidence = new Map();
544
+ const rawEvidence = new Map();
539
545
  for (const work of works) {
540
546
  const result = readJsonResult(work.result);
541
547
  for (const item of result.evidence ?? [])
542
548
  if (item.criterion_id)
543
- evidence.set(item.criterion_id, unique([...(evidence.get(item.criterion_id) ?? []), ...(item.refs ?? [])]));
549
+ rawEvidence.set(item.criterion_id, unique([...(rawEvidence.get(item.criterion_id) ?? []), ...(item.refs ?? [])]));
544
550
  }
545
551
  const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
546
- const hasEvidence = (criterionId) => (evidence.get(criterionId)?.length ?? 0) > 0;
552
+ const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
553
+ const acceptanceEvidence = new Map(acceptance.map((item) => {
554
+ const reported = rawEvidence.get(item.criterion_id) ?? [];
555
+ const current = unique(item.check_refs.flatMap((checkRef) => {
556
+ const receipt = receiptByDeclaration.get(checkRef);
557
+ return receipt?.status === "passed" ? [currentReceiptRef(receipt, options)] : [];
558
+ }));
559
+ const evidenceRefs = unique([...reported.filter((ref) => !historicalReceiptRefs.has(ref)), ...current]);
560
+ const historical = reported.filter((ref) => historicalReceiptRefs.has(ref) && !current.includes(ref));
561
+ return [item.criterion_id, { evidenceRefs, historical }];
562
+ }));
563
+ const hasEvidence = (criterionId) => (acceptanceEvidence.get(criterionId)?.evidenceRefs.length ?? 0) > 0;
547
564
  const isPassed = (checkId, criterionId) => receiptByDeclaration.get(checkId)?.status === "passed" || (declarations.find((check) => check.id === checkId)?.run_at === "external" && hasEvidence(criterionId));
548
565
  return {
549
566
  checks: declarations.map((check) => {
@@ -554,10 +571,20 @@ export function verificationProjection(works, receipts) {
554
571
  acceptance: acceptance.map((item) => {
555
572
  const external = item.check_refs.some((id) => declarations.find((check) => check.id === id)?.run_at === "external");
556
573
  const confirmed = item.check_refs.every((id) => isPassed(id, item.criterion_id)) && hasEvidence(item.criterion_id);
557
- 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.get(item.criterion_id) ?? [] };
574
+ 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 } : {}) };
558
576
  })
559
577
  };
560
578
  }
579
+ function currentReceiptRef(receipt, options) {
580
+ if (!options.runId || !options.runHome)
581
+ return receipt.receipt_path;
582
+ const relative = path.relative(options.runHome, receipt.receipt_path).split(path.sep).join("/");
583
+ return relative && !relative.startsWith("../") ? `run://${options.runId}/${relative}` : receipt.receipt_path;
584
+ }
585
+ function receiptReferences(receipt, options) {
586
+ return unique([receipt.receipt_path, currentReceiptRef(receipt, options)]);
587
+ }
561
588
  function readJsonResult(value) { try {
562
589
  const parsed = JSON.parse(value ?? "{}");
563
590
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -609,7 +636,7 @@ function requireRootWork(context, projectId, runId) {
609
636
  return work;
610
637
  }
611
638
  function requireRun(context, projectId, runId) {
612
- const run = context.db.get("SELECT id, project_id, run_home_path, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
639
+ const run = context.db.get("SELECT id, project_id, run_root, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
613
640
  if (!run)
614
641
  throw new AppError("not_found", "RUN is not registered", 1);
615
642
  return run;
@@ -667,16 +694,16 @@ function readReadiness(file) {
667
694
  }
668
695
  }
669
696
  function requireHome(run) {
670
- if (!run.run_home_path)
671
- throw new AppError("runtime_missing", "RUN workspace is unavailable", 1);
672
- return run.run_home_path;
697
+ if (!run.run_root)
698
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1);
699
+ return run.run_root;
673
700
  }
674
701
  function finishCommand(context, runId, projectRoot, verificationFile) {
675
702
  return `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`;
676
703
  }
677
704
  function readVerification(context, input) {
678
705
  const file = path.resolve(input.file);
679
- validateSchema({ schemaName: "code-verification", file, projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId });
706
+ validateSchema({ schemaName: "code-verification", file, projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot });
680
707
  return JSON.parse(fs.readFileSync(file, "utf8"));
681
708
  }
682
709
  function verificationForFinish(context, input) {
@@ -7,12 +7,6 @@ import { getFlowRunVariables } from "./runs.js";
7
7
  import { listWorks } from "./work-registry.js";
8
8
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
9
9
  export const subagentCapacityKey = "runtime.subagent.available_slots";
10
- export const capacityProbe = {
11
- fanout_size: 15,
12
- probe_hold_seconds: 60,
13
- cleanup_deadline_seconds: 180,
14
- completion_token: "AGENT-NN"
15
- };
16
10
  export function fanoutState(input) {
17
11
  if (!input.hasWork)
18
12
  return input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
@@ -42,10 +36,10 @@ export function readFanoutDescriptor(stageRoot) {
42
36
  export function getVnextFanoutStatus(context, input) {
43
37
  const projectRoot = resolveProjectRoot(input.projectRoot);
44
38
  const project = requireProjectByRoot(context, projectRoot);
45
- const run = context.db.get("SELECT run_home_path FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
46
- if (!run?.run_home_path)
47
- throw new AppError("not_found", "RUN workspace is unavailable", 1, { run_id: input.runId });
48
- const stageRoot = path.join(run.run_home_path, vnextStageDirectory(input.stage));
39
+ const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
40
+ if (!run?.run_root)
41
+ throw new AppError("not_found", "RUN artifact root is unavailable", 1, { run_id: input.runId });
42
+ const stageRoot = path.join(run.run_root, vnextStageDirectory(input.stage));
49
43
  const descriptor = readFanoutDescriptor(stageRoot);
50
44
  if (!descriptor)
51
45
  return { ok: true, run_id: input.runId, stage: input.stage, orchestration: null };
@@ -70,8 +64,7 @@ export function getVnextFanoutStatus(context, input) {
70
64
  state: fanoutState({ dispatch: descriptor.dispatch, hasWork, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
71
65
  capacity: {
72
66
  run_key: subagentCapacityKey,
73
- available_slots: capacityKnown ? available : null,
74
- ...(descriptor.capacity_required && !capacityKnown ? { probe: capacityProbe } : {})
67
+ available_slots: capacityKnown ? available : null
75
68
  },
76
69
  works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, start_command: work.start_command })) }
77
70
  }
@@ -1,3 +1,4 @@
1
+ import crypto from "node:crypto";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { execFileSync, spawnSync } from "node:child_process";
@@ -45,14 +46,16 @@ export function ensureVnextMergeRequest(context, input) {
45
46
  throw new AppError("merge_source_invalid", "MERGE could not determine the target branch", 1, { target_branch: targetBranch });
46
47
  const semantic = planChecks(run.workspace_root, protocols);
47
48
  validateMergeAcceptance(run.workspace_root, protocols, semantic);
48
- const effective = effectiveCheckDeclarations(run.workspace_root, semantic, ["merge"]);
49
+ const effective = effectiveCheckDeclarations(targetWorkspace, semantic, ["work", "code", "readiness", "merge"]);
49
50
  if (run.workspace_root !== targetWorkspace && effective.length === 0)
50
51
  throw new AppError("merge_gate_missing", "A real source/target integration requires at least one semantic or policy merge check", 2, { run_id: run.id, protocol_ids: protocols });
51
52
  const ignored = ignoredGitPaths(context, project.id);
52
53
  const accepted = input.acceptedPaths ? [...new Set(input.acceptedPaths.filter((item) => !ignored.includes(item)))].sort() : null;
53
54
  const freezeRoot = path.join(home, stageDir);
54
55
  const freezeFile = path.join(freezeRoot, "source-freeze.json");
56
+ const gateFile = path.join(freezeRoot, "merge-gate.json");
55
57
  fs.mkdirSync(freezeRoot, { recursive: true });
58
+ freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, profileHash: readCodeCheckProfile(targetWorkspace).hash, now: context.now() });
56
59
  let sourceCommit;
57
60
  let sourcePaths;
58
61
  if (fs.existsSync(freezeFile)) {
@@ -210,22 +213,11 @@ export async function finishVnextMerge(context, input) {
210
213
  if (unmerged(request.target_workspace).length)
211
214
  throw new AppError("merge_conflicts_unresolved", "Resolve every unmerged path before finishing MERGE", 2, { paths: unmerged(request.target_workspace) });
212
215
  const resultPath = path.join(requireHome(run), stageDir, "works", request.executor_work_id, "result.json");
213
- validateSchema({ schemaName: "merge-result", file: resultPath, projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id });
216
+ validateSchema({ schemaName: "merge-result", file: resultPath, projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireHome(run) });
214
217
  const semantic = JSON.parse(fs.readFileSync(resultPath, "utf8"));
215
218
  if (semantic.outcome !== "completed")
216
219
  throw new AppError("merge_semantic_blocked", "A blocked semantic result cannot complete MERGE", 2, { result_path: resultPath });
217
- if (!["integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(request.checkpoint)) {
218
- commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
219
- const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
220
- context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [commit, context.now(), request.merge_request_id]);
221
- request = requireRequest(context, request.merge_request_id);
222
- }
223
- else if (meaningfulStatus(request.target_workspace, ignoredGitPaths(context, project.id)).length) {
224
- commitIntegration(request.target_workspace, `${run.id}-fix`, ignoredGitPaths(context, project.id));
225
- context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [gitValue(request.target_workspace, ["rev-parse", "HEAD"]), context.now(), request.merge_request_id]);
226
- request = requireRequest(context, request.merge_request_id);
227
- }
228
- if (request.checkpoint === "integration_committed") {
220
+ if (request.checkpoint === "apply_recorded") {
229
221
  input.progress?.("bootstrapping integrated target");
230
222
  runBootstrap(run, request.target_workspace);
231
223
  context.db.run("UPDATE merge_requests SET checkpoint = 'bootstrap_ready', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
@@ -246,8 +238,19 @@ export async function finishVnextMerge(context, input) {
246
238
  const missingRefs = requiredRefs.filter((ref) => !passedRefs.has(ref));
247
239
  if (missingRefs.length)
248
240
  throw new AppError("merge_acceptance_unproven", "Current MERGE receipts do not cover every merge acceptance reference", 2, { missing_check_refs: missingRefs });
249
- context.db.run("UPDATE merge_requests SET checkpoint = 'checks_passed', status = 'active', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
241
+ const acceptedTree = gitValue(request.target_workspace, ["write-tree"]);
242
+ context.db.run("UPDATE merge_requests SET checkpoint = 'checks_passed', accepted_tree = ?, status = 'active', updated_at = ? WHERE merge_request_id = ?", [acceptedTree, context.now(), request.merge_request_id]);
250
243
  request = requireRequest(context, request.merge_request_id);
244
+ if (!request.integration_commit) {
245
+ commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
246
+ const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
247
+ const committedTree = gitValue(request.target_workspace, ["rev-parse", "HEAD^{tree}"]);
248
+ if (!request.accepted_tree || committedTree !== request.accepted_tree) {
249
+ return recovery(context, request, "merge_commit_tree_drift", { accepted_tree: request.accepted_tree, committed_tree: committedTree, integration_commit: commit });
250
+ }
251
+ context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [commit, context.now(), request.merge_request_id]);
252
+ request = requireRequest(context, request.merge_request_id);
253
+ }
251
254
  verifyLocalDelivery(request);
252
255
  context.db.run("UPDATE merge_requests SET checkpoint = 'delivered', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
253
256
  request = requireRequest(context, request.merge_request_id);
@@ -301,12 +304,28 @@ catch (error) {
301
304
  throw error;
302
305
  } refreshRunWorkProjection(context, request.project_id, request.run_id); return requestView(context, requireRequest(context, request.merge_request_id)); }
303
306
  function mergePrompt(context, input) { const pause = `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`; return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(executionSettings(input.run).merge_delivery)}`, `- cleanup: ${JSON.stringify(executionSettings(input.run).merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...effectiveMergeChecks(input.run, input.request).map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual integration conflicts in the integration workspace. Do not repeat merge apply.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, "If a check fails, inspect only the returned receipt/logs, repair the integrated target in this same Work, update the semantic result, and repeat the same finish command. Do not create a repair Work or rerun independent review.", "If a material conflict has no reasonable answer in accepted evidence, pause this same Work with the exact heredoc below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
304
- function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
305
- function effectiveMergeChecks(run, request) { return effectiveCheckDeclarations(request.target_workspace, planChecks(run.workspace_root, JSON.parse(request.protocol_ids_json)), ["merge"]); }
307
+ function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
308
+ function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
306
309
  function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
307
- return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.run_at === "merge").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
310
+ return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.availability === "available").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
308
311
  function validateMergeAcceptance(workspace, protocols, checks) { const known = new Set(checks.map((check) => check.canonical_ref)); const missing = mergeAcceptanceRefs(workspace, protocols).filter((ref) => !known.has(ref)); if (missing.length)
309
- throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a non-merge check", 2, { check_refs: missing }); }
312
+ throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a missing check", 2, { check_refs: missing }); }
313
+ function freezeMergeGate(file, input) { const canonical = JSON.stringify(input.checks); const hash = cryptoHash(canonical); if (fs.existsSync(file)) {
314
+ const existing = JSON.parse(fs.readFileSync(file, "utf8"));
315
+ if (existing.run_id !== input.runId || existing.checks_hash !== hash)
316
+ throw new AppError("merge_gate_freeze_conflict", "MERGE gate is already frozen with different checks", 2, { file });
317
+ return;
318
+ } fs.writeFileSync(file, `${JSON.stringify({ schema_id: "dd-flow/merge-gate@1", run_id: input.runId, protocols: input.protocols, checks: input.checks, checks_hash: hash, profile_hash: input.profileHash, frozen_at: input.now }, null, 2)}\n`); }
319
+ function readFrozenMergeGate(file, requestId) { try {
320
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
321
+ if (value.schema_id !== "dd-flow/merge-gate@1" || !Array.isArray(value.checks) || value.checks_hash !== cryptoHash(JSON.stringify(value.checks)))
322
+ throw new Error("invalid gate");
323
+ return { checks: value.checks };
324
+ }
325
+ catch (cause) {
326
+ throw new AppError("merge_gate_missing", "MERGE gate is missing or invalid", 1, { merge_request_id: requestId, file, cause: String(cause) });
327
+ } }
328
+ function cryptoHash(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
310
329
  function mergeAcceptanceRefs(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
311
330
  return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.acceptance ?? []).filter((entry) => entry.gate === "merge").flatMap((entry) => (entry.check_refs ?? []).map((ref) => `${protocol}/${ref}`)); }); }
312
331
  function protocolIds(home) { const root = path.join(home, "03-plan"); if (!fs.existsSync(root))
@@ -377,11 +396,11 @@ function requestForRun(context, projectId, runId) { const request = context.db.g
377
396
  throw new AppError("merge_request_missing", "MERGE request was not materialized by the prior terminal stage", 1, { run_id: runId }); return request; }
378
397
  function requireRequest(context, id) { const request = context.db.get("SELECT * FROM merge_requests WHERE merge_request_id = ?", [id]); if (!request)
379
398
  throw new AppError("not_found", "MERGE request is not registered", 1, { merge_request_id: id }); return request; }
380
- function requestView(context, request) { return { ok: true, merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source: { workspace: request.source_workspace, branch: request.source_branch, commit: request.source_commit }, target: { workspace: request.target_workspace, branch: request.target_branch, enqueue_head: request.enqueue_target_head, execution_head: request.execution_target_head, integration_commit: request.integration_commit }, route: request.execution_route, status: request.status, checkpoint: request.checkpoint, queue: queueStatus(context, request), created_at: request.created_at, completed_at: request.completed_at }; }
381
- function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_home_path, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
399
+ function requestView(context, request) { return { ok: true, merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source: { workspace: request.source_workspace, branch: request.source_branch, commit: request.source_commit }, target: { workspace: request.target_workspace, branch: request.target_branch, enqueue_head: request.enqueue_target_head, execution_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit }, route: request.execution_route, status: request.status, checkpoint: request.checkpoint, queue: queueStatus(context, request), created_at: request.created_at, completed_at: request.completed_at }; }
400
+ function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
382
401
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
383
- function requireHome(run) { if (!run.run_home_path)
384
- throw new AppError("runtime_missing", "RUN workspace is unavailable", 1); return run.run_home_path; }
402
+ function requireHome(run) { if (!run.run_root)
403
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
385
404
  function findRootWork(context, projectId, runId) { const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!work)
386
405
  throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return work; }
387
406
  function requireRootWork(context, projectId, runId) { const work = findRootWork(context, projectId, runId); if (work.status !== "running")