@deksden-com/dd-flow-cli 0.9.0-beta.49 → 0.9.0-beta.52

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.
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { createContext } from "../runtime/context.js";
5
5
  import { getResourceDatabase } from "../storage/database.js";
6
6
  import { AppError } from "../shared/errors.js";
7
- import { finishManagedProcess, managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
7
+ import { finishManagedProcess, managedLeaseReleased, managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
8
8
  import { requestRuntimeScopeControl, runtimeBudgetStatus, scopeLocalProcessRole, validateRuntimeScopeManifest } from "./runtime-budget.js";
9
9
  import { nativeInventoryVersion, recordRunControlIntent, retainedDaemonOperations, runControlStatus } from "./run-control.js";
10
10
  import { requestRunControl } from "./run-control-worker.js";
@@ -125,7 +125,7 @@ export function retainScopeDrain(context, control, processes, runs, native, prob
125
125
  }
126
126
  export function scopeRunProcessEvidence(record, runs, registryHome) {
127
127
  const metadata = JSON.parse(record.metadata_json);
128
- if (!record.pid || !record.pid_started_at || !record.project_id || !record.run_id || !metadata.dd_flow_home)
128
+ if ((!managedLeaseReleased(record) && (!record.pid || !record.pid_started_at)) || !record.project_id || !record.run_id || !metadata.dd_flow_home)
129
129
  return null;
130
130
  for (const run of runs) {
131
131
  if (run.runtime_home !== metadata.dd_flow_home || run.project_id !== record.project_id || run.run_id !== record.run_id)
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { runHarnessAdapter } from "./harness-adapter.js";
6
- import { managedProcessBinding, managedProcessStatus, processIsAlive, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
6
+ import { managedLeaseReleased, managedProcessBinding, managedProcessStatus, processIsAlive, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
7
7
  import { fenceRuntimeBudget, runtimeBudgetStatus, scopeLocalProcessRole } from "./runtime-budget.js";
8
8
  import { retainedDaemonOperations } from "./run-control.js";
9
9
  import { historicalDaemonSettlement } from "./native-daemon-history.js";
@@ -62,6 +62,10 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
62
62
  for (const record of runtimeBudgetStatus(context, scopeId).processes) {
63
63
  if (covered.has(record.id))
64
64
  continue;
65
+ if (managedLeaseReleased(record)) {
66
+ nodes.push({ process_id: record.id, settled: true });
67
+ continue;
68
+ }
65
69
  const metadata = JSON.parse(record.metadata_json);
66
70
  // RUN control owns active controllers/checks. Adopt only its already-finished
67
71
  // physical registrations here; death alone is not a completed RUN operation.
@@ -165,7 +165,7 @@ export function recalculateRunUsage(context, input) {
165
165
  const usageFinal = final && unsettledSessions.length === 0;
166
166
  const persistRunProjection = !input.sessionId && !input.stage;
167
167
  if (persistRunProjection) {
168
- context.db.exec("BEGIN IMMEDIATE");
168
+ context.db.exec("SAVEPOINT run_usage");
169
169
  try {
170
170
  context.db.run("DELETE FROM usage WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]);
171
171
  for (const row of rows) {
@@ -185,10 +185,11 @@ export function recalculateRunUsage(context, input) {
185
185
  AND status = 'idle'
186
186
  AND NOT EXISTS (SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running')`, [observedAt, session.completed_at, input.projectId, input.runId, session.session_id, session.session_id]);
187
187
  }
188
- context.db.exec("COMMIT");
188
+ context.db.exec("RELEASE run_usage");
189
189
  }
190
190
  catch (error) {
191
- context.db.exec("ROLLBACK");
191
+ context.db.exec("ROLLBACK TO run_usage");
192
+ context.db.exec("RELEASE run_usage");
192
193
  throw error;
193
194
  }
194
195
  }
@@ -10,15 +10,17 @@ import { validateSchema } from "./schema-validation.js";
10
10
  import { flowCommand } from "./stage-pause.js";
11
11
  import { assertStageStartHookEvent } from "./hooks.js";
12
12
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
13
- import { addVnextCodeRepair } from "./vnext-code.js";
14
- import { finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
15
- import { addWorkBatch, bindStageCoordinatorWork, finishWork, refreshRunWorkProjection } from "./work-registry.js";
13
+ import { addVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
14
+ import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
15
+ import { addWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
16
+ import { withStageSettlement } from "./runs.js";
16
17
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
17
18
  import { writeStageReport } from "./stage-report-renderer.js";
18
19
  import { applyExternalStageContext } from "./stage-context.js";
19
20
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
20
21
  import { ensureVnextMergeRequest } from "./vnext-merge.js";
21
22
  import { validateVnextCodeHandoff } from "./vnext-plan.js";
23
+ import { assertRunMutationAllowed } from "./run-recovery.js";
22
24
  const stage = "code-review";
23
25
  const stageDir = vnextStageDirectory(stage);
24
26
  const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
@@ -123,6 +125,7 @@ export async function finishVnextCodeReview(context, input) {
123
125
  const mode = reviewContext.mode;
124
126
  if (!mode)
125
127
  throw new AppError("runtime_missing", "CODE-REVIEW effective mode was not frozen at stage start", 1);
128
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
126
129
  if (reviewContext.code_report_sha256 !== sha256File(path.join(home, "05-code", "stage-report.json")))
127
130
  throw new AppError("code_review_input_changed", "Accepted CODE report changed after CODE-REVIEW start", 2);
128
131
  const handoffFailures = validateVnextCodeHandoff(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home });
@@ -131,7 +134,9 @@ export async function finishVnextCodeReview(context, input) {
131
134
  const reportPath = path.join(root, "stage-report.json");
132
135
  if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
133
136
  const report = readJson(reportPath);
134
- return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: report.semantic?.next_action ?? "code_review_completed" };
137
+ const next = report.semantic?.next_action ?? "code_review_completed";
138
+ resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next);
139
+ return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: next };
135
140
  }
136
141
  const rootWork = requireRootWork(context, project.id, run.id);
137
142
  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]);
@@ -145,6 +150,7 @@ export async function finishVnextCodeReview(context, input) {
145
150
  const cycle = reviewContext.cycle ?? "initial";
146
151
  const reviewers = reviewerWorks(context, project.id, run.id, cycle);
147
152
  if (mode !== "off") {
153
+ assertCodeReviewCoverage(reviewGroups(home), reviewers);
148
154
  const incomplete = reviewers.filter((work) => work.status !== "completed");
149
155
  if (incomplete.length)
150
156
  throw new AppError("reviewer_jobs_incomplete", "CODE-REVIEW cannot finish while reviewer Work is unsettled", 2, { works: incomplete.map((work) => ({ work_id: work.work_id, status: work.status })) });
@@ -173,13 +179,10 @@ export async function finishVnextCodeReview(context, input) {
173
179
  const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
174
180
  const finalChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }));
175
181
  if (input.retryCheckId) {
176
- validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: finalChecks, artifactRoot: path.join(requireHome(run), stageDir), reason: input.retryReason });
182
+ validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: finalChecks, reason: input.retryReason });
177
183
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
178
184
  }
179
- const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: finalChecks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId } : {}) });
180
- if (unchangedFailures.length)
181
- throw new AppError("code_review_gate_repair_required", "CODE-REVIEW final gate already failed for the unchanged workspace; choose source repair or restore the environment and retry that receipt", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures, retry_command: `${finishCommand(context, run.id, projectRoot, decisionFile)} --retry-check ${unchangedFailures[0].id} --reason "<environment recovery evidence>"` });
182
- const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.progress ? { progress: input.progress } : {}) });
185
+ const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}), ...(input.progress ? { progress: input.progress } : {}) });
183
186
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
184
187
  if (failed.length)
185
188
  throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
@@ -200,18 +203,21 @@ export async function finishVnextCodeReview(context, input) {
200
203
  const acceptedPaths = changedPaths(run.workspace_root);
201
204
  const mergeRequest = nextAction === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
202
205
  writeReport(root, report);
203
- completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
204
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
205
- if (nextAction === "start_merge")
206
- advanceFlowRun(context, { settlementStage: "code-review", projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
207
- else {
208
- await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
209
- completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
210
- }
211
- refreshRunWorkProjection(context, project.id, run.id);
206
+ withStageSettlement(context, () => {
207
+ assertRunMutationAllowed(context, project.id, run.id, generation);
208
+ completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
209
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
210
+ if (nextAction === "start_merge")
211
+ advanceFlowRun(context, { settlementStage: "code-review", projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
212
+ else {
213
+ finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
214
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
215
+ }
216
+ refreshRunWorkProjection(context, project.id, run.id);
217
+ });
212
218
  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` } } : {}) };
213
219
  }
214
- function reviewGroups(home) {
220
+ export function reviewGroups(home) {
215
221
  const expected = new Set(baselineAspects);
216
222
  const declared = [];
217
223
  for (const file of findFiles(path.join(home, "03-plan"), "aspect-map.json")) {
@@ -226,15 +232,33 @@ function reviewGroups(home) {
226
232
  if (!declared.length)
227
233
  return [{ key: "baseline", aspect_ids: [...expected].sort() }];
228
234
  const assigned = new Set();
235
+ const scoped = new Set();
229
236
  for (const group of declared)
230
237
  for (const aspectId of group.aspect_ids) {
231
- if (assigned.has(aspectId))
232
- throw new AppError("code_review_groups_invalid", "A CODE-REVIEW aspect appears in more than one accepted group", 2, { aspect_id: aspectId });
238
+ const assignment = `${group.key.slice(0, group.key.lastIndexOf("/"))}/${aspectId}`;
239
+ if (scoped.has(assignment))
240
+ throw new AppError("code_review_groups_invalid", "A CODE-REVIEW aspect appears in more than one group of the same protocol", 2, { aspect_id: aspectId });
241
+ scoped.add(assignment);
233
242
  assigned.add(aspectId);
234
243
  }
235
244
  const missing = [...expected].filter((aspectId) => !assigned.has(aspectId)).sort();
236
245
  return [...declared, ...(missing.length ? [{ key: "baseline", aspect_ids: missing }] : [])];
237
246
  }
247
+ export function assertCodeReviewCoverage(groups, reviewers) {
248
+ const expected = new Map(groups.map(group => [group.key, [...group.aspect_ids].sort()]));
249
+ if (!expected.size || expected.size !== groups.length)
250
+ throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW expected group identities must be nonempty and unique", 2);
251
+ const seen = new Set();
252
+ for (const work of reviewers) {
253
+ const group = readJsonString(work.payload_json).group;
254
+ if (!group?.key || seen.has(group.key) || !expected.has(group.key) || !Array.isArray(group.aspect_ids) || JSON.stringify([...group.aspect_ids].sort()) !== JSON.stringify(expected.get(group.key)))
255
+ throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW Work does not match its expected group", 2, { work_id: work.work_id });
256
+ seen.add(group.key);
257
+ }
258
+ const missing = [...expected.keys()].filter(key => !seen.has(key));
259
+ if (missing.length || !expected.size)
260
+ throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
261
+ }
238
262
  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.`; }
239
263
  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"); }
240
264
  function canonicalReviewEvidence(decision, reviewers) {
@@ -4,21 +4,22 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { AppError } from "../shared/errors.js";
6
6
  import { resolveProjectRoot } from "../storage/paths.js";
7
- import { aggregateCheckDeclarations, checkReceipts, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
7
+ import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
8
8
  import { requireProjectByRoot } from "./projects.js";
9
- import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
9
+ import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
10
10
  import { validateSchema } from "./schema-validation.js";
11
11
  import { flowCommand } from "./stage-pause.js";
12
12
  import { validateVnextCodeHandoff } from "./vnext-plan.js";
13
13
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
14
14
  import { readVnextSpecifyResult } from "./vnext-specify.js";
15
- import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
15
+ import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
16
16
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
17
17
  import { writeStageReport } from "./stage-report-renderer.js";
18
18
  import { ensureVnextMergeRequest } from "./vnext-merge.js";
19
19
  import { assertStageStartHookEvent } from "./hooks.js";
20
20
  import { applyExternalStageContext } from "./stage-context.js";
21
21
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
22
+ import { assertRunMutationAllowed } from "./run-recovery.js";
22
23
  const stage = "code";
23
24
  const stageDir = vnextStageDirectory("code");
24
25
  export async function startVnextCode(context, input) {
@@ -36,6 +37,7 @@ export async function startVnextCode(context, input) {
36
37
  const externalContext = applyExternalStageContext({ stageRoot: existingRoot, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
37
38
  const prompt = fs.readFileSync(existingPrompt, "utf8");
38
39
  const rootWork = requireRootWork(context, project.id, input.runId);
40
+ bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(existingRoot, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
39
41
  const orchestration = readFanoutDescriptor(existingRoot) ?? writeFanoutDescriptor(existingRoot, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
40
42
  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")) } };
41
43
  }
@@ -108,8 +110,10 @@ export async function finishVnextCode(context, input) {
108
110
  const run = requireRun(context, project.id, input.runId);
109
111
  const home = requireHome(run);
110
112
  const root = path.join(home, stageDir);
113
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
111
114
  requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
112
115
  if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
116
+ resumeAcceptedCodeStage(context, run, projectRoot, stage, JSON.parse(fs.readFileSync(path.join(root, "stage-report.json"), "utf8")), nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))));
113
117
  return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: path.join(root, "stage-report.json"), next_action: nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))) };
114
118
  }
115
119
  const handoffFailures = validateVnextCodeHandoff(context, {
@@ -158,32 +162,14 @@ export async function finishVnextCode(context, input) {
158
162
  }
159
163
  const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
160
164
  if (input.retryCheckId) {
161
- validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, artifactRoot: path.join(requireHome(run), stageDir), reason: input.retryReason });
165
+ validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
162
166
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
163
167
  }
164
- const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: checks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId } : {}) });
165
- if (unchangedFailures.length)
166
- throw new AppError("code_gate_repair_required", "CODE final gate already failed for the unchanged workspace; choose source repair or restore the environment and retry that receipt", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures, retry_command: `${finishCommand(context, run.id, projectRoot, input.verificationFile)} --retry-check ${unchangedFailures[0].id} --reason "<environment recovery evidence>"` });
167
- let receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks });
168
- if (!receipts) {
169
- const release = acquireAggregateGateLock(root);
170
- try {
171
- // A prior caller may have completed the slow gate while this caller was
172
- // waiting to acquire the lock. Reuse that immutable evidence.
173
- receipts = reusableAggregateGateReceipts(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, checks }) ?? await runCodeChecks(context, {
174
- projectId: project.id,
175
- runId: run.id,
176
- runHome: home,
177
- workspaceRoot: run.workspace_root,
178
- scope: "aggregate",
179
- checks,
180
- ...(input.progress ? { progress: input.progress } : {})
181
- });
182
- }
183
- finally {
184
- release();
185
- }
186
- }
168
+ const receipts = await runCodeChecks(context, {
169
+ projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, scope: "aggregate", checks,
170
+ ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}),
171
+ ...(input.progress ? { progress: input.progress } : {})
172
+ });
187
173
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
188
174
  if (failed.length) {
189
175
  throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
@@ -195,9 +181,8 @@ export async function finishVnextCode(context, input) {
195
181
  retry_command: `${finishCommand(context, run.id, projectRoot, input.verificationFile)} --retry-check ${failed[0].id} --reason "<environment recovery evidence>"`
196
182
  });
197
183
  }
198
- const allReceipts = checkReceipts(context, { projectId: project.id, runId: run.id });
199
- const finalReceipts = latestReceiptsByCommand(allReceipts);
200
- const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: allReceipts, runId: run.id, runHome: home });
184
+ const finalReceipts = receipts;
185
+ const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: finalReceipts, runId: run.id, runHome: home });
201
186
  const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
202
187
  if (unresolvedAcceptance.length)
203
188
  throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
@@ -249,29 +234,32 @@ export async function finishVnextCode(context, input) {
249
234
  // so an invalid merge policy leaves CODE safely running and retryable.
250
235
  const mergeRequest = next === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
251
236
  writeReport(root, report);
252
- completeFlowRunStage(context, {
253
- projectRoot,
254
- runId: run.id,
255
- stage,
256
- status: "done",
257
- data: "stage-report.json",
258
- dataSchemaId: "dd-flow/stage-report@1",
259
- report: "stage-report.md",
260
- stageReport: "stage-report.html"
261
- });
262
- const rootWork = requireRootWork(context, project.id, run.id);
263
- appendFlowRunTimelineEvent(context, project.id, run.id, {
264
- type: "code_accepted",
265
- work_id: rootWork.work_id,
266
- checks: finalReceipts.map((receipt) => receipt.id)
237
+ withStageSettlement(context, () => {
238
+ assertRunMutationAllowed(context, project.id, run.id, generation);
239
+ completeFlowRunStage(context, {
240
+ projectRoot,
241
+ runId: run.id,
242
+ stage,
243
+ status: "done",
244
+ data: "stage-report.json",
245
+ dataSchemaId: "dd-flow/stage-report@1",
246
+ report: "stage-report.md",
247
+ stageReport: "stage-report.html"
248
+ });
249
+ const rootWork = requireRootWork(context, project.id, run.id);
250
+ appendFlowRunTimelineEvent(context, project.id, run.id, {
251
+ type: "code_accepted",
252
+ work_id: rootWork.work_id,
253
+ checks: finalReceipts.map((receipt) => receipt.id)
254
+ });
255
+ if (next === "code_completed") {
256
+ finishStageCoordinatorWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
257
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
258
+ }
259
+ else {
260
+ advanceFlowRun(context, { settlementStage: "code", projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
261
+ }
267
262
  });
268
- if (next === "code_completed") {
269
- await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
270
- completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
271
- }
272
- else {
273
- advanceFlowRun(context, { settlementStage: "code", projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
274
- }
275
263
  return {
276
264
  ok: true,
277
265
  run_id: run.id,
@@ -477,76 +465,6 @@ function coordinatorPrompt(context, input) {
477
465
  export function profileCommands(workspaceRoot) {
478
466
  return aggregateCheckDeclarations(workspaceRoot).map((check) => check.command);
479
467
  }
480
- function latestReceiptsByCommand(receipts) {
481
- const latest = new Map();
482
- for (const receipt of receipts)
483
- for (const ref of receipt.check_refs)
484
- latest.set(ref, receipt);
485
- return [...new Set(latest.values())];
486
- }
487
- /**
488
- * A CODE finish may be retried after a client loses the long command's output.
489
- * Reusing complete, same-workspace receipts is correct; rerunning them is both
490
- * expensive and can race the still-settling original invocation.
491
- */
492
- export function reusableAggregateGateReceipts(context, input) {
493
- const expected = new Set(input.checks.map((check) => check.id));
494
- const current = workspaceFingerprint(input.workspaceRoot);
495
- const latest = new Map();
496
- for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId })) {
497
- if (receipt.scope === "aggregate")
498
- for (const ref of receipt.check_refs)
499
- if (expected.has(ref))
500
- latest.set(ref, receipt);
501
- }
502
- if (latest.size !== expected.size)
503
- return null;
504
- const receipts = input.checks.map((check) => latest.get(check.id));
505
- return receipts.every((receipt) => receipt.status === "passed" && receipt.workspace_fingerprint === current) ? receipts : null;
506
- }
507
- /** One aggregate gate per RUN; a second invocation must wait, never duplicate it. */
508
- function acquireAggregateGateLock(root) {
509
- const directory = path.join(root, "checks");
510
- const lock = path.join(directory, ".aggregate-gate.lock");
511
- fs.mkdirSync(directory, { recursive: true });
512
- try {
513
- const descriptor = fs.openSync(lock, "wx");
514
- fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
515
- fs.closeSync(descriptor);
516
- }
517
- catch (error) {
518
- if (!isExistingLock(error))
519
- throw error;
520
- const activePid = lockPid(lock);
521
- if (activePid && processAlive(activePid)) {
522
- throw new AppError("code_gate_in_progress", "CODE aggregate gate is already running; wait for that exact stage finish command to return", 1, { pid: activePid, lock });
523
- }
524
- fs.rmSync(lock, { force: true });
525
- return acquireAggregateGateLock(root);
526
- }
527
- return () => fs.rmSync(lock, { force: true });
528
- }
529
- function isExistingLock(error) {
530
- return Boolean(error && typeof error === "object" && error.code === "EEXIST");
531
- }
532
- function lockPid(lock) {
533
- try {
534
- const parsed = JSON.parse(fs.readFileSync(lock, "utf8"));
535
- return Number.isInteger(parsed.pid) && Number(parsed.pid) > 0 ? Number(parsed.pid) : null;
536
- }
537
- catch {
538
- return null;
539
- }
540
- }
541
- function processAlive(pid) {
542
- try {
543
- process.kill(pid, 0);
544
- return true;
545
- }
546
- catch {
547
- return false;
548
- }
549
- }
550
468
  /**
551
469
  * The immutable Work result is a repair history. The stage report is a current
552
470
  * acceptance projection, so a registered receipt from an older check epoch
@@ -770,6 +688,28 @@ function uniqueBy(values, key) {
770
688
  return true;
771
689
  });
772
690
  }
691
+ /** Repair pre-atomic terminal stages without repeating accepted checks or rewinding a successor. */
692
+ export function resumeAcceptedCodeStage(context, run, projectRoot, stage, report, next) {
693
+ const index = JSON.parse(run.index_json);
694
+ if ((index.current_stage ?? index.stage_runs.at(-1)?.stage) !== stage)
695
+ return;
696
+ const terminal = next === "code_completed" || next === "code_review_completed";
697
+ if (!terminal && index.next_action === next)
698
+ return;
699
+ withStageSettlement(context, () => {
700
+ if (!terminal) {
701
+ advanceFlowRun(context, { settlementStage: stage, projectRoot, runId: run.id, status: "running", verdict: `${stage.replaceAll("-", "_")}_completed`, nextAction: next });
702
+ return;
703
+ }
704
+ const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [run.project_id, run.id]);
705
+ if (!work)
706
+ throw new AppError("runtime_missing", "Accepted stage has no root coordinator", 1);
707
+ if (work.status !== "completed")
708
+ finishStageCoordinatorWork(context, work.work_id, JSON.stringify(report, null, 2));
709
+ if (index.status !== "done")
710
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: next });
711
+ });
712
+ }
773
713
  function writeReport(root, report) {
774
714
  writeStageReport(root, report);
775
715
  }