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

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.14
4
+
5
+ ### Patch Changes
6
+
7
+ - Audit runtime recovery: distinguish process owners from children, retain live check resources, await failed check startup cleanup, share managed workspace bootstrap between CODE/MERGE, correct assigned repair examples, and reject publication without canonical build provenance.
8
+
9
+ ## 0.9.0-beta.13
10
+
11
+ ### Patch Changes
12
+
13
+ - Make long-running CODE checks suspend-safe and prevent an expired lease from
14
+ stealing a live process. Publish `dd-flow/code-work-result@3`, which unifies
15
+ repair resolutions with evidence references assigned by the Work packet.
16
+
17
+ ## 0.9.0-beta.12
18
+
19
+ ### Patch Changes
20
+
21
+ - Return a failed integration gate to an explicit source CODE and independent
22
+ CODE-REVIEW repair cycle, instead of allowing product repairs in the
23
+ integration workspace.
24
+
3
25
  ## 0.9.0-beta.11
4
26
 
5
27
  ### Patch Changes
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.11",
4
- "cli_commit": "67fea9fdb3fbcc04cc0a45de9afc5f7120c13a08",
5
- "built_at": "2026-09-04T20:59:42.597Z",
3
+ "cli_version": "0.9.0-beta.14",
4
+ "cli_commit": "29b30887789f9ee686d5903a4ddcea40a24dc8ce",
5
+ "built_at": "2026-09-05T08:57:36.734Z",
6
6
  "built_with_canon": {
7
- "version": "4.0.2",
8
- "commit": "fccdb9fe7359f2ba321eebace328fd18557dcd25",
7
+ "version": "4.0.4",
8
+ "commit": "c8cae271f844fd3b3c7492f164e13a60eaba4839",
9
9
  "flow_contract": "dd-flow-canonical-2026-08",
10
10
  "repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
11
11
  "memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
package/dist/cli/help.js CHANGED
@@ -369,11 +369,12 @@ Usage:
369
369
  dd-flow merge request status <MRG-ID> --project-root <root> --json
370
370
  dd-flow merge request route <MRG-ID> --mode same_session|server --reason <text> --project-root <root> --json
371
371
  dd-flow merge apply <MRG-ID> --work <WRK-ID> --project-root <root> --json --progress-jsonl
372
+ dd-flow merge repair <MRG-ID> --project-root <root> --json
372
373
  dd-flow merge serve --agent-profile <id> [--once] [--poll-seconds <n>] [--max-parallel-projects <n>] --progress-jsonl
373
374
  dd-flow merge server status --json
374
375
  dd-flow merge server stop <MSV-ID> --json
375
376
 
376
- CODE or CODE-REVIEW creates one MRG request and child MERGE Work. stage start waits in the shared project FIFO, reports progress, locks the execution target baseline and returns the exact apply/finish commands. merge apply is the only supported initial Git mutation. merge serve is a deterministic dispatcher: it launches a fresh Session through the configured harness adapter but does not perform agent work itself. It preserves FIFO inside each project and may serve different projects concurrently up to the explicit bound.`
377
+ CODE or CODE-REVIEW creates one MRG request and child MERGE Work. stage start waits in the shared project FIFO, reports progress, locks the execution target baseline and returns the exact apply/finish commands. merge apply is the only supported initial Git mutation. If its integration gate fails, merge repair restores the target baseline and creates a source CODE → independent CODE-REVIEW → replacement-MRG cycle; it never patches product code in the integration workspace. merge serve is a deterministic dispatcher: it launches a fresh Session through the configured harness adapter but does not perform agent work itself. It preserves FIFO inside each project and may serve different projects concurrently up to the explicit bound.`
377
378
  ],
378
379
  [
379
380
  "cleanup",
@@ -39,7 +39,7 @@ import { finishVnextPlan, isVnextPlanRun, startVnextPlan } from "../services/vne
39
39
  import { dispatchVnextPlanReview, finishVnextPlanReview, isVnextPlanReviewRun, recordVnextPlanReviewCapacity, startVnextPlanReview } from "../services/vnext-plan-review.js";
40
40
  import { addVnextCodeRepair, finishVnextCode, startVnextCode } from "../services/vnext-code.js";
41
41
  import { addVnextCodeReviewRepair, finishVnextCodeReview, isVnextCodeReviewRun, startVnextCodeReview } from "../services/vnext-code-review.js";
42
- import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
42
+ import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
43
43
  import { mergeServerStatus, serveMergeRequests, stopMergeServer } from "../services/merge-server.js";
44
44
  import { getVnextFanoutStatus } from "../services/vnext-fanout.js";
45
45
  import { pauseStageForUser, resumeStageAfterUser } from "../services/stage-pause.js";
@@ -1247,6 +1247,8 @@ async function dispatchMerge(context, command, parsed, progress) {
1247
1247
  }
1248
1248
  if (command === "apply")
1249
1249
  return applyVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id"), workId: requiredOption(parsed, "work"), ...(progress ? { progress } : {}) });
1250
+ if (command === "repair")
1251
+ return await repairVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id") });
1250
1252
  if (command === "request") {
1251
1253
  const action = requiredPosition(parsed, 0, "request action");
1252
1254
  const requestId = requiredPosition(parsed, 1, "merge-request-id");
@@ -1589,6 +1591,7 @@ async function dispatchRuntime(context, command, parsed) {
1589
1591
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1590
1592
  const process = registerManagedProcess(context, {
1591
1593
  kind: requiredOption(parsed, "kind"), ownerId: requiredOption(parsed, "owner"),
1594
+ ownerPid: optionalPositiveNumber(parsed, "owner-pid", true),
1592
1595
  projectId: optionalOption(parsed, "project-id") ?? null, runId: optionalOption(parsed, "run") ?? null,
1593
1596
  workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
1594
1597
  operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
@@ -1599,7 +1602,7 @@ async function dispatchRuntime(context, command, parsed) {
1599
1602
  if (action === "confirm") {
1600
1603
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1601
1604
  const processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
1602
- return { ok: true, process: confirmManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId ? { processGroupId } : {}), ...(leaseMs ? { leaseMs } : {}) }) };
1605
+ return { ok: true, process: confirmManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId ? { processGroupId } : {}), ...(leaseMs ? { leaseMs } : {}) }) };
1603
1606
  }
1604
1607
  if (action === "heartbeat") {
1605
1608
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
@@ -2324,7 +2327,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2324
2327
  if (family === "merge-queue") {
2325
2328
  return projectRootForMergeQueueMutation(context, command, parsed);
2326
2329
  }
2327
- if (family === "merge" && ["one-shot", "apply", "request"].includes(command ?? "")) {
2330
+ if (family === "merge" && ["one-shot", "apply", "repair", "request"].includes(command ?? "")) {
2328
2331
  return requiredOption(parsed, "project-root");
2329
2332
  }
2330
2333
  if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
@@ -20,6 +20,6 @@
20
20
  "documentUpdate": {"type": "object", "additionalProperties": false, "required": ["path", "action", "owner", "reason", "baseline_sha256"], "properties": {"path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update"]}, "owner": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}, "baseline_sha256": {"type": ["string", "null"], "pattern": "^[a-f0-9]{64}$"}}},
21
21
  "reviewFinding": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "priority", "problem", "impact", "required_outcome", "evidence_refs", "obligation_refs", "decision_reason", "check_refs"], "properties": {"finding_ref": {"type": "string", "minLength": 1}, "priority": {"enum": ["p0", "p1", "p2", "p3"]}, "problem": {"type": "string", "minLength": 1}, "impact": {"type": "string", "minLength": 1}, "required_outcome": {"type": "string", "minLength": 1}, "evidence_refs": {"$ref": "#/$defs/strings"}, "obligation_refs": {"$ref": "#/$defs/strings"}, "decision_reason": {"type": "string", "minLength": 1}, "check_refs": {"$ref": "#/$defs/strings"}}},
22
22
  "repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_findings"]}, {"required": ["semantic_unresolved", "verification_path"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_findings": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/reviewFinding"}}, "semantic_unresolved": {"$ref": "#/$defs/strings"}, "verification_path": {"type": "string", "minLength": 1}}},
23
- "work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@2"}}}
23
+ "work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@3"}}}
24
24
  }
25
25
  }
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "dd-flow/code-work-result@2",
3
+ "$id": "dd-flow/code-work-result@3",
4
4
  "type": "object",
5
5
  "additionalProperties": false,
6
6
  "required": ["schema_id", "summary", "changed_paths", "evidence", "deviations", "blockers"],
7
7
  "properties": {
8
- "schema_id": {"const": "dd-flow/code-work-result@2"},
8
+ "schema_id": {"const": "dd-flow/code-work-result@3"},
9
9
  "summary": {"type": "string", "minLength": 1},
10
10
  "changed_paths": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
11
- "evidence": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["criterion_id", "refs"], "properties": {"criterion_id": {"type": "string", "minLength": 1}, "refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}},
11
+ "evidence": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["obligation_ref", "refs"], "properties": {"obligation_ref": {"type": "string", "minLength": 1}, "refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}},
12
12
  "deviations": {"type": "array", "items": {"type": "string", "minLength": 1}},
13
13
  "blockers": {"type": "array", "items": {"type": "string", "minLength": 1}},
14
14
  "resolutions": {"type": "array", "uniqueItems": true, "items": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "summary", "evidence_refs"], "properties": {"finding_ref": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}}
@@ -3,7 +3,7 @@ import { spawn, spawnSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { AppError } from "../shared/errors.js";
6
- import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, processIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
6
+ import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
7
7
  const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
8
8
  // Receipt reservation precedes child spawn so an interrupted CLI leaves an
9
9
  // auditable attempt. A second caller must not mistake that small handoff window
@@ -99,7 +99,7 @@ export async function runCodeChecks(context, input) {
99
99
  try {
100
100
  fs.mkdirSync(evidenceDir, { recursive: true });
101
101
  writeReceipt(receiptPath, receiptFor({ ...reserved, resources, result, status: "running", after: before, mutationPaths: [], artifacts: [] }));
102
- const managed = registerManagedProcess(context, { kind: "check", ownerId: `check:${id}`, projectId: input.projectId, runId: input.runId, workId: input.workId ?? null, checkId: id, stdoutPath, stderrPath, metadata: { command: group.command } });
102
+ const managed = registerManagedProcess(context, { kind: "check", ownerPid: process.pid, ownerId: `check:${id}`, projectId: input.projectId, runId: input.runId, workId: input.workId ?? null, checkId: id, stdoutPath, stderrPath, metadata: { command: group.command } });
103
103
  const allocation = await reservePorts(context, { ownerId: managed.owner_id, processId: managed.id, names: group.ports });
104
104
  resources = allocation.ports;
105
105
  input.progress?.(`check ${index + 1}/${groups.length} started: ${group.command}`);
@@ -109,13 +109,18 @@ export async function runCodeChecks(context, input) {
109
109
  result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), () => heartbeatManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token }));
110
110
  }
111
111
  finally {
112
- allocation.release();
113
112
  fs.closeSync(stdout);
114
113
  fs.closeSync(stderr);
115
- finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
116
114
  }
115
+ const current = managedProcessStatus(context).find(item => item.id === managed.id);
116
+ if (processTreeIsAlive(current))
117
+ throw new AppError("check_in_progress", "The check shell exited but its process tree is still live; retain this receipt and its resources until settlement", 2, { process_id: managed.id, check_receipt_id: id });
118
+ allocation.release();
119
+ finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
117
120
  }
118
121
  catch (error) {
122
+ if (error instanceof AppError && error.code === "check_in_progress")
123
+ throw error;
119
124
  result = { exitCode: null, error: error instanceof Error ? error.message : String(error), aborted: true };
120
125
  }
121
126
  const after = workspaceState(input.workspaceRoot, [input.runHome]);
@@ -155,7 +160,10 @@ export function reconcileUnfinishedChecks(context, input) {
155
160
  const process = processes.filter((item) => item.check_id === pendingReceipt.id).at(-1);
156
161
  const receipt = readReceipt(pendingReceipt.receipt_path);
157
162
  const completion = receipt ? readCompletion(receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json")) : null;
158
- if (process && ["starting", "running", "stopping"].includes(process.state) && processLeaseIsCurrent(process) && (process.state === "starting" || processIsAlive(process))) {
163
+ // A stale lease after host sleep is not permission to duplicate a living
164
+ // check. Its durable completion marker or explicit reconciliation decides
165
+ // the outcome before another invocation may start.
166
+ if (process && ["starting", "running", "stopping"].includes(process.state) && (processTreeIsAlive(process) || (process.state === "starting" && (managedOwnerIsAlive(process) !== false || Date.parse(process.lease_expires_at) > Date.parse(context.now()))))) {
159
167
  throw new AppError("check_in_progress", "A previous check invocation is still running", 1, { check_receipt_id: pendingReceipt.id, process_id: process.id, pid: process.pid });
160
168
  }
161
169
  if (!process && !completion && Date.now() - Date.parse(pendingReceipt.started_at) < receiptStartingGraceMs) {
@@ -212,7 +220,6 @@ function readCompletion(file) { try {
212
220
  catch {
213
221
  return null;
214
222
  } }
215
- function processLeaseIsCurrent(process) { return Date.parse(process.lease_expires_at ?? "") >= Date.now(); }
216
223
  export function unchangedFinalGateFailures(context, input) { const wanted = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)])); const current = workspaceFingerprint(input.workspaceRoot); const latest = new Map(); for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId }).filter((item) => item.scope === "aggregate" && item.check_refs.some((ref) => wanted.has(ref))))
217
224
  for (const ref of receipt.check_refs)
218
225
  latest.set(ref, receipt); return [...new Set(latest.values())].filter((receipt) => receipt.status === "failed" && receipt.before_fingerprint === current); }
@@ -267,34 +274,65 @@ function collectRequiredArtifacts(root, required) { const missing = []; const it
267
274
  } return { complete: missing.length === 0, missing, items }; }
268
275
  function listFiles(root, current = root) { return fs.readdirSync(current, { withFileTypes: true }).flatMap((entry) => { if ([".git", "node_modules"].includes(entry.name))
269
276
  return []; const absolute = path.join(current, entry.name); return entry.isDirectory() ? listFiles(root, absolute) : [path.relative(root, absolute)]; }).sort(); }
270
- function runCheck(command, cwd, environment, stdout, stderr, heartbeat, onSpawn, renewLease) { return new Promise((resolve) => { const started = Date.now(); const child = spawn("/bin/sh", ["-lc", checkShellScript()], { cwd, env: { ...environment, DD_FLOW_CHECK_COMMAND: command }, stdio: ["ignore", stdout, stderr], detached: process.platform !== "win32" }); if (!child.pid) {
271
- resolve({ exitCode: null, error: "check process did not provide a PID", aborted: true });
272
- return;
273
- } try {
274
- onSpawn(child.pid);
275
- }
276
- catch (error) {
277
- terminateProcessGroup(child.pid, "SIGTERM");
278
- resolve({ exitCode: null, error: error instanceof Error ? error.message : String(error), aborted: true });
279
- return;
280
- } const inactivityMs = 15 * 60 * 1000; let timeout; const lastOutputSize = new Map(); const armInactivityTimeout = () => { if (timeout)
281
- clearTimeout(timeout); timeout = setTimeout(() => { timedOut = true; terminateProcessGroup(child.pid, "SIGTERM"); escalation = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 5_000); }, inactivityMs); }; const progress = setInterval(() => { const elapsed = Math.floor((Date.now() - started) / 1000); if (!renewLease()) {
282
- error = "managed process lease lost";
283
- timedOut = true;
284
- terminateProcessGroup(child.pid, "SIGTERM");
285
- return;
286
- } for (const file of [stdout, stderr]) {
287
- try {
288
- const size = fs.fstatSync(file).size, previous = lastOutputSize.get(file) ?? 0;
289
- if (size > previous) {
290
- lastOutputSize.set(file, size);
291
- armInactivityTimeout();
277
+ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, onSpawn, renewLease) {
278
+ return new Promise((resolve) => {
279
+ const started = Date.now();
280
+ const child = spawn("/bin/sh", ["-lc", checkShellScript()], { cwd, env: { ...environment, DD_FLOW_CHECK_COMMAND: command }, stdio: ["ignore", stdout, stderr], detached: process.platform !== "win32" });
281
+ let error = null;
282
+ let stopping = false;
283
+ let progress;
284
+ let escalation;
285
+ const stop = (cause) => {
286
+ if (stopping)
287
+ return;
288
+ stopping = true;
289
+ error = cause instanceof Error ? cause.message : String(cause);
290
+ if (progress) {
291
+ clearInterval(progress);
292
+ progress = undefined;
293
+ }
294
+ if (child.pid) {
295
+ terminateProcessGroup(child.pid, "SIGTERM");
296
+ escalation = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 5_000);
297
+ }
298
+ };
299
+ // Register handlers before checking pid or calling persistence callbacks.
300
+ // Failure is not completion until the spawned process has actually closed.
301
+ child.once("error", (value) => { error = value.message; });
302
+ child.once("close", (exitCode, signal) => {
303
+ if (progress)
304
+ clearInterval(progress);
305
+ if (escalation)
306
+ clearTimeout(escalation);
307
+ if (stopping)
308
+ terminateProcessGroup(child.pid, "SIGKILL");
309
+ resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: stopping || Boolean(signal) || Boolean(error) });
310
+ });
311
+ if (!child.pid) {
312
+ error = "check process did not provide a PID";
313
+ return;
292
314
  }
293
- }
294
- catch { /* terminal stream */ }
295
- } heartbeat(elapsed); }, 15_000); let timedOut = false; let error = null; let escalation; armInactivityTimeout(); child.once("error", (value) => { error = value.message; }); child.once("close", (exitCode, signal) => { clearInterval(progress); if (timeout)
296
- clearTimeout(timeout); if (escalation)
297
- clearTimeout(escalation); resolve({ exitCode, error: error ?? (timedOut ? "timed out after 900 seconds without process output" : signal ? `terminated by ${signal}` : null), aborted: timedOut || Boolean(signal) || Boolean(error) }); }); }); }
315
+ try {
316
+ onSpawn(child.pid);
317
+ }
318
+ catch (cause) {
319
+ stop(cause);
320
+ return;
321
+ }
322
+ progress = setInterval(() => {
323
+ try {
324
+ if (!renewLease()) {
325
+ stop("managed process lease lost");
326
+ return;
327
+ }
328
+ heartbeat(Math.floor((Date.now() - started) / 1000));
329
+ }
330
+ catch (cause) {
331
+ stop(cause);
332
+ }
333
+ }, 15_000);
334
+ });
335
+ }
298
336
  function checkShellScript() {
299
337
  return [
300
338
  'completion="${DD_FLOW_CHECK_COMPLETION_FILE:-}"',
@@ -13,7 +13,7 @@ export function registerManagedProcess(context, input) {
13
13
  const token = crypto.randomUUID();
14
14
  db.run(`INSERT INTO managed_processes
15
15
  (id, kind, pid, pid_started_at, owner_id, lease_token, lease_expires_at, project_id, run_id, work_id, check_id, operation_id, stdout_path, stderr_path, state, started_at, updated_at, finished_at, termination_reason, metadata_json)
16
- VALUES (?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, NULL, NULL, ?)`, [id, input.kind, input.ownerId, token, leaseExpiry(now, input.leaseMs), input.projectId ?? null, input.runId ?? null, input.workId ?? null, input.checkId ?? null, input.operationId ?? null, input.stdoutPath ?? null, input.stderrPath ?? null, now, now, JSON.stringify(input.metadata ?? {})]);
16
+ VALUES (?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, NULL, NULL, ?)`, [id, input.kind, input.ownerId, token, leaseExpiry(now, input.leaseMs), input.projectId ?? null, input.runId ?? null, input.workId ?? null, input.checkId ?? null, input.operationId ?? null, input.stdoutPath ?? null, input.stderrPath ?? null, now, now, JSON.stringify({ ...input.metadata, ...(input.ownerPid ? { owner_pid: input.ownerPid, owner_pid_started_at: processStartedAt(input.ownerPid) } : {}) })]);
17
17
  return requireProcess(db, id);
18
18
  }
19
19
  export function confirmManagedProcess(context, input) {
@@ -23,6 +23,10 @@ export function confirmManagedProcess(context, input) {
23
23
  const metadata = parseMetadata(current.metadata_json);
24
24
  if (input.processGroupId)
25
25
  metadata.process_group_id = input.processGroupId;
26
+ if (input.ownerPid) {
27
+ metadata.owner_pid = input.ownerPid;
28
+ metadata.owner_pid_started_at = processStartedAt(input.ownerPid);
29
+ }
26
30
  const result = db.run(`UPDATE managed_processes
27
31
  SET pid = ?, pid_started_at = ?, state = 'running', lease_expires_at = ?, updated_at = ?, metadata_json = ?
28
32
  WHERE id = ? AND lease_token = ? AND state = 'starting'`, [input.pid, processStartedAt(input.pid), leaseExpiry(now, input.leaseMs), now, JSON.stringify(metadata), input.id, input.leaseToken]);
@@ -48,12 +52,15 @@ export function processIsAlive(record) {
48
52
  try {
49
53
  process.kill(record.pid, 0);
50
54
  }
51
- catch {
52
- return false;
55
+ catch (error) {
56
+ return error.code !== "ESRCH";
53
57
  }
54
- return !record.pid_started_at || record.pid_started_at === processStartedAt(record.pid);
58
+ const started = processStartedAt(record.pid);
59
+ return !record.pid_started_at || !started || record.pid_started_at === started;
55
60
  }
56
- function processTreeIsAlive(record) {
61
+ export function processTreeIsAlive(record) {
62
+ if (processIsAlive(record))
63
+ return true;
57
64
  const group = parseMetadata(record.metadata_json).process_group_id;
58
65
  if (process.platform !== "win32" && typeof group === "number") {
59
66
  try {
@@ -64,9 +71,19 @@ function processTreeIsAlive(record) {
64
71
  return false;
65
72
  }
66
73
  }
67
- return processIsAlive(record);
74
+ return false;
75
+ }
76
+ export function managedOwnerIsAlive(record) {
77
+ const metadata = parseMetadata(record.metadata_json);
78
+ if (typeof metadata.owner_pid !== "number")
79
+ return null;
80
+ return processIsAlive({ pid: metadata.owner_pid, pid_started_at: typeof metadata.owner_pid_started_at === "string" ? metadata.owner_pid_started_at : null });
68
81
  }
69
- /** Claims only expired records. Callers must still verify `processIsAlive` before stopping a PID. */
82
+ /**
83
+ * Expiry proves that observation stopped, not that the owner died. A live PID
84
+ * remains owned until its result is reconciled; only a dead process can be
85
+ * atomically claimed for cleanup.
86
+ */
70
87
  export function claimExpiredManagedProcesses(context, ownerId) {
71
88
  const db = registry(context);
72
89
  const now = context.now();
@@ -75,6 +92,9 @@ export function claimExpiredManagedProcesses(context, ownerId) {
75
92
  const candidates = db.all("SELECT * FROM managed_processes WHERE state IN ('starting','running','stopping','orphaned') AND lease_expires_at < ? ORDER BY lease_expires_at, id", [now]);
76
93
  const claimed = [];
77
94
  for (const candidate of candidates) {
95
+ const ownerAlive = managedOwnerIsAlive(candidate);
96
+ if (ownerAlive === true || (ownerAlive === null && processTreeIsAlive(candidate)))
97
+ continue;
78
98
  const token = crypto.randomUUID();
79
99
  const result = db.run("UPDATE managed_processes SET owner_id = ?, lease_token = ?, state = 'orphaned', lease_expires_at = ?, updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping','orphaned') AND lease_expires_at < ?", [ownerId, token, leaseExpiry(now), now, candidate.id, candidate.lease_token, now]);
80
100
  if (result.changes === 1)
@@ -150,7 +170,9 @@ function processStartedAt(pid) { const result = spawnSync("ps", ["-o", "lstart="
150
170
  function releasePortClaims(db, claims) { for (const claim of claims)
151
171
  db.run("DELETE FROM managed_resources WHERE resource_kind = 'port' AND resource_key = ? AND lease_token = ?", [claim.key, claim.token]); }
152
172
  function terminateOwnedProcess(record, signal) {
153
- if (!record.pid || !record.pid_started_at || !processTreeIsAlive(record))
173
+ // Never signal a group merely because its former leader PID was once ours.
174
+ // PID reuse or a departed leader makes group ownership unprovable.
175
+ if (!record.pid || !record.pid_started_at || !processIsAlive(record))
154
176
  return;
155
177
  const group = parseMetadata(record.metadata_json).process_group_id;
156
178
  try {
@@ -102,7 +102,7 @@ async function dispatch(context, input) {
102
102
  clearInterval(leaseHeartbeat);
103
103
  }
104
104
  }
105
- function nextServerRequests(context, limit) { const selected = new Set(); return context.db.all("SELECT merge_request_id, project_id, run_id, executor_work_id, target_workspace, execution_route, status, created_at FROM merge_requests WHERE status = 'queued' AND execution_route = 'server' ORDER BY created_at, merge_request_id").filter((request) => { if (selected.has(request.project_id) || context.db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?)) LIMIT 1", [request.project_id, request.created_at, request.created_at, request.merge_request_id]))
105
+ function nextServerRequests(context, limit) { const selected = new Set(); return context.db.all("SELECT merge_request_id, project_id, run_id, executor_work_id, target_workspace, execution_route, status, created_at FROM merge_requests WHERE status = 'queued' AND execution_route = 'server' ORDER BY created_at, merge_request_id").filter((request) => { if (selected.has(request.project_id) || context.db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?)) LIMIT 1", [request.project_id, request.created_at, request.created_at, request.merge_request_id]))
106
106
  return false; selected.add(request.project_id); return true; }).slice(0, limit); }
107
107
  function reconcileExpiredDispatches(context) { const expired = context.db.all("SELECT merge_request_id, executor_work_id FROM merge_requests WHERE status = 'dispatching' AND dispatch_lease_expires_at < ?", [context.now()]); for (const request of expired) {
108
108
  const work = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
@@ -15,7 +15,7 @@ import { refreshRunSessionProjection } from "./run-projection.js";
15
15
  import { resolveCanonRoot } from "./canon.js";
16
16
  import { bindCurrentEngineToRun } from "./engines.js";
17
17
  import { executionProfilePath, loadVnextExecutionProfile } from "./vnext-execution-profile.js";
18
- import { isLegalVnextTransition, vnextStages } from "../domain/stage-catalog.js";
18
+ import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
19
19
  import { publicSessionIdentity } from "./session-identity.js";
20
20
  const runSchemaId = "dd-flow/flow-run@3";
21
21
  const runtimeSchemaId = "dd-flow/flow-run@3";
@@ -754,6 +754,66 @@ export function advanceFlowRun(context, input) {
754
754
  const updatedRun = requireRunById(context, project.id, run.id);
755
755
  return { ok: true, run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
756
756
  }
757
+ /**
758
+ * The only legal backward edge in vNext: an integration gate exposed a
759
+ * product defect, so the source must be repaired and reviewed again. Normal
760
+ * stage start remains fail-closed for completed stages.
761
+ */
762
+ export function prepareVnextMergeSourceRepairAttempt(context, input) {
763
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
764
+ const run = resolveRun(context, project.id, input.runId);
765
+ const index = authoritativeIndex(run);
766
+ const code = index.stage_runs.find((item) => item.stage === "code");
767
+ const review = index.stage_runs.find((item) => item.stage === "code-review");
768
+ const merge = index.stage_runs.find((item) => item.stage === "merge");
769
+ if (!code || code.status !== "done" || !review || review.status !== "done" || !merge || merge.status !== "running") {
770
+ throw new AppError("invalid_merge_repair_state", "Source repair requires accepted CODE/CODE-REVIEW and a running MERGE attempt", 2, {
771
+ run_id: run.id,
772
+ code: code?.status ?? null,
773
+ code_review: review?.status ?? null,
774
+ merge: merge?.status ?? null
775
+ });
776
+ }
777
+ const root = runArtifactRoot(run);
778
+ for (const stage of ["code", "code-review", "merge"])
779
+ archiveExistingStageAttempt(root, vnextStageDirectory(stage));
780
+ const now = context.now();
781
+ const restarted = {
782
+ ...code,
783
+ status: "running",
784
+ started_at: now,
785
+ updated_at: now,
786
+ attempt: nextAttempt(code)
787
+ };
788
+ delete restarted.completed_at;
789
+ delete restarted.duration_ms;
790
+ delete restarted.stage_report;
791
+ delete restarted.data;
792
+ delete restarted.report;
793
+ delete restarted.artifact_aliases;
794
+ const cycle = `MRR-${input.mergeRequestId}`;
795
+ index.stage_runs = index.stage_runs.filter((item) => item.stage !== "code" && item.stage !== "code-review" && item.stage !== "merge");
796
+ index.stage_runs.push(restarted);
797
+ index.variables = { ...(index.variables ?? {}), "merge.source_repair": { cycle, merge_request_id: input.mergeRequestId, prepared_at: now } };
798
+ index.status = "running";
799
+ index.verdict = "merge_source_repair";
800
+ index.next_action = "start_code_repair";
801
+ index.current_stage = "code";
802
+ index.updated_at = now;
803
+ index.attempts = index.stage_runs.map((item) => ({
804
+ stage: item.stage,
805
+ attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1,
806
+ root: item.dir,
807
+ archive: null,
808
+ status: item.status,
809
+ started_at: item.started_at ?? now,
810
+ finished_at: item.completed_at ?? null
811
+ }));
812
+ persistRunState(context, project, run, index);
813
+ appendAudit(context, { projectId: project.id, eventType: "flow_run.merge_source_repair_prepared", payload: { run_id: run.id, merge_request_id: input.mergeRequestId, cycle } });
814
+ appendRunTimeline(root, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
815
+ return { run_id: run.id, cycle };
816
+ }
757
817
  function closeOpenStagesForOverride(index, status, now) {
758
818
  const stageStatus = status === "cancelled" ? "skipped" : "failed";
759
819
  for (const stage of index.stage_runs) {
@@ -48,10 +48,11 @@ export function startVnextCodeReview(context, input) {
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
50
  const mode = effectiveMode(home, run);
51
+ const cycle = reviewCycle(run);
51
52
  freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
52
53
  const promptPath = path.join(root, "stage-prompt.md");
53
54
  const reviewContextPath = path.join(root, "review-context.json");
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
+ 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, workspace_fingerprint: workspaceFingerprint(run.workspace_root), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
55
56
  if (mode.mode === "off") {
56
57
  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`;
57
58
  fs.writeFileSync(promptPath, prompt);
@@ -61,7 +62,7 @@ export function startVnextCodeReview(context, input) {
61
62
  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")) } };
62
63
  }
63
64
  const groups = reviewGroups(home);
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 } })) };
65
+ 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 } })) };
65
66
  const batchFile = path.join(root, "review-work-batch.json");
66
67
  fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
67
68
  const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups });
@@ -79,7 +80,7 @@ export function addVnextCodeReviewRepair(context, input) {
79
80
  const projectRoot = resolveProjectRoot(input.projectRoot);
80
81
  const project = requireProjectByRoot(context, projectRoot);
81
82
  const run = requireRun(context, project.id, input.runId);
82
- const reviewers = reviewerWorks(context, project.id, run.id);
83
+ const reviewers = reviewerWorks(context, project.id, run.id, reviewCycle(run));
83
84
  const findings = canonicalCodeFindings(reviewers);
84
85
  const selected = findings.filter((finding) => input.findingIds.includes(finding.finding_ref));
85
86
  if (selected.length !== new Set(input.findingIds).size)
@@ -108,7 +109,8 @@ export function addVnextCodeReviewRepair(context, input) {
108
109
  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] ?? [] })),
109
110
  reviewChecks,
110
111
  originWorkIds: origins,
111
- objective: input.objective
112
+ objective: input.objective,
113
+ reviewCycle: reviewCycle(run)
112
114
  });
113
115
  }
114
116
  export async function finishVnextCodeReview(context, input) {
@@ -140,7 +142,8 @@ export async function finishVnextCodeReview(context, input) {
140
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`);
141
143
  validateSchema({ schemaName: "code-review-decision", file: decisionFile, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
142
144
  const decision = readJson(decisionFile);
143
- const reviewers = reviewerWorks(context, project.id, run.id);
145
+ const cycle = reviewContext.cycle ?? "initial";
146
+ const reviewers = reviewerWorks(context, project.id, run.id, cycle);
144
147
  if (mode !== "off") {
145
148
  const incomplete = reviewers.filter((work) => work.status !== "completed");
146
149
  if (incomplete.length)
@@ -149,7 +152,7 @@ export async function finishVnextCodeReview(context, input) {
149
152
  validateReviewerResult(context, work, run);
150
153
  const canonical = reviewDecisionForRepair(decision, reviewers);
151
154
  const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
152
- let repairs = reviewRepairWorks(context, project.id, run.id, root);
155
+ let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
153
156
  const decisionSha = sha256File(decisionFile);
154
157
  const frozenShaFile = path.join(root, ".decision-sha256");
155
158
  if (fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== decisionSha)
@@ -160,14 +163,14 @@ export async function finishVnextCodeReview(context, input) {
160
163
  fs.writeFileSync(frozenShaFile, `${decisionSha}\n`);
161
164
  return { ok: true, run_id: run.id, stage, outcome: "repair_required", decision_sha256: decisionSha, repair, instruction: "Run the returned repair Work in one fresh child session. When it completes, invoke the same stage finish command again with the unchanged decision file.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
162
165
  }
163
- repairs = reviewRepairWorks(context, project.id, run.id, root);
166
+ repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
164
167
  const incompleteRepairs = repairs.filter((work) => work.status !== "completed");
165
168
  if (incompleteRepairs.length)
166
169
  throw new AppError("review_repair_incomplete", "CODE-REVIEW cannot finish while a repair Work is unsettled", 2, { works: incompleteRepairs.map((work) => ({ work_id: work.work_id, status: work.status })) });
167
170
  validateDecision(context, { decision: canonical.decision, reviewers: canonical.reviewers, repairs, workspaceRoot: run.workspace_root });
168
171
  }
169
172
  const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
170
- const repairs = reviewRepairWorks(context, project.id, run.id, root);
173
+ const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
171
174
  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 : []; }));
172
175
  const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: finalChecks });
173
176
  if (unchangedFailures.length)
@@ -320,7 +323,7 @@ function validateReviewerResult(_context, work, _run) {
320
323
  if (result.aspects.some((aspect) => aspect.verdict === "blocked"))
321
324
  throw new AppError("review_blocked", "A blocked review aspect must be resolved before CODE-REVIEW can finish", 2, { work_id: work.work_id });
322
325
  }
323
- function reviewerWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const p = readJsonString(work.payload_json); return Boolean(p && p.kind === "code-review"); }); }
326
+ function reviewerWorks(context, projectId, runId, cycle) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const payload = readJsonString(work.payload_json); return Boolean(payload.kind === "code-review" && (typeof payload.review_cycle === "string" ? payload.review_cycle : "initial") === cycle); }); }
324
327
  function codeWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).filter((work) => readJsonString(work.payload_json).schema_id === "dd-flow/code-work-packet@5" && !reviewFindingIds(work).length); }
325
328
  function acceptedCodeChecks(context, projectId, runId) { return codeWorks(context, projectId, runId).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }); }
326
329
  function validateRepairChecks(context, input) { const known = new Set(acceptedCodeChecks(context, input.projectId, input.runId).map((check) => check.id)); const selected = {}; for (const item of input.decision.findings.filter((item) => item.disposition === "fix")) {
@@ -332,7 +335,7 @@ function validateRepairChecks(context, input) { const known = new Set(acceptedCo
332
335
  throw new AppError("review_check_reference_unknown", "CODE-REVIEW repair selects a check absent from the accepted CODE handoff", 2, { finding_ref: item.finding_ref, check_refs: unknown });
333
336
  selected[item.finding_ref] = refs;
334
337
  } return selected; }
335
- function reviewRepairWorks(context, projectId, runId, root) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => isCodeReviewStageRepair(work, root)); }
338
+ function reviewRepairWorks(context, projectId, runId, root, cycle) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const payload = readJsonString(work.payload_json); return isCodeReviewStageRepair(work, root) && (typeof payload.review_cycle === "string" ? payload.review_cycle : "initial") === cycle; }); }
336
339
  export function isCodeReviewStageRepair(work, root) {
337
340
  if (reviewFindingIds(work).length)
338
341
  return true;
@@ -350,7 +353,8 @@ function reviewFindingIds(work) { const repair = readJsonString(work.payload_jso
350
353
  function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
351
354
  return []; const findings = repair.review_findings; return Array.isArray(findings) ? [...new Set(findings.flatMap((value) => typeof value === "object" && value !== null && Array.isArray(value.check_refs) ? value.check_refs.filter((ref) => typeof ref === "string") : []))] : []; }
352
355
  function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
353
- 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")
356
+ function effectiveMode(home, run) { const index = JSON.parse(run.index_json); if (index.variables?.["merge.source_repair"])
357
+ return { mode: "standard", source: "project_policy", reason: "A failed integration gate requires independent review of its source repair." }; const configured = index.settings?.code_review; const requested = configured?.mode ?? "auto"; if (requested !== "auto")
354
358
  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." }; }
355
359
  function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
356
360
  function changedPaths(workspaceRoot) { try {
@@ -366,6 +370,14 @@ function stageStatus(run, name) { try {
366
370
  catch {
367
371
  return null;
368
372
  } }
373
+ function reviewCycle(run) { try {
374
+ const value = JSON.parse(run.index_json);
375
+ const repair = value.variables?.["merge.source_repair"];
376
+ return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.cycle === "string" ? String(repair.cycle) : "initial";
377
+ }
378
+ catch {
379
+ return "initial";
380
+ } }
369
381
  function stageStartedAt(run, fallback) { try {
370
382
  return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === stage)?.started_at ?? fallback;
371
383
  }
@@ -1,10 +1,10 @@
1
1
  import crypto from "node:crypto";
2
- import { spawn } from "node:child_process";
2
+ import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { AppError } from "../shared/errors.js";
6
6
  import { resolveProjectRoot } from "../storage/paths.js";
7
- import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
7
+ import { aggregateCheckDeclarations, checkReceipts, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
8
8
  import { requireProjectByRoot } from "./projects.js";
9
9
  import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
10
10
  import { validateSchema } from "./schema-validation.js";
@@ -31,6 +31,7 @@ export async function startVnextCode(context, input) {
31
31
  const existingRoot = path.join(home, stageDir);
32
32
  const existingPrompt = path.join(existingRoot, "stage-prompt.md");
33
33
  const existingStage = stageStatus(run, stage);
34
+ const sourceRepairAttempt = isMergeSourceRepairAttempt(run);
34
35
  if (existingStage && fs.existsSync(existingPrompt)) {
35
36
  const externalContext = applyExternalStageContext({ stageRoot: existingRoot, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
36
37
  const prompt = fs.readFileSync(existingPrompt, "utf8");
@@ -73,14 +74,16 @@ export async function startVnextCode(context, input) {
73
74
  resultPath: path.join(root, "stage-report.json"),
74
75
  ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
75
76
  });
76
- attachFlowRunStage(context, {
77
- projectRoot,
78
- runId: input.runId,
79
- stage,
80
- dir: stageDir,
81
- status: "running",
82
- dataSchemaId: "dd-flow/stage-report@1"
83
- });
77
+ if (!sourceRepairAttempt) {
78
+ attachFlowRunStage(context, {
79
+ projectRoot,
80
+ runId: input.runId,
81
+ stage,
82
+ dir: stageDir,
83
+ status: "running",
84
+ dataSchemaId: "dd-flow/stage-report@1"
85
+ });
86
+ }
84
87
  const graph = codeWorkGraph(context, project.id, input.runId);
85
88
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
86
89
  appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
@@ -357,7 +360,8 @@ export function addVnextCodeRepair(context, input) {
357
360
  ...(receipt ? ["Stop and report the blocker if the proposed repair contradicts an accepted requirement or non-goal."] : ["Do not modify accepted PLAN artifacts, code-work-batch.json, or review decisions. Stop and report if the finding cannot be repaired within accepted scope."])
358
361
  ]),
359
362
  depends_on: origins.map((work) => work.work_id),
360
- result_schema: "dd-flow/code-work-result@2"
363
+ result_schema: "dd-flow/code-work-result@3",
364
+ ...(input.reviewCycle ? { review_cycle: input.reviewCycle } : {})
361
365
  };
362
366
  const file = path.join(home, stageDir, `.repair-${crypto.randomUUID()}.json`);
363
367
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -545,8 +549,8 @@ export function verificationProjection(works, receipts, options = {}) {
545
549
  for (const work of works) {
546
550
  const result = readJsonResult(work.result);
547
551
  for (const item of result.evidence ?? [])
548
- if (item.criterion_id)
549
- rawEvidence.set(item.criterion_id, unique([...(rawEvidence.get(item.criterion_id) ?? []), ...(item.refs ?? [])]));
552
+ if (item.obligation_ref)
553
+ rawEvidence.set(item.obligation_ref, unique([...(rawEvidence.get(item.obligation_ref) ?? []), ...(item.refs ?? [])]));
550
554
  }
551
555
  const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
552
556
  const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
@@ -649,50 +653,22 @@ async function ensureCodeWorkspaceReady(context, projectId, run, root, readiness
649
653
  throw new AppError("execution_profile_invalid", "CODE requires a frozen bootstrap command in the RUN execution profile", 1, { run_id: run.id });
650
654
  }
651
655
  const receiptPath = path.join(root, "workspace-readiness.json");
652
- const prior = readReadiness(receiptPath);
653
- if (prior?.workspace_root === run.workspace_root && prior.command === bootstrap.command && prior.status === "passed" && JSON.stringify(prior.check_declarations ?? []) === JSON.stringify(readinessChecks))
654
- return { ...prior, reused: true };
656
+ // Re-run the project bootstrap on stage entry: a prior receipt cannot prove
657
+ // that ignored dependencies or local services still exist.
655
658
  const startedAt = new Date().toISOString();
656
659
  progress?.(`workspace bootstrap started: ${bootstrap.command}`);
657
- const result = await runBootstrap(bootstrap.command, run.workspace_root, codeExecutionEnvironment(run.workspace_root), progress);
660
+ const result = await runWorkspaceBootstrap(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, command: bootstrap.command, artifactDir: "05-code/readiness", progress });
658
661
  const bootstrapPassed = result.exit_code === 0 && !result.error;
659
662
  const checks = bootstrapPassed && readinessChecks.length
660
663
  ? await runCodeChecks(context, { projectId, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, scope: "aggregate", artifactDir: "05-code/readiness", checks: readinessChecks, ...(progress ? { progress } : {}) })
661
664
  : [];
662
- const receipt = { schema_id: "dd-flow/workspace-readiness@2", workspace_root: run.workspace_root, command: bootstrap.command, policy_ref: bootstrap.policy_ref, started_at: startedAt, finished_at: new Date().toISOString(), status: bootstrapPassed && checks.every((check) => check.status === "passed") ? "passed" : "failed", exit_code: result.exit_code, stdout: result.stdout, stderr: result.stderr, check_declarations: readinessChecks, checks: checks.map((check) => ({ id: check.id, status: check.status, receipt_path: check.receipt_path })), ...(result.error ? { error: result.error } : {}), receipt_path: receiptPath };
665
+ const receipt = { schema_id: "dd-flow/workspace-readiness@2", workspace_root: run.workspace_root, command: bootstrap.command, policy_ref: bootstrap.policy_ref, started_at: startedAt, finished_at: new Date().toISOString(), status: bootstrapPassed && checks.every((check) => check.status === "passed") ? "passed" : "failed", process_id: result.process_id, stdout_path: result.stdout_path, stderr_path: result.stderr_path, completion_path: result.completion_path, exit_code: result.exit_code, stdout: result.stdout, stderr: result.stderr, check_declarations: readinessChecks, checks: checks.map((check) => ({ id: check.id, status: check.status, receipt_path: check.receipt_path })), ...(result.error ? { error: result.error } : {}), receipt_path: receiptPath };
663
666
  fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
664
667
  if (receipt.status !== "passed")
665
668
  throw new AppError("workspace_readiness_failed", "CODE workspace bootstrap failed", 1, receipt);
666
669
  progress?.("workspace bootstrap passed");
667
670
  return receipt;
668
671
  }
669
- function runBootstrap(command, cwd, environment, progress) {
670
- return new Promise((resolve) => {
671
- const started = Date.now();
672
- const child = spawn("/bin/sh", ["-lc", command], { cwd, env: environment, stdio: ["ignore", "pipe", "pipe"] });
673
- let stdout = "";
674
- let stderr = "";
675
- let error = null;
676
- child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
677
- child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
678
- const heartbeat = setInterval(() => progress?.(`workspace bootstrap still running (${Math.floor((Date.now() - started) / 1000)}s)`), 15_000);
679
- const timeout = setTimeout(() => child.kill("SIGTERM"), 15 * 60 * 1000);
680
- child.once("error", (value) => { error = value.message; });
681
- child.once("close", (exitCode, signal) => {
682
- clearInterval(heartbeat);
683
- clearTimeout(timeout);
684
- resolve({ exit_code: exitCode, stdout, stderr, error: error ?? (signal ? `terminated by ${signal}` : null) });
685
- });
686
- });
687
- }
688
- function readReadiness(file) {
689
- try {
690
- return JSON.parse(fs.readFileSync(file, "utf8"));
691
- }
692
- catch {
693
- return null;
694
- }
695
- }
696
672
  function requireHome(run) {
697
673
  if (!run.run_root)
698
674
  throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1);
@@ -726,6 +702,8 @@ export function writeProtocolFlowStatus(workspaceRoot, runHome, runId, status) {
726
702
  }
727
703
  function nextAction(run, changedPaths) {
728
704
  const index = JSON.parse(run.index_json);
705
+ if (index.variables?.["merge.source_repair"])
706
+ return "start_code_review";
729
707
  const requested = index.settings?.code_review?.mode ?? "auto";
730
708
  const enabled = requested === "off" ? false : requested === "auto" ? changedPaths.length > 0 : true;
731
709
  const stopTarget = index.execution_profile?.settings?.stop_target;
@@ -741,6 +719,15 @@ function stageStatus(run, name) {
741
719
  return null;
742
720
  }
743
721
  }
722
+ function isMergeSourceRepairAttempt(run) {
723
+ try {
724
+ const value = JSON.parse(run.index_json);
725
+ return Boolean(value.variables?.["merge.source_repair"]);
726
+ }
727
+ catch {
728
+ return false;
729
+ }
730
+ }
744
731
  function changedPathsFromReport(file) {
745
732
  try {
746
733
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { execFileSync, spawnSync } from "node:child_process";
@@ -8,11 +9,12 @@ import { resolveProjectRoot } from "../storage/paths.js";
8
9
  import { requireProjectByRoot } from "./projects.js";
9
10
  import { nextMergeRequestId } from "./ids.js";
10
11
  import { readProjectConfig } from "./config.js";
11
- import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
12
+ import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareVnextMergeSourceRepairAttempt } from "./runs.js";
12
13
  import { flowCommand, stagePauseCommandTemplate } from "./stage-pause.js";
13
14
  import { validateSchema } from "./schema-validation.js";
14
15
  import { writeStageReport } from "./stage-report-renderer.js";
15
- import { bindStageCoordinatorWork, createChildWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
16
+ import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
17
+ import { addVnextCodeRepair } from "./vnext-code.js";
16
18
  import { checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
17
19
  import { applyExternalStageContext } from "./stage-context.js";
18
20
  import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
@@ -27,7 +29,7 @@ export function ensureVnextMergeRequest(context, input) {
27
29
  const projectRoot = resolveProjectRoot(input.projectRoot);
28
30
  const project = requireProjectByRoot(context, projectRoot);
29
31
  const run = requireRun(context, project.id, input.runId);
30
- const existing = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at LIMIT 1", [project.id, run.id]);
32
+ const existing = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') ORDER BY created_at DESC LIMIT 1", [project.id, run.id]);
31
33
  if (existing)
32
34
  return requestView(context, existing);
33
35
  const settings = executionSettings(run);
@@ -88,14 +90,14 @@ export function ensureVnextMergeRequest(context, input) {
88
90
  const now = context.now();
89
91
  context.db.exec("BEGIN IMMEDIATE");
90
92
  try {
91
- const raced = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ?", [project.id, run.id]);
93
+ const raced = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') ORDER BY created_at DESC LIMIT 1", [project.id, run.id]);
92
94
  if (raced) {
93
95
  context.db.exec("COMMIT");
94
96
  return requestView(context, raced);
95
97
  }
96
98
  child = createChildWork(context, { parentWorkId: root.work_id, slug: "merge", task: `Integrate frozen source ${sourceCommit} into ${targetBranch}, resolve material conflicts, and preserve accepted behavior.`, launchPolicy: route === "server" ? "fresh_agent_required" : "reuse_allowed", resultSchema: "dd-flow/merge-result@1", payload: { kind: "merge", checks: semantic, protocol_ids: protocols } });
97
99
  requestId = nextMergeRequestId(context);
98
- context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route, readCodeCheckProfile(targetWorkspace).hash, now, now]);
100
+ context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, replacement_of_merge_request_id, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route, readCodeCheckProfile(targetWorkspace).hash, replacementOf(run), now, now]);
99
101
  context.db.exec("COMMIT");
100
102
  }
101
103
  catch (error) {
@@ -219,7 +221,7 @@ export async function finishVnextMerge(context, input) {
219
221
  throw new AppError("merge_semantic_blocked", "A blocked semantic result cannot complete MERGE", 2, { result_path: resultPath });
220
222
  if (request.checkpoint === "apply_recorded") {
221
223
  input.progress?.("bootstrapping integrated target");
222
- runBootstrap(run, request.target_workspace);
224
+ await runBootstrap(context, run, request.target_workspace, request.merge_request_id, input.progress);
223
225
  context.db.run("UPDATE merge_requests SET checkpoint = 'bootstrap_ready', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
224
226
  request = requireRequest(context, request.merge_request_id);
225
227
  }
@@ -231,7 +233,7 @@ export async function finishVnextMerge(context, input) {
231
233
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
232
234
  if (failed.length) {
233
235
  context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_gate_failed", failures: failed }), context.now(), request.merge_request_id]);
234
- throw new AppError("merge_gate_failed", "Integrated target checks failed; repair in the same MERGE Work and retry finish", 2, { failures: failed });
236
+ throw new AppError("merge_source_repair_required", "Integrated target checks failed. Do not edit the integration workspace: run the returned source-repair command.", 2, { failures: failed, repair_command: `${flowCommand(context)} merge repair ${request.merge_request_id} --project-root ${JSON.stringify(projectRoot)} --json` });
235
237
  }
236
238
  const requiredRefs = mergeAcceptanceRefs(run.workspace_root, JSON.parse(request.protocol_ids_json));
237
239
  const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
@@ -303,7 +305,33 @@ catch (error) {
303
305
  context.db.exec("ROLLBACK");
304
306
  throw error;
305
307
  } refreshRunWorkProjection(context, request.project_id, request.run_id); return requestView(context, requireRequest(context, request.merge_request_id)); }
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"); }
308
+ /** Move a failed integration gate back to the source CODE/CODE-REVIEW loop. */
309
+ export async function repairVnextMerge(context, input) {
310
+ const projectRoot = resolveProjectRoot(input.projectRoot);
311
+ const project = requireProjectByRoot(context, projectRoot);
312
+ const request = requireRequest(context, input.requestId);
313
+ if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
314
+ throw new AppError("invalid_merge_repair_state", "MERGE source repair requires an action-required failed integration gate", 2, { merge_request_id: input.requestId, status: request.status, code: errorCode(request) });
315
+ }
316
+ abortIntegration(request);
317
+ const originWorkIds = sourceCodeWorkIds(context, request.project_id, request.run_id);
318
+ if (!originWorkIds.length)
319
+ throw new AppError("runtime_missing", "MERGE source repair requires accepted CODE Work evidence", 1, { run_id: request.run_id });
320
+ const failed = failedReceiptIds(request);
321
+ if (!failed.length)
322
+ throw new AppError("runtime_missing", "MERGE failure has no durable failed receipt", 1, { merge_request_id: request.merge_request_id });
323
+ // Settle the child while its canonical result path still exists; the next
324
+ // step archives the entire MERGE attempt as immutable evidence.
325
+ await failWork(context, request.executor_work_id, `Superseded by source repair after failed integration gate ${failed[0]}.`);
326
+ const prepared = prepareVnextMergeSourceRepairAttempt(context, { projectRoot, runId: request.run_id, mergeRequestId: request.merge_request_id });
327
+ const repair = addVnextCodeRepair(context, { projectRoot, runId: request.run_id, checkReceiptId: failed[0], originWorkIds, objective: `Repair source behavior evidenced by failed integration check ${failed[0]}.` });
328
+ const now = context.now();
329
+ context.db.run("UPDATE merge_requests SET status = 'superseded', checkpoint = 'source_repair_required', last_error_json = ?, completed_at = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_source_repair", replacement_pending: true, failed_receipt_ids: failed, repair_work_id: repair.repair_work_id }), now, now, request.merge_request_id]);
330
+ releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
331
+ appendFlowRunTimelineEvent(context, project.id, request.run_id, { type: "merge_source_repair_created", merge_request_id: request.merge_request_id, repair_work_id: repair.repair_work_id, cycle: prepared.cycle, failed_receipt_ids: failed });
332
+ return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: repair, next: { kind: "start_stage", stage: "code", command: `${flowCommand(context)} stage start ${request.run_id} --stage code --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } };
333
+ }
334
+ 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`; const repair = `${flowCommand(context)} merge repair ${input.request.merge_request_id} --project-root ${JSON.stringify(input.projectRoot)} --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 unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `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 gate fails, inspect only the returned receipt/logs, then run this exact source-repair command: ${repair}. It restores the integration target to baseline and creates a CODE → independent CODE-REVIEW → replacement MRG cycle. Do not repair the integration target and do not retry this MRG.`, "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"); }
307
335
  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
336
  function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
309
337
  function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
@@ -333,9 +361,14 @@ function protocolIds(home) { const root = path.join(home, "03-plan"); if (!fs.ex
333
361
  function workspaceRoute(home) { const file = path.join(home, "02-protocolize", "workspace-route.json"); const value = JSON.parse(fs.readFileSync(file, "utf8")); if (!value.policy?.integration_branch)
334
362
  throw new AppError("workspace_route_missing", "MERGE requires the frozen integration branch", 1, { file }); return { integration_branch: value.policy.integration_branch }; }
335
363
  function executionSettings(run) { return JSON.parse(run.index_json).execution_profile?.settings ?? {}; }
336
- function runBootstrap(run, cwd) { const command = executionSettings(run).code_bootstrap?.command; if (!command)
337
- return; const result = spawnSync("/bin/sh", ["-lc", command], { cwd, encoding: "utf8" }); if (result.status !== 0)
338
- throw new AppError("merge_bootstrap_failed", "Integrated target bootstrap failed", 2, { command, stdout: result.stdout, stderr: result.stderr, status: result.status }); }
364
+ async function runBootstrap(context, run, cwd, mergeId, progress) {
365
+ const command = executionSettings(run).code_bootstrap?.command;
366
+ if (!command)
367
+ throw new AppError("execution_profile_invalid", "MERGE requires a frozen workspace bootstrap command", 2);
368
+ const result = await runWorkspaceBootstrap(context, { projectId: run.project_id, runId: run.id, runHome: requireHome(run), workspaceRoot: cwd, command, artifactDir: `${stageDir}/${mergeId}/readiness`, progress });
369
+ if (result.exit_code !== 0 || result.error)
370
+ throw new AppError("merge_bootstrap_failed", "Integrated target bootstrap failed; inspect its retained logs", 2, { command, ...result });
371
+ }
339
372
  function verifyLocalDelivery(request) { const head = gitValue(request.target_workspace, ["rev-parse", request.target_branch]); if (!request.integration_commit || head !== request.integration_commit)
340
373
  throw new AppError("merge_delivery_unproven", "Local integration branch does not resolve to the accepted integration commit", 1, { target_branch: request.target_branch, expected: request.integration_commit, actual: head }); }
341
374
  function cleanupReceiptPath(run) { return path.join(requireHome(run), stageDir, "cleanup-receipt.json"); }
@@ -365,7 +398,7 @@ function releaseMergeLane(context, projectRoot, request, reason) {
365
398
  }
366
399
  catch { /* An expired lease cannot invalidate an already completed MERGE. */ }
367
400
  }
368
- function queueAhead(context, request) { return context.db.get("SELECT COUNT(*) AS count FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?))", [request.project_id, request.created_at, request.created_at, request.merge_request_id])?.count ?? 0; }
401
+ function queueAhead(context, request) { return context.db.get("SELECT COUNT(*) AS count FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?))", [request.project_id, request.created_at, request.created_at, request.merge_request_id])?.count ?? 0; }
369
402
  function queueStatus(context, request) { return { position: queueAhead(context, request) + 1, requests_ahead: queueAhead(context, request), status: request.status, route: request.execution_route }; }
370
403
  function requireOwnedActiveRequest(context, input) { const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)); const request = requireRequest(context, input.requestId); if (request.project_id !== project.id || request.executor_work_id !== input.workId || !["active", "action_required"].includes(request.status))
371
404
  throw new AppError("invalid_merge_state", "MRG/Work/project is not the active integration owner", 2, { merge_request_id: input.requestId, work_id: input.workId, status: request.status }); return request; }
@@ -392,11 +425,42 @@ catch (error) {
392
425
  function gitValue(cwd, args, allowFailure = false) { const result = spawnSync("git", args, { cwd, encoding: "utf8" }); if (result.status !== 0 && !allowFailure)
393
426
  throw new AppError("git_operation_failed", `git ${args[0]} failed`, 1, { cwd, args, stderr: result.stderr }); return result.status === 0 ? result.stdout.trim() : ""; }
394
427
  function applyReceiptPath(context, request) { const run = requireRun(context, request.project_id, request.run_id); return path.join(requireHome(run), stageDir, "works", request.executor_work_id, "apply-receipt.json"); }
395
- function requestForRun(context, projectId, runId) { const request = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at LIMIT 1", [projectId, runId]); if (!request)
428
+ function requestForRun(context, projectId, runId) { const request = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC LIMIT 1", [projectId, runId]); if (!request)
396
429
  throw new AppError("merge_request_missing", "MERGE request was not materialized by the prior terminal stage", 1, { run_id: runId }); return request; }
397
430
  function requireRequest(context, id) { const request = context.db.get("SELECT * FROM merge_requests WHERE merge_request_id = ?", [id]); if (!request)
398
431
  throw new AppError("not_found", "MERGE request is not registered", 1, { merge_request_id: id }); return request; }
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 }; }
432
+ 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, ...(request.replacement_of_merge_request_id ? { replacement_of_merge_request_id: request.replacement_of_merge_request_id } : {}), queue: queueStatus(context, request), created_at: request.created_at, completed_at: request.completed_at }; }
433
+ function replacementOf(run) { try {
434
+ const variables = JSON.parse(run.index_json).variables;
435
+ const repair = variables?.["merge.source_repair"];
436
+ return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.merge_request_id === "string" ? String(repair.merge_request_id) : null;
437
+ }
438
+ catch {
439
+ return null;
440
+ } }
441
+ function errorCode(request) { try {
442
+ const value = JSON.parse(request.last_error_json ?? "{}");
443
+ return typeof value?.code === "string" ? value.code : null;
444
+ }
445
+ catch {
446
+ return null;
447
+ } }
448
+ function failedReceiptIds(request) { try {
449
+ const value = JSON.parse(request.last_error_json ?? "{}");
450
+ return (value.failures ?? []).flatMap((failure) => typeof failure === "object" && failure !== null && typeof failure.id === "string" ? [String(failure.id)] : []);
451
+ }
452
+ catch {
453
+ return [];
454
+ } }
455
+ function sourceCodeWorkIds(context, projectId, runId) { return context.db.all("SELECT work_id, payload_json FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).flatMap((work) => { try {
456
+ return JSON.parse(work.payload_json ?? "").schema_id === "dd-flow/code-work-packet@5" ? [work.work_id] : [];
457
+ }
458
+ catch {
459
+ return [];
460
+ } }); }
461
+ function abortIntegration(request) { if (gitValue(request.target_workspace, ["rev-parse", "-q", "--verify", "MERGE_HEAD"], true))
462
+ git(request.target_workspace, ["merge", "--abort"]); const head = gitValue(request.target_workspace, ["rev-parse", "HEAD"]); const branch = gitValue(request.target_workspace, ["branch", "--show-current"]); const dirty = meaningfulStatus(request.target_workspace, []); if (head !== request.execution_target_head || branch !== request.target_branch || dirty.length)
463
+ throw new AppError("merge_recovery_required", "Integration workspace could not be restored to its locked baseline", 1, { merge_request_id: request.merge_request_id, expected_head: request.execution_target_head, head, expected_branch: request.target_branch, branch, dirty }); }
400
464
  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)
401
465
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
402
466
  function requireHome(run) { if (!run.run_root)
@@ -431,7 +431,7 @@ function projectCodeWorkBatch(input) {
431
431
  provides_checks: value.checks.filter((check) => check.availability === "planned" && check.provided_by === item.id),
432
432
  stop_conditions: item.execution_context.stop_conditions,
433
433
  depends_on: item.depends_on.map((dependency) => `${protocolId}:${dependency}`),
434
- result_schema: "dd-flow/code-work-result@2"
434
+ result_schema: "dd-flow/code-work-result@3"
435
435
  });
436
436
  }));
437
437
  const byProtocol = new Map(input.plans.map((plan) => [plan.protocolId, plan.value.items]));
@@ -428,7 +428,7 @@ function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, r
428
428
  finally {
429
429
  fs.rmSync(candidate, { force: true });
430
430
  }
431
- if (work.result_schema === "dd-flow/code-work-result@2")
431
+ if (work.result_schema === "dd-flow/code-work-result@3")
432
432
  validateCodeWorkResult(work, parsed, workspaceRoot, runHome, runId);
433
433
  if (work.result_schema === "dd-flow/code-review-result@1")
434
434
  validateCodeReviewResult(work, parsed, { workspaceRoot, runHome, runId });
@@ -442,10 +442,15 @@ function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
442
442
  const packet = codePacket(work);
443
443
  if (!packet)
444
444
  return;
445
- const acceptedCriteria = new Set(packet.acceptance.map((item) => item.criterion_id).filter((item) => typeof item === "string"));
445
+ const acceptedObligations = new Set(packet.acceptance.map((item) => item.criterion_id).filter((item) => typeof item === "string"));
446
+ for (const check of packet.checks)
447
+ acceptedObligations.add(check.id);
448
+ for (const finding of packet.repair?.review_findings ?? [])
449
+ for (const ref of finding.obligation_refs ?? [])
450
+ acceptedObligations.add(ref);
446
451
  for (const item of result.evidence ?? []) {
447
- if (!item.criterion_id || !acceptedCriteria.has(item.criterion_id))
448
- throw new AppError("evidence_criterion_unknown", "CODE Work evidence must reference an acceptance criterion assigned to this Work", 2, { work_id: work.work_id, criterion_id: item.criterion_id ?? null });
452
+ if (!item.obligation_ref || !acceptedObligations.has(item.obligation_ref))
453
+ throw new AppError("evidence_obligation_unknown", "CODE Work evidence must reference an obligation assigned to this Work", 2, { work_id: work.work_id, obligation_ref: item.obligation_ref ?? null, accepted_obligation_refs: [...acceptedObligations] });
449
454
  for (const ref of item.refs ?? [])
450
455
  assertPortableArtifactRef(ref, { workspaceRoot: projectRoot, runHome, runId });
451
456
  }
@@ -459,10 +464,10 @@ function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
459
464
  if (unchangedDocuments.length)
460
465
  throw new AppError("document_update_not_materialized", "Assigned durable document updates must exist and differ from their PLAN baseline", 2, { work_id: work.work_id, unchanged_paths: unchangedDocuments });
461
466
  const assigned = packet.repair?.review_findings?.map((finding) => finding.finding_ref) ?? [];
467
+ validateReviewResolutions(work.work_id, assigned, result.resolutions ?? []);
462
468
  if (assigned.length) {
463
469
  if ((result.changed_paths?.length ?? 0) === 0)
464
470
  throw new AppError("review_repair_no_change", "Review repair must materialize a project change; a no-op cannot resolve a finding", 2, { work_id: work.work_id, findings: assigned });
465
- validateReviewResolutions(work.work_id, assigned, result.resolutions ?? []);
466
471
  for (const resolution of result.resolutions ?? [])
467
472
  for (const ref of resolution.evidence_refs ?? [])
468
473
  assertPortableArtifactRef(ref, { workspaceRoot: projectRoot, runHome, runId });
@@ -479,22 +484,41 @@ export function validateReviewResolutions(workId, assigned, resolutions) {
479
484
  function renderWorkerPrompt(context, work, run, dependencies) {
480
485
  const command = flowCommand(context);
481
486
  const packet = codePacket(work);
482
- const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<write_boundary_invariant>", "For a mutation guarded by membership, ownership, authorization or parent lifecycle state, preserve that predicate in the write statement or make guard and write one explicit transaction with the needed lock. A prior read may diagnose an error but never proves a later write remains allowed. Apply this to create, update, delete and parent-state mutations.", "</write_boundary_invariant>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies the materialization and then executes the check.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
487
+ const mergeWork = parsePayload(work)?.kind === "merge";
488
+ const writeBoundary = mergeWork
489
+ ? ["MERGE does not own product-code repair. Use only the stage packet's merge apply command; resolve only paths that Git reports as unmerged. Do not edit integration code, tests, documentation or configuration to make a gate pass.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result."]
490
+ : [`HARD RULE: project source reads and writes must remain under ${run.workspace_root}.`, "Do not write through project_root, outside workspace_root, into another RUN, or into Git/worktree control data. Do not create, switch, merge or delete branches/worktrees.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result. Accepted requirements, non-goals and stop_conditions are semantic hard boundaries. planned_write_areas is not."];
491
+ const completionRepair = mergeWork
492
+ ? ["A failed integration check is evidence for a source-repair cycle, not permission to repair the integration workspace. Follow the exact merge repair command returned by dd-flow."]
493
+ : ["Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then call Finish again. Do not invent a cause that does not appear in the retained output."];
494
+ const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<write_boundary_invariant>", "For a mutation guarded by membership, ownership, authorization or parent lifecycle state, preserve that predicate in the write statement or make guard and write one explicit transaction with the needed lock. A prior read may diagnose an error but never proves a later write remains allowed. Apply this to create, update, delete and parent-state mutations.", "</write_boundary_invariant>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies materialization at Work finish. Execution occurs at its declared run_at gate, not necessarily in this Work.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
483
495
  if (packet)
484
496
  codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<completion_contract>", "Successful completion requires empty deviations and blockers and every assigned document update in changed_paths. A necessary path outside planned_write_areas is normal coordination drift, not a blocker; include it in changed_paths and continue.", "</completion_contract>", "");
485
- return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<hard_write_boundary>", `HARD RULE: project source reads and writes must remain under ${run.workspace_root}.`, "Do not write through project_root, outside workspace_root, into another RUN, or into Git/worktree control data. Do not create, switch, merge or delete branches/worktrees.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result. Accepted requirements, non-goals and stop_conditions are semantic hard boundaries. planned_write_areas is not.", "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and stores the canonical receipt.", "</result_contract>", ""] : []), "<completion>", "The CLI runs every declared required check before accepting this Work. A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", "Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then call Finish again. Do not invent a cause that does not appear in the retained output.", "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command, piping your JSON object to stdin: ${command} work finish ${work.work_id} --result-stdin --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`, `Fail only for an evidenced external or semantic-contract blocker: ${command} work fail ${work.work_id} --reason "receipt path + exact external or semantic blocker" --project-root ${JSON.stringify(run.project_root)} --json`, "</completion>", ""].join("\n");
497
+ return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<hard_write_boundary>", ...writeBoundary, "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and stores the canonical receipt.", "</result_contract>", ""] : []), "<completion>", "Work finish runs only declared run_at=work checks. Stage finish owns readiness/code/merge gates; successful Work completion does not mean those gates have passed. A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", ...completionRepair, "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command, piping your JSON object to stdin: ${command} work finish ${work.work_id} --result-stdin --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`, `Fail only for an evidenced external or semantic-contract blocker: ${command} work fail ${work.work_id} --reason "receipt path + exact external or semantic blocker" --project-root ${JSON.stringify(run.project_root)} --json`, "</completion>", ""].join("\n");
486
498
  }
487
- function resultSchemaGuidance(work, runId) {
499
+ export function resultSchemaGuidance(work, runId) {
488
500
  const schema = work.result_schema;
489
501
  const refs = `Evidence refs for project source are relative to workspace_root; RUN evidence uses run://${runId}/path/to/artifact.`;
490
- if (schema === "dd-flow/code-work-result@2")
491
- return [refs, "Use this complete minimal shape. For a review repair, include exactly one resolution per assigned finding; explain what changed and cite evidence that demonstrates the required outcome:", "```json", JSON.stringify({ schema_id: schema, summary: "What was implemented.", changed_paths: ["project-relative/path"], evidence: [{ criterion_id: "AC-001", refs: ["project-relative/evidence", `run://${runId}/05-code/checks/receipt.json`] }], deviations: [], blockers: [], resolutions: [{ finding_ref: "WRK-000/FIND-001", summary: "How the required outcome was achieved.", evidence_refs: ["project-relative/evidence"] }] }, null, 2), "```"];
502
+ if (schema === "dd-flow/code-work-result@3") {
503
+ const packet = codePacket(work);
504
+ const obligations = [...new Set([
505
+ ...(packet?.acceptance ?? []).map((item) => item.criterion_id).filter((id) => Boolean(id)),
506
+ ...(packet?.checks ?? []).map((item) => item.id),
507
+ ...(packet?.repair?.review_findings ?? []).flatMap((finding) => finding.obligation_refs ?? [])
508
+ ])];
509
+ const findings = packet?.repair?.review_findings ?? [];
510
+ return [refs, `Assigned obligation refs: ${JSON.stringify(obligations)}. Cite only these in evidence.obligation_ref.`,
511
+ "This is a shape template, not a completed result. Populate changed_paths and evidence from the actual work and existing files; empty arrays below do not establish completion.",
512
+ "For a gate repair, cite the assigned check id. For a review repair, replace each resolution summary and fill evidence_refs with existing evidence proving its required outcome. Ordinary Work has resolutions: [].",
513
+ "```json", JSON.stringify({ schema_id: schema, summary: "Describe the actual implementation.", changed_paths: [], evidence: [], deviations: [], blockers: [], resolutions: findings.map((finding) => ({ finding_ref: finding.finding_ref, summary: "Describe how the assigned required outcome was achieved.", evidence_refs: [] })) }, null, 2), "```",
514
+ "Each evidence entry has exactly obligation_ref (one assigned ref) and refs (nonempty list of existing evidence paths). Each resolution has finding_ref, summary and nonempty evidence_refs. Do not invent file names or finding ids."];
515
+ }
492
516
  if (schema === "dd-flow/code-review-result@1") {
493
- return [refs, "Assess every assigned aspect exactly once. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id to form the canonical reference.", "Use this complete minimal shape. Report only material, direct-evidence findings; taste and cosmetics are not findings:", "```json", JSON.stringify({ schema_id: schema, verdict: "pass | findings | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | findings | blocked", summary: "Conclusion.", evidence_refs: ["path/to/file", `run://${runId}/05-code/checks/receipt.json`] }], findings: [{ finding_id: "FIND-001", aspect_id: "assigned_aspect_id", priority: "p0 | p1 | p2 | p3", problem: "Violated obligation or rule.", impact: "Concrete risk or failure.", required_outcome: "Smallest observable result that closes the defect.", evidence_refs: ["path/to/file"], obligation_refs: ["R-001 | AC-001 | policy ref"] }] }, null, 2), "```"];
517
+ return [refs, "Assess every assigned aspect exactly once. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id to form the canonical reference.", "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Report only material, direct-evidence findings; taste and cosmetics are not findings:", "```json", JSON.stringify({ schema_id: schema, verdict: "pass | findings | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | findings | blocked", summary: "Conclusion.", evidence_refs: [] }], findings: [{ finding_id: "FIND-001", aspect_id: "assigned_aspect_id", priority: "p0 | p1 | p2 | p3", problem: "Violated obligation or rule.", impact: "Concrete risk or failure.", required_outcome: "Smallest observable result that closes the defect.", evidence_refs: ["path/to/file"], obligation_refs: ["R-001 | AC-001 | policy ref"] }] }, null, 2), "```"];
494
518
  }
495
519
  if (schema !== "dd-flow/plan-review-result@1")
496
520
  return [];
497
- return [refs, "Use this complete minimal shape; do not add fields. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id when the coordinator classifies them:", "```json", JSON.stringify({ schema_id: schema, plan_revision: 1, overall_verdict: "pass | watch | needs_changes | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | watch | needs_changes | blocked", summary: "Conclusion for this aspect.", evidence_refs: ["path/to/file", `run://${runId}/03-plan/plan.json`], findings: [{ finding_id: "FIND-001", severity: "high | medium | low | info", summary: "Problem, if any.", evidence_refs: ["path/to/file"] }] }] }, null, 2), "```"];
521
+ return [refs, "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Do not add fields. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id when the coordinator classifies them:", "```json", JSON.stringify({ schema_id: schema, plan_revision: 1, overall_verdict: "pass | watch | needs_changes | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | watch | needs_changes | blocked", summary: "Conclusion for this aspect.", evidence_refs: [], findings: [{ finding_id: "FIND-001", severity: "high | medium | low | info", summary: "Problem, if any.", evidence_refs: ["path/to/file"] }] }] }, null, 2), "```"];
498
522
  }
499
523
  export function validateCodeReviewResultIdentity(work, value) {
500
524
  const group = codeReviewGroup(work);
@@ -0,0 +1,50 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { codeExecutionEnvironment, runCheck } from "./code-checks.js";
6
+ import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedOwnerIsAlive, managedProcessStatus, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
7
+ /** CODE and MERGE prepare their own checkout through the same managed executor.
8
+ * Bootstrap is a project operation, not an alias classified by the engine as a test. */
9
+ export async function runWorkspaceBootstrap(context, input) {
10
+ const operationId = `bootstrap:${crypto.createHash("sha256").update(JSON.stringify([input.runId, input.workspaceRoot, input.artifactDir, input.command])).digest("hex")}`;
11
+ const pending = managedProcessStatus(context).find(item => item.operation_id === operationId && ["starting", "running", "stopping"].includes(item.state));
12
+ if (pending) {
13
+ if (processTreeIsAlive(pending) || (pending.state === "starting" && managedOwnerIsAlive(pending) !== false)) {
14
+ throw new AppError("bootstrap_in_progress", "Workspace preparation is still running; inspect its logs and wait for the existing process, do not launch another copy", 2, { process_id: pending.id, stdout_path: pending.stdout_path, stderr_path: pending.stderr_path });
15
+ }
16
+ const completionPath = path.join(path.dirname(pending.stdout_path), "completion.json");
17
+ let completion = null;
18
+ try {
19
+ completion = JSON.parse(fs.readFileSync(completionPath, "utf8"));
20
+ }
21
+ catch { /* Interrupted shell has no terminal marker. */ }
22
+ const exitCode = typeof completion?.exit_code === "number" ? completion.exit_code : null;
23
+ const error = exitCode === null ? "bootstrap ended without a completion marker; inspect retained logs before retrying" : null;
24
+ finishManagedProcess(context, { id: pending.id, leaseToken: pending.lease_token, state: exitCode === 0 ? "stopped" : "failed", reason: error ?? "bootstrap_completion_reconciled" });
25
+ return result(pending.id, pending.stdout_path, pending.stderr_path, completionPath, exitCode, error);
26
+ }
27
+ const directory = path.join(input.runHome, input.artifactDir, `bootstrap-${crypto.randomUUID()}`);
28
+ fs.mkdirSync(directory, { recursive: true });
29
+ const stdoutPath = path.join(directory, "stdout.log"), stderrPath = path.join(directory, "stderr.log"), completionPath = path.join(directory, "completion.json");
30
+ const managed = registerManagedProcess(context, { kind: "workspace-bootstrap", ownerId: operationId, ownerPid: process.pid, operationId, projectId: input.projectId, runId: input.runId, stdoutPath, stderrPath, metadata: { command: input.command, workspace_root: input.workspaceRoot } });
31
+ const stdout = fs.openSync(stdoutPath, "a"), stderr = fs.openSync(stderrPath, "a");
32
+ try {
33
+ input.progress?.(`workspace bootstrap started: ${input.command}; logs: ${directory}`);
34
+ const completed = await runCheck(input.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_CHECK_COMPLETION_FILE: completionPath }, stdout, stderr, elapsed => input.progress?.(`workspace bootstrap still running (${elapsed}s); logs: ${directory}; next report in 15s`), pid => confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), () => heartbeatManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token }));
35
+ const current = managedProcessStatus(context).find(item => item.id === managed.id);
36
+ if (processTreeIsAlive(current))
37
+ throw new AppError("bootstrap_tree_not_settled", "Bootstrap shell exited but still owns live processes; resources were retained for inspection", 2, { process_id: managed.id, stdout_path: stdoutPath, stderr_path: stderrPath });
38
+ finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: completed.exitCode === 0 && !completed.error ? "stopped" : "failed", reason: completed.error });
39
+ return result(managed.id, stdoutPath, stderrPath, completionPath, completed.exitCode, completed.error);
40
+ }
41
+ finally {
42
+ fs.closeSync(stdout);
43
+ fs.closeSync(stderr);
44
+ }
45
+ }
46
+ function result(id, stdout, stderr, completion, exitCode, error) {
47
+ // Full logs remain on disk; keep the returned prompt bounded.
48
+ const excerpt = (file) => fs.existsSync(file) ? fs.readFileSync(file, "utf8").slice(-64_000) : "";
49
+ return { exit_code: exitCode, stdout: excerpt(stdout), stderr: excerpt(stderr), error, process_id: id, stdout_path: stdout, stderr_path: stderr, completion_path: completion };
50
+ }
@@ -574,6 +574,7 @@ function migrate(db, dbPath) {
574
574
  lock_acquired_at TEXT,
575
575
  checkpoint TEXT NOT NULL DEFAULT 'queued',
576
576
  profile_hash TEXT,
577
+ replacement_of_merge_request_id TEXT,
577
578
  adapter_receipt_json TEXT,
578
579
  result_json TEXT,
579
580
  last_error_json TEXT,
@@ -585,8 +586,8 @@ function migrate(db, dbPath) {
585
586
  );
586
587
  CREATE INDEX IF NOT EXISTS idx_merge_requests_fifo
587
588
  ON merge_requests(project_id, status, created_at, merge_request_id);
588
- CREATE UNIQUE INDEX IF NOT EXISTS idx_merge_requests_run
589
- ON merge_requests(project_id, run_id);
589
+ CREATE INDEX IF NOT EXISTS idx_merge_requests_run
590
+ ON merge_requests(project_id, run_id, created_at);
590
591
  CREATE UNIQUE INDEX IF NOT EXISTS idx_merge_requests_active_project
591
592
  ON merge_requests(project_id)
592
593
  WHERE status IN ('active', 'waiting_user', 'action_required', 'recovery_required');
@@ -811,6 +812,11 @@ function migrate(db, dbPath) {
811
812
  ensureColumn(db, "runs", "run_root", "ALTER TABLE runs ADD COLUMN run_root TEXT");
812
813
  db.prepare("UPDATE runs SET run_root = COALESCE(run_root, run_home_path, run_dir) WHERE run_root IS NULL OR run_root = ''").run();
813
814
  ensureColumn(db, "merge_requests", "accepted_tree", "ALTER TABLE merge_requests ADD COLUMN accepted_tree TEXT");
815
+ ensureColumn(db, "merge_requests", "replacement_of_merge_request_id", "ALTER TABLE merge_requests ADD COLUMN replacement_of_merge_request_id TEXT");
816
+ // A failed integration gate may create a new immutable source candidate in
817
+ // the same RUN. The historical unique index made that repair path impossible.
818
+ db.exec("DROP INDEX IF EXISTS idx_merge_requests_run");
819
+ db.exec("CREATE INDEX IF NOT EXISTS idx_merge_requests_run ON merge_requests(project_id, run_id, created_at)");
814
820
  ensureColumn(db, "runs", "layout_version", "ALTER TABLE runs ADD COLUMN layout_version TEXT");
815
821
  ensureColumn(db, "runs", "artifact_root_kind", "ALTER TABLE runs ADD COLUMN artifact_root_kind TEXT");
816
822
  ensureColumn(db, "works", "payload_json", "ALTER TABLE works ADD COLUMN payload_json TEXT");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.11",
3
+ "version": "0.9.0-beta.14",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {