@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +561 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +91 -15
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/run-cli.js +1506 -334
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  11. package/dist/harness-runtime/bin/dd-agy.mjs +39 -0
  12. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  13. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  14. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  15. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  16. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  17. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  18. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +449 -0
  19. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  21. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  22. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  23. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  24. package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
  25. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
  26. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  27. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -0
  28. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  29. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  30. package/dist/harness-runtime/lib/dd-zcode.mjs +941 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  33. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  34. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  35. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  36. package/dist/harness-runtime/lib/managed-daemon.mjs +267 -0
  37. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  38. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  39. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  40. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  41. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  42. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  43. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  44. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  45. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  46. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  47. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  48. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  49. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  50. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  51. package/dist/runtime/context.js +3 -1
  52. package/dist/schemas/agent-profile.schema.json +1 -1
  53. package/dist/schemas/code-review-result.schema.json +2 -2
  54. package/dist/schemas/code-work-batch.schema.json +4 -3
  55. package/dist/schemas/code-work-result.schema.json +4 -4
  56. package/dist/schemas/harness-config.schema.json +23 -0
  57. package/dist/schemas/plan-review-decision.schema.json +1 -1
  58. package/dist/schemas/plan-review-result.schema.json +2 -2
  59. package/dist/schemas/run-control-receipt.schema.json +99 -0
  60. package/dist/schemas/run-control-request.schema.json +36 -0
  61. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  62. package/dist/services/canon.js +11 -8
  63. package/dist/services/cleanup.js +75 -21
  64. package/dist/services/cli-operation-classifier.js +22 -24
  65. package/dist/services/code-checks.js +676 -79
  66. package/dist/services/codex-hook-delivery.js +28 -0
  67. package/dist/services/command-context.js +80 -0
  68. package/dist/services/config.js +11 -8
  69. package/dist/services/continuation-outcome.js +20 -0
  70. package/dist/services/controller-fanout.js +134 -0
  71. package/dist/services/dashboard.js +103 -31
  72. package/dist/services/engines.js +125 -80
  73. package/dist/services/eval-snapshots.js +838 -74
  74. package/dist/services/execution-policy.js +170 -0
  75. package/dist/services/external-work-launch.js +110 -0
  76. package/dist/services/harness-adapter.js +147 -24
  77. package/dist/services/harness-config.js +68 -0
  78. package/dist/services/hooks.js +609 -310
  79. package/dist/services/lanes.js +62 -53
  80. package/dist/services/lifecycle-command.js +103 -17
  81. package/dist/services/lifecycle-contract.js +30 -0
  82. package/dist/services/lifecycle-invocations.js +1128 -0
  83. package/dist/services/managed-daemon-binding.js +38 -0
  84. package/dist/services/managed-processes.js +425 -0
  85. package/dist/services/merge-queue.js +179 -104
  86. package/dist/services/merge-server.js +78 -27
  87. package/dist/services/migrations.js +14 -9
  88. package/dist/services/native-daemon-history.js +49 -0
  89. package/dist/services/native-session-control.js +39 -0
  90. package/dist/services/plan-runtime.js +19 -8
  91. package/dist/services/plans.js +28 -25
  92. package/dist/services/portable-refs.js +57 -0
  93. package/dist/services/projects.js +18 -5
  94. package/dist/services/prompts.js +27 -18
  95. package/dist/services/protocols.js +64 -20
  96. package/dist/services/recovery-observation-budget.js +61 -0
  97. package/dist/services/recovery-snapshot-database.js +107 -0
  98. package/dist/services/repair-intents.js +40 -0
  99. package/dist/services/run-control-receipt.js +56 -0
  100. package/dist/services/run-control-worker.js +363 -0
  101. package/dist/services/run-control.js +876 -0
  102. package/dist/services/run-controller-adapter.js +222 -0
  103. package/dist/services/run-controller-capture.js +136 -0
  104. package/dist/services/run-controller-process.js +189 -0
  105. package/dist/services/run-controller-recovery.js +222 -0
  106. package/dist/services/run-controller-state.js +36 -0
  107. package/dist/services/run-controller.js +803 -0
  108. package/dist/services/run-engine-bindings.js +20 -62
  109. package/dist/services/run-fork.js +138 -0
  110. package/dist/services/run-observations.js +112 -0
  111. package/dist/services/run-projection.js +10 -8
  112. package/dist/services/run-recovery-runtime.js +69 -0
  113. package/dist/services/run-recovery.js +343 -0
  114. package/dist/services/runs.js +437 -81
  115. package/dist/services/runtime-budget.js +439 -0
  116. package/dist/services/runtime-command.js +43 -0
  117. package/dist/services/runtime-scope-capture.js +50 -0
  118. package/dist/services/runtime-scope-control.js +450 -0
  119. package/dist/services/runtime-scope-resume.js +565 -0
  120. package/dist/services/runtime-scope-stop.js +99 -0
  121. package/dist/services/runtime-scope-worker.js +243 -0
  122. package/dist/services/runtime-service.js +97 -0
  123. package/dist/services/schema-validation.js +21 -19
  124. package/dist/services/session-identity.js +19 -0
  125. package/dist/services/sessions.js +61 -80
  126. package/dist/services/stage-blocker.js +17 -7
  127. package/dist/services/stage-context.js +44 -18
  128. package/dist/services/stage-lifecycle.js +112 -108
  129. package/dist/services/stage-pause.js +117 -73
  130. package/dist/services/stage-work-graph.js +35 -0
  131. package/dist/services/usage.js +108 -132
  132. package/dist/services/vnext-code-review.js +245 -98
  133. package/dist/services/vnext-code.js +355 -257
  134. package/dist/services/vnext-execution-profile.js +5 -3
  135. package/dist/services/vnext-fanout.js +166 -26
  136. package/dist/services/vnext-merge.js +413 -147
  137. package/dist/services/vnext-plan-review.js +182 -109
  138. package/dist/services/vnext-plan.js +199 -82
  139. package/dist/services/vnext-protocolize.js +133 -60
  140. package/dist/services/vnext-specify.js +94 -71
  141. package/dist/services/work-registry.js +813 -160
  142. package/dist/services/workspace-bootstrap.js +76 -0
  143. package/dist/services/worktrees.js +85 -35
  144. package/dist/shared/entity-references.js +8 -0
  145. package/dist/shared/errors.js +12 -0
  146. package/dist/storage/database.js +569 -16
  147. package/dist/storage/paths.js +17 -4
  148. package/dist/storage/work-references.js +23 -0
  149. package/dist/storage/writer-contract.js +77 -0
  150. package/dist/storage/writer-migration.js +102 -0
  151. package/package.json +24 -13
  152. package/tools/audit-runtime-fix-boundaries.mjs +96 -0
  153. package/tools/repair-paused-run-status.mjs +59 -0
@@ -1,3 +1,6 @@
1
+ import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import crypto from "node:crypto";
3
+ import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
1
4
  import fs from "node:fs";
2
5
  import path from "node:path";
3
6
  import { execFileSync, spawnSync } from "node:child_process";
@@ -7,13 +10,18 @@ import { resolveProjectRoot } from "../storage/paths.js";
7
10
  import { requireProjectByRoot } from "./projects.js";
8
11
  import { nextMergeRequestId } from "./ids.js";
9
12
  import { readProjectConfig } from "./config.js";
10
- import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
11
- import { flowCommand, stagePauseCommandTemplate } from "./stage-pause.js";
12
- import { validateSchema } from "./schema-validation.js";
13
+ import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareFlowRunStageAttachment, prepareVnextMergeSourceRepairAttempt } from "./runs.js";
14
+ import { stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
15
+ import { runtimeCommand, checkRetryArgs, recoveryEvidenceInput } from "./runtime-command.js";
13
16
  import { writeStageReport } from "./stage-report-renderer.js";
14
- import { bindStageCoordinatorWork, createChildWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
15
- import { checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
16
- import { applyExternalStageContext } from "./stage-context.js";
17
+ import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
18
+ import { addVnextCodeRepair } from "./vnext-code.js";
19
+ import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
20
+ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
21
+ import { ensureLaneWorkspace, prepareLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
22
+ import { assertRunMutationAllowed } from "./run-recovery.js";
23
+ import { assertStageStartHookEvent } from "./hooks.js";
24
+ import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
17
25
  const stage = "merge";
18
26
  const stageDir = vnextStageDirectory(stage);
19
27
  export function isVnextMergeRun(context, input) {
@@ -22,12 +30,17 @@ export function isVnextMergeRun(context, input) {
22
30
  }
23
31
  /** Terminal CODE/CODE-REVIEW calls this once; it freezes source and creates the child MERGE Work. */
24
32
  export function ensureVnextMergeRequest(context, input) {
33
+ return prepareVnextMergeRequest(context, input)();
34
+ }
35
+ /** Freeze filesystem evidence outside the stage writer lock; return only the SQL settlement. */
36
+ export function prepareVnextMergeRequest(context, input) {
25
37
  const projectRoot = resolveProjectRoot(input.projectRoot);
26
38
  const project = requireProjectByRoot(context, projectRoot);
27
39
  const run = requireRun(context, project.id, input.runId);
28
- 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]);
40
+ assertRunMutationAllowed(context, project.id, run.id);
41
+ 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]);
29
42
  if (existing)
30
- return requestView(context, existing);
43
+ return () => requestView(context, requireRequest(context, existing.merge_request_id));
31
44
  const settings = executionSettings(run);
32
45
  const route = settings.merge_mode ?? "same_session";
33
46
  const home = requireHome(run);
@@ -35,96 +48,170 @@ export function ensureVnextMergeRequest(context, input) {
35
48
  const sourceBranch = gitValue(run.workspace_root, ["branch", "--show-current"]);
36
49
  if (!sourceBranch)
37
50
  throw new AppError("merge_source_invalid", "MERGE requires a named source branch", 1, { workspace_root: run.workspace_root });
38
- commitPendingSource(run.workspace_root, run.id, ignoredGitPaths(context, project.id));
39
- const sourceCommit = gitValue(run.workspace_root, ["rev-parse", "HEAD"]);
40
51
  const targetBranch = workspaceRoute(home).integration_branch;
41
- if (!sourceCommit || !targetBranch)
42
- throw new AppError("merge_source_invalid", "MERGE could not freeze source commit or target branch", 1, { source_commit: sourceCommit, target_branch: targetBranch });
43
52
  const targetWorkspace = projectRoot;
44
- const enqueueTarget = gitValue(targetWorkspace, ["rev-parse", targetBranch], true);
53
+ // Validate every immutable prerequisite before touching the source tree. A
54
+ // rejected MERGE handoff must not leave a new commit behind or let a caller
55
+ // subsequently mark CODE/CODE-REVIEW complete without a queue request.
56
+ if (!targetBranch)
57
+ throw new AppError("merge_source_invalid", "MERGE could not determine the target branch", 1, { target_branch: targetBranch });
45
58
  const semantic = planChecks(run.workspace_root, protocols);
46
59
  validateMergeAcceptance(run.workspace_root, protocols, semantic);
47
- const effective = effectiveCheckDeclarations(run.workspace_root, semantic, ["merge"]);
60
+ const acceptanceRefs = mergeAcceptanceRefs(run.workspace_root, protocols);
61
+ const effective = effectiveCheckDeclarations(targetWorkspace, semantic, ["work", "code", "readiness", "merge"]);
48
62
  if (run.workspace_root !== targetWorkspace && effective.length === 0)
49
63
  throw new AppError("merge_gate_missing", "A real source/target integration requires at least one semantic or policy merge check", 2, { run_id: run.id, protocol_ids: protocols });
50
- const root = requireRootWork(context, project.id, run.id);
51
- let child;
52
- let requestId;
53
- const now = context.now();
54
- context.db.exec("BEGIN IMMEDIATE");
55
- try {
56
- const raced = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ?", [project.id, run.id]);
57
- if (raced) {
58
- context.db.exec("COMMIT");
59
- return requestView(context, raced);
60
- }
61
- 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 } });
62
- requestId = nextMergeRequestId(context);
63
- 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]);
64
- context.db.exec("COMMIT");
64
+ const ignored = ignoredGitPaths(context, project.id);
65
+ const accepted = input.acceptedPaths ? [...new Set(input.acceptedPaths.filter((item) => !ignored.includes(item)))].sort() : null;
66
+ const freezeRoot = path.join(home, stageDir);
67
+ const freezeFile = path.join(freezeRoot, "source-freeze.json");
68
+ const gateFile = path.join(freezeRoot, "merge-gate.json");
69
+ fs.mkdirSync(freezeRoot, { recursive: true });
70
+ freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, acceptanceRefs, profileHash: readCodeCheckProfile(targetWorkspace).hash, now: context.now() });
71
+ let sourceCommit;
72
+ let sourcePaths;
73
+ if (fs.existsSync(freezeFile)) {
74
+ const frozen = JSON.parse(fs.readFileSync(freezeFile, "utf8"));
75
+ sourceCommit = frozen.run_id === run.id && typeof frozen.source_commit === "string" ? frozen.source_commit : "";
76
+ sourcePaths = Array.isArray(frozen.paths) ? frozen.paths : [];
77
+ // A retry sees a clean tree: the original paths are already in sourceCommit.
78
+ // Reuse that receipt, but never accept new edits merely because names match.
79
+ if (!sourceCommit || frozen.source_workspace !== run.workspace_root || frozen.source_branch !== sourceBranch || gitValue(run.workspace_root, ["rev-parse", "HEAD"]) !== sourceCommit || meaningfulStatus(run.workspace_root, ignored).length)
80
+ throw new AppError("merge_source_freeze_conflict", "Existing MERGE source freeze does not match the accepted source", 2, { freeze_file: freezeFile });
65
81
  }
66
- catch (error) {
67
- context.db.exec("ROLLBACK");
68
- throw error;
82
+ else {
83
+ sourcePaths = meaningfulStatus(run.workspace_root, ignored);
84
+ if (accepted) {
85
+ const unexpected = sourcePaths.filter((item) => !accepted.includes(item));
86
+ const absent = accepted.filter((item) => !sourcePaths.includes(item));
87
+ if (unexpected.length || absent.length)
88
+ throw new AppError("merge_source_drift", "MERGE source differs from the accepted terminal-stage file set", 2, { unexpected, absent });
89
+ }
90
+ const sourceFingerprint = workspaceFingerprint(run.workspace_root);
91
+ commitPendingSource(run.workspace_root, run.id, ignored);
92
+ sourceCommit = gitValue(run.workspace_root, ["rev-parse", "HEAD"]);
93
+ if (sourceCommit)
94
+ fs.writeFileSync(freezeFile, `${JSON.stringify({ schema_id: "dd-flow/merge-source-freeze@1", run_id: run.id, source_workspace: run.workspace_root, source_branch: sourceBranch, source_commit: sourceCommit, precommit_workspace_fingerprint: sourceFingerprint, paths: sourcePaths, at: context.now() }, null, 2)}\n`);
69
95
  }
70
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_request_queued", merge_request_id: requestId, work_id: child.work_id, route, source_commit: sourceCommit, target_branch: targetBranch });
71
- return requestView(context, requireRequest(context, requestId));
96
+ if (!sourceCommit)
97
+ throw new AppError("merge_source_invalid", "MERGE could not freeze source commit", 1, { source_commit: sourceCommit, target_branch: targetBranch });
98
+ const enqueueTarget = gitValue(targetWorkspace, ["rev-parse", targetBranch], true);
99
+ return () => context.db.writeTransaction(() => {
100
+ assertRunMutationAllowed(context, project.id, run.id);
101
+ const root = requireRootWork(context, project.id, run.id);
102
+ const now = context.now();
103
+ 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]);
104
+ if (raced)
105
+ return requestView(context, raced);
106
+ const child = createChildWork(context, { parentWorkId: root.work_id, stage: "merge", 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 } });
107
+ const requestId = nextMergeRequestId(context);
108
+ 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]);
109
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_request_queued", merge_request_id: requestId, work_id: child.work_id, route, source_commit: sourceCommit, target_branch: targetBranch });
110
+ return requestView(context, requireRequest(context, requestId));
111
+ });
72
112
  }
73
- export async function startVnextMerge(context, input) {
113
+ export function prepareVnextMergeStart(context, input) {
74
114
  const projectRoot = resolveProjectRoot(input.projectRoot);
75
115
  const project = requireProjectByRoot(context, projectRoot);
76
116
  const run = requireRun(context, project.id, input.runId);
77
117
  const home = requireHome(run);
78
118
  const request = requestForRun(context, project.id, run.id);
119
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
120
+ const common = { projectRoot, project, run, request, generation };
79
121
  if (request.status === "completed")
122
+ return { ...common, terminal: true };
123
+ if (!["queued", "dispatching", "active", "action_required"].includes(request.status))
124
+ throw new AppError("merge_request_state_invalid", "MERGE request cannot be started in its current state", 1, { status: request.status, merge_request_id: request.merge_request_id });
125
+ const root = path.join(home, stageDir), workRoot = path.join(root, "works", request.executor_work_id);
126
+ const resultPath = path.join(workRoot, "result.json"), promptPath = path.join(workRoot, "prompt.md");
127
+ const work = context.db.get("SELECT status, project_id, run_id FROM works WHERE work_id = ?", [request.executor_work_id]);
128
+ if (!work || work.project_id !== project.id || work.run_id !== run.id || !["created", "running"].includes(work.status))
129
+ throw new AppError("runtime_artifact_invalid", "MERGE executor Work is missing or not startable", 1, { work_id: request.executor_work_id });
130
+ const index = JSON.parse(run.index_json);
131
+ if (!index.stage_runs?.some(entry => entry.stage === stage))
132
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
133
+ prepareLaneWorkspace(context, { projectRoot, lane: "merge", workspacePath: request.target_workspace, branch: request.target_branch });
134
+ const checks = effectiveMergeChecks(run, request), settings = executionSettings(run);
135
+ const existingPrompt = ["active", "action_required"].includes(request.status) && fs.existsSync(promptPath) ? fs.readFileSync(promptPath, "utf8") : null;
136
+ if (existingPrompt === null || input.externalContext)
137
+ prepareOutputFile(promptPath);
138
+ if (input.externalContext)
139
+ prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
140
+ return { ...common, terminal: false, root, workRoot, resultPath, promptPath, checks, settings, existingPrompt };
141
+ }
142
+ export async function startVnextMerge(context, input, prepared = prepareVnextMergeStart(context, input)) {
143
+ const { projectRoot, project, run, request, generation } = prepared;
144
+ if (prepared.terminal)
80
145
  return requestView(context, request);
81
- const root = path.join(home, stageDir);
82
- const workRoot = path.join(root, "works", request.executor_work_id);
146
+ const { root, workRoot, resultPath, promptPath, checks, settings, existingPrompt } = prepared;
147
+ assertRunMutationAllowed(context, project.id, run.id, generation);
148
+ assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
83
149
  fs.mkdirSync(workRoot, { recursive: true });
84
- const resultPath = path.join(workRoot, "result.json");
85
- const promptPath = path.join(workRoot, "prompt.md");
86
- if (["active", "action_required"].includes(request.status)) {
87
- const pendingWork = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
88
- const binding = pendingWork?.status === "created"
89
- ? startStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) })
90
- : bindStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
91
- const prompt = fs.existsSync(promptPath) ? fs.readFileSync(promptPath, "utf8") : mergePrompt(context, { run, request, resultPath, projectRoot });
92
- if (!fs.existsSync(promptPath))
93
- fs.writeFileSync(promptPath, prompt);
150
+ ensureLaneWorkspace(context, { projectRoot, lane: "merge", workspacePath: request.target_workspace, branch: request.target_branch });
151
+ const lane = await waitAcquireLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, timeoutSeconds: 3600, pollIntervalSeconds: 15, ttlSeconds: 7200, reason: `MERGE ${request.merge_request_id}`, ...(input.progress ? { progress: input.progress } : {}) });
152
+ if (!lane.acquired)
153
+ throw new AppError("merge_lane_wait_failed", "MERGE could not acquire the project integration lane", 1, { merge_request_id: request.merge_request_id, status: lane.status ?? "unknown" });
154
+ try {
155
+ assertRunMutationAllowed(context, project.id, run.id, generation);
156
+ if (["active", "action_required"].includes(request.status)) {
157
+ const pendingWork = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
158
+ const binding = pendingWork?.status === "created"
159
+ ? startStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) })
160
+ : bindStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
161
+ const prompt = existingPrompt ?? mergePrompt(context, { run, request, resultPath, projectRoot }, checks, settings);
162
+ if (existingPrompt === null)
163
+ fs.writeFileSync(promptPath, prompt);
164
+ const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
165
+ ensureMergeStageAttached(context, run, projectRoot);
166
+ return startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
167
+ }
168
+ const current = requireRequest(context, request.merge_request_id);
169
+ const targetHead = gitValue(current.target_workspace, ["rev-parse", current.target_branch], true);
170
+ const checkedOutTarget = gitValue(current.target_workspace, ["branch", "--show-current"], true);
171
+ if (checkedOutTarget !== current.target_branch)
172
+ throw new AppError("merge_target_branch_mismatch", "Integration workspace is not on the configured target branch", 1, { expected: current.target_branch, actual: checkedOutTarget, target_workspace: current.target_workspace });
173
+ const targetStatus = meaningfulStatus(current.target_workspace, ignoredGitPaths(context, project.id));
174
+ if (targetStatus.length)
175
+ throw new AppError("merge_target_dirty", "Integration workspace must be clean before MERGE starts", 1, { target_workspace: current.target_workspace, status: targetStatus });
176
+ const claimed = context.db.run("UPDATE merge_requests SET status = 'active', execution_target_head = ?, lock_acquired_at = ?, checkpoint = 'baseline_locked', updated_at = ? WHERE merge_request_id = ? AND status IN ('queued','dispatching','action_required')", [targetHead, context.now(), context.now(), current.merge_request_id]);
177
+ if (claimed.changes !== 1 && requireRequest(context, current.merge_request_id).status !== "active")
178
+ throw new AppError("merge_claim_conflict", "MERGE request could not acquire the integration lane", 1, { merge_request_id: current.merge_request_id });
179
+ const started = startStageCoordinatorWork(context, { workId: current.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
180
+ const prompt = mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot }, checks, settings);
181
+ fs.writeFileSync(promptPath, prompt);
94
182
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
95
- ensureMergeStageAttached(context, run, projectRoot);
96
- return startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, externalContext);
183
+ context.db.run("UPDATE work_sessions SET prompt_path = ?, result_path = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [promptPath, resultPath, context.now(), current.executor_work_id]);
184
+ attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
185
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_started", merge_request_id: current.merge_request_id, work_id: current.executor_work_id, target_head: targetHead });
186
+ return startPacket(context, run, current, started, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
187
+ }
188
+ catch (error) {
189
+ const latest = requireRequest(context, request.merge_request_id);
190
+ if (error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale"].includes(error.code)) {
191
+ releaseMergeLane(context, projectRoot, latest, `MERGE ${latest.merge_request_id} interrupted`);
192
+ throw error;
193
+ }
194
+ if (["active", "dispatching"].includes(latest.status))
195
+ context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_start_failed", error: error instanceof Error ? error.message : String(error) }), context.now(), latest.merge_request_id]);
196
+ releaseMergeLane(context, projectRoot, latest, `MERGE ${latest.merge_request_id} start failed`);
197
+ throw error;
97
198
  }
98
- await waitForTurn(context, request, input.progress);
99
- const current = requireRequest(context, request.merge_request_id);
100
- const targetHead = gitValue(current.target_workspace, ["rev-parse", current.target_branch], true);
101
- const checkedOutTarget = gitValue(current.target_workspace, ["branch", "--show-current"], true);
102
- if (checkedOutTarget !== current.target_branch)
103
- throw new AppError("merge_target_branch_mismatch", "Integration workspace is not on the configured target branch", 1, { expected: current.target_branch, actual: checkedOutTarget, target_workspace: current.target_workspace });
104
- const targetStatus = meaningfulStatus(current.target_workspace, ignoredGitPaths(context, project.id));
105
- if (targetStatus.length)
106
- throw new AppError("merge_target_dirty", "Integration workspace must be clean before MERGE starts", 1, { target_workspace: current.target_workspace, status: targetStatus });
107
- const claimed = context.db.run("UPDATE merge_requests SET status = 'active', execution_target_head = ?, lock_acquired_at = ?, checkpoint = 'baseline_locked', updated_at = ? WHERE merge_request_id = ? AND status IN ('queued','dispatching','action_required')", [targetHead, context.now(), context.now(), current.merge_request_id]);
108
- if (claimed.changes !== 1 && requireRequest(context, current.merge_request_id).status !== "active")
109
- throw new AppError("merge_claim_conflict", "MERGE request could not acquire the integration lane", 1, { merge_request_id: current.merge_request_id });
110
- const started = startStageCoordinatorWork(context, { workId: current.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
111
- const prompt = mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot });
112
- fs.writeFileSync(promptPath, prompt);
113
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
114
- context.db.run("UPDATE work_sessions SET prompt_path = ?, result_path = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [promptPath, resultPath, context.now(), current.executor_work_id]);
115
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
116
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_started", merge_request_id: current.merge_request_id, work_id: current.executor_work_id, target_head: targetHead });
117
- return startPacket(context, run, current, started, promptPath, resultPath, prompt, projectRoot, externalContext);
118
199
  }
119
- function startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, externalContext) {
120
- return { ok: true, run_id: run.id, stage, merge_request_id: request.merge_request_id, work_id: request.executor_work_id, session_binding: binding.session_binding ?? binding, prompt_path: promptPath, result_path: resultPath, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), queue: queueStatus(context, request), effective_checks: effectiveMergeChecks(run, request), next: { apply_command: applyCommand(context, request, projectRoot), finish_command: finishCommand(context, run.id, request, projectRoot) } };
200
+ function startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, checks, externalContext) {
201
+ return { ok: true, run_id: run.id, stage, merge_request_id: request.merge_request_id, work_id: request.executor_work_id, session_binding: binding.session_binding ?? binding, prompt_path: promptPath, result_path: resultPath, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), queue: queueStatus(context, request), effective_checks: checks, next: { apply_command: applyCommand(context, request, projectRoot), finish_command: finishCommand(context, run.id, request, projectRoot) } };
121
202
  }
122
203
  export function applyVnextMerge(context, input) {
123
204
  const existing = requireRequest(context, input.requestId);
124
205
  const receiptFile = applyReceiptPath(context, existing);
125
- if (existing.executor_work_id === input.workId && ["apply_recorded", "integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(existing.checkpoint) && fs.existsSync(receiptFile))
126
- return JSON.parse(fs.readFileSync(receiptFile, "utf8"));
127
206
  const request = requireOwnedActiveRequest(context, input);
207
+ assertRunMutationAllowed(context, request.project_id, request.run_id);
208
+ if (["apply_recorded", "integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(request.checkpoint) && fs.existsSync(receiptFile)) {
209
+ const receipt = JSON.parse(fs.readFileSync(receiptFile, "utf8"));
210
+ if (receipt.merge_request_id !== request.merge_request_id || receipt.work_id !== request.executor_work_id || receipt.source_commit !== request.source_commit)
211
+ throw new AppError("merge_apply_receipt_invalid", "Existing merge apply receipt does not match the active request", 1, { receipt_path: receiptFile });
212
+ return receipt;
213
+ }
214
+ heartbeatLaneLock(context, { projectRoot: input.projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, ttlSeconds: 7200 });
128
215
  assertExecutionBaseline(request);
129
216
  input.progress?.(`applying ${request.source_commit} to ${request.target_branch}`);
130
217
  let outcome = "applied_clean";
@@ -145,68 +232,102 @@ export function applyVnextMerge(context, input) {
145
232
  context.db.run("UPDATE merge_requests SET checkpoint = 'apply_recorded', result_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ apply_receipt: receiptFile }), context.now(), request.merge_request_id]);
146
233
  return { ok: true, ...receipt };
147
234
  }
148
- export async function finishVnextMerge(context, input) {
235
+ export function prepareVnextMergeFinish(context, input) {
149
236
  const projectRoot = resolveProjectRoot(input.projectRoot);
150
237
  const project = requireProjectByRoot(context, projectRoot);
151
238
  const run = requireRun(context, project.id, input.runId);
152
239
  const existing = requireRequest(context, input.requestId);
240
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
153
241
  if (existing.project_id !== project.id || existing.run_id !== run.id || existing.executor_work_id !== input.workId)
154
242
  throw new AppError("merge_request_mismatch", "MRG/Work/RUN/project identity does not match", 2);
155
- if (existing.status === "completed") {
156
- recoverCompletedMergeRun(context, run, projectRoot, existing);
157
- return { ok: true, resumed: true, run_id: run.id, stage, outcome: "completed", merge_request_id: existing.merge_request_id, work_id: existing.executor_work_id, integration_commit: existing.integration_commit, report_path: path.join(requireHome(run), stageDir, "stage-report.json"), next_action: "merge_completed" };
158
- }
159
- let request = requireOwnedActiveRequest(context, input);
243
+ if (existing.status === "completed")
244
+ return { projectRoot, project, run, existing, generation, terminal: true };
245
+ const request = requireOwnedActiveRequest(context, input);
160
246
  if (request.run_id !== run.id)
161
247
  throw new AppError("merge_request_mismatch", "MRG does not belong to RUN", 2);
162
248
  if (request.checkpoint === "baseline_locked" || !fs.existsSync(applyReceiptPath(context, request)))
163
249
  throw new AppError("merge_apply_required", "Run the exact merge apply command before MERGE finish", 2, { merge_request_id: request.merge_request_id, checkpoint: request.checkpoint });
164
- if (unmerged(request.target_workspace).length)
165
- throw new AppError("merge_conflicts_unresolved", "Resolve every unmerged path before finishing MERGE", 2, { paths: unmerged(request.target_workspace) });
250
+ const conflicts = unmerged(request.target_workspace);
251
+ if (conflicts.length)
252
+ throw new AppError("merge_conflicts_unresolved", "Resolve every unmerged path before finishing MERGE", 2, { paths: conflicts });
166
253
  const resultPath = path.join(requireHome(run), stageDir, "works", request.executor_work_id, "result.json");
167
- validateSchema({ schemaName: "merge-result", file: resultPath, projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id });
168
- const semantic = JSON.parse(fs.readFileSync(resultPath, "utf8"));
169
- if (semantic.outcome !== "completed")
254
+ const result = prepareJsonSchema(resultPath, "MERGE result", { schemaName: "merge-result", projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireHome(run) });
255
+ if (result.value.outcome !== "completed")
170
256
  throw new AppError("merge_semantic_blocked", "A blocked semantic result cannot complete MERGE", 2, { result_path: resultPath });
171
- if (!["integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(request.checkpoint)) {
172
- commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
173
- const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
174
- context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [commit, context.now(), request.merge_request_id]);
175
- request = requireRequest(context, request.merge_request_id);
176
- }
177
- else if (meaningfulStatus(request.target_workspace, ignoredGitPaths(context, project.id)).length) {
178
- commitIntegration(request.target_workspace, `${run.id}-fix`, ignoredGitPaths(context, project.id));
179
- context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [gitValue(request.target_workspace, ["rev-parse", "HEAD"]), context.now(), request.merge_request_id]);
180
- request = requireRequest(context, request.merge_request_id);
257
+ const gate = readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id);
258
+ if (input.retryCheckId)
259
+ validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: gate.checks, reason: input.retryReason });
260
+ return { projectRoot, project, run, existing, generation, terminal: false, request, resultPath, result, gate };
261
+ }
262
+ export async function finishVnextMerge(context, input, prepared = prepareVnextMergeFinish(context, input)) {
263
+ const { projectRoot, project, run, existing, generation } = prepared;
264
+ if (prepared.terminal) {
265
+ recoverCompletedMergeRun(context, run, projectRoot, existing);
266
+ releaseMergeLane(context, projectRoot, existing, `MERGE ${existing.merge_request_id} completion recovered`);
267
+ return { ok: true, resumed: true, run_id: run.id, stage, outcome: "completed", merge_request_id: existing.merge_request_id, work_id: existing.executor_work_id, integration_commit: existing.integration_commit, report_path: path.join(requireHome(run), stageDir, "stage-report.json"), next_action: "merge_completed" };
181
268
  }
182
- if (request.checkpoint === "integration_committed") {
269
+ let request = prepared.request;
270
+ heartbeatLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, ttlSeconds: 7200 });
271
+ const semantic = prepared.result.value;
272
+ if (request.checkpoint === "apply_recorded") {
183
273
  input.progress?.("bootstrapping integrated target");
184
- runBootstrap(run, request.target_workspace);
274
+ await runBootstrap(context, run, request.target_workspace, request.merge_request_id, input.progress);
275
+ assertRunMutationAllowed(context, project.id, run.id, generation);
185
276
  context.db.run("UPDATE merge_requests SET checkpoint = 'bootstrap_ready', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
186
277
  request = requireRequest(context, request.merge_request_id);
187
278
  }
188
- const checks = effectiveMergeChecks(run, request);
189
- const unchanged = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: request.target_workspace, declarations: checks });
190
- if (unchanged.length)
191
- throw new AppError("merge_check_repair_required", "MERGE checks already failed for the unchanged integrated tree", 2, { failures: unchanged });
192
- const receipts = request.checkpoint === "checks_passed" || request.checkpoint === "delivered" || request.checkpoint === "finalized" ? checkReceipts(context, { projectId: project.id, runId: run.id, workId: request.executor_work_id }).filter((receipt) => receipt.status === "passed" && receipt.before_fingerprint === workspaceFingerprint(request.target_workspace)) : await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: requireHome(run), workspaceRoot: request.target_workspace, workId: request.executor_work_id, artifactDir: path.join(stageDir, "works", request.executor_work_id), scope: "aggregate", checks, ...(input.progress ? { progress: input.progress } : {}) });
279
+ const gate = prepared.gate, checks = gate.checks;
280
+ if (input.retryCheckId)
281
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
282
+ const acceptancePath = path.join(requireHome(run), stageDir, "merge-gate-acceptance.json");
283
+ const recovered = readMergeGateAcceptance(context, acceptancePath, request, gate);
284
+ const resumingGate = ["checks_passed", "integration_committed", "delivered"].includes(request.checkpoint);
285
+ if (resumingGate && !recovered.valid)
286
+ return recovery(context, request, "merge_evidence_missing", { checkpoint: request.checkpoint });
287
+ if (request.checkpoint === "checks_passed" && request.accepted_tree && gitValue(request.target_workspace, ["write-tree"]) !== request.accepted_tree)
288
+ return recovery(context, request, "merge_workspace_drift_after_checks", { accepted_tree: request.accepted_tree });
289
+ const receipts = resumingGate ? recovered.receipts : await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: requireHome(run), workspaceRoot: request.target_workspace, workId: request.executor_work_id, artifactDir: path.join(stageDir, "works", request.executor_work_id), scope: "aggregate", checks, expectedProfileHash: gate.profileHash, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}), ...(input.progress ? { progress: input.progress } : {}) });
290
+ assertRunMutationAllowed(context, project.id, run.id, generation);
193
291
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
194
292
  if (failed.length) {
195
293
  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]);
196
- throw new AppError("merge_gate_failed", "Integrated target checks failed; repair in the same MERGE Work and retry finish", 2, { failures: failed });
294
+ throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: repairCommand(context, request, projectRoot), continuation: { ...recoveryEvidenceInput(run.run_root, failed[0].id), command_after_input: finishCommand(context, run.id, request, projectRoot, failed[0].id) } });
197
295
  }
198
- const requiredRefs = mergeAcceptanceRefs(run.workspace_root, JSON.parse(request.protocol_ids_json));
199
296
  const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
200
- const missingRefs = requiredRefs.filter((ref) => !passedRefs.has(ref));
297
+ const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
201
298
  if (missingRefs.length)
202
- throw new AppError("merge_acceptance_unproven", "Current MERGE receipts do not cover every merge acceptance reference", 2, { missing_check_refs: missingRefs });
203
- context.db.run("UPDATE merge_requests SET checkpoint = 'checks_passed', status = 'active', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
204
- request = requireRequest(context, request.merge_request_id);
299
+ throw new AppError("merge_acceptance_unproven", "Current MERGE receipts do not cover every frozen merge-gate reference", 2, { missing_check_refs: missingRefs });
300
+ if (!request.integration_commit) {
301
+ const acceptedTree = gitValue(request.target_workspace, ["write-tree"]);
302
+ writeMergeGateAcceptance(context, acceptancePath, request, gate, acceptedTree, receipts);
303
+ context.db.run("UPDATE merge_requests SET checkpoint = 'checks_passed', accepted_tree = ?, status = 'active', updated_at = ? WHERE merge_request_id = ?", [acceptedTree, context.now(), request.merge_request_id]);
304
+ request = requireRequest(context, request.merge_request_id);
305
+ }
306
+ if (!request.integration_commit) {
307
+ commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
308
+ const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
309
+ const committedTree = gitValue(request.target_workspace, ["rev-parse", "HEAD^{tree}"]);
310
+ if (!request.accepted_tree || committedTree !== request.accepted_tree) {
311
+ return recovery(context, request, "merge_commit_tree_drift", { accepted_tree: request.accepted_tree, committed_tree: committedTree, integration_commit: commit });
312
+ }
313
+ context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [commit, context.now(), request.merge_request_id]);
314
+ request = requireRequest(context, request.merge_request_id);
315
+ }
316
+ if (request.integration_commit && request.accepted_tree && gitValue(request.target_workspace, ["rev-parse", `${request.integration_commit}^{tree}`]) !== request.accepted_tree)
317
+ return recovery(context, request, "merge_committed_tree_drift", { accepted_tree: request.accepted_tree, integration_commit: request.integration_commit });
205
318
  verifyLocalDelivery(request);
206
319
  context.db.run("UPDATE merge_requests SET checkpoint = 'delivered', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
207
320
  request = requireRequest(context, request.merge_request_id);
208
321
  performConfiguredCleanup(run, request);
209
- await finishWork(context, request.executor_work_id, fs.readFileSync(resultPath, "utf8"));
322
+ const acceptedResult = prepared.result.text;
323
+ const executor = context.db.get("SELECT status, result FROM works WHERE work_id = ?", [request.executor_work_id]);
324
+ if (!executor)
325
+ return recovery(context, request, "merge_executor_missing", { work_id: request.executor_work_id });
326
+ if (executor.status === "running")
327
+ await finishWork(context, request.executor_work_id, acceptedResult);
328
+ else if (executor.status !== "completed" || executor.result !== acceptedResult)
329
+ return recovery(context, request, "merge_executor_settlement_mismatch", { work_id: request.executor_work_id, status: executor.status });
330
+ assertRunMutationAllowed(context, project.id, run.id, generation);
210
331
  const now = context.now();
211
332
  context.db.run("UPDATE merge_requests SET status = 'completed', checkpoint = 'finalized', result_json = ?, completed_at = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify(semantic), now, now, request.merge_request_id]);
212
333
  const report = mergeReport(context, run, requireRequest(context, request.merge_request_id), semantic, receipts);
@@ -218,6 +339,7 @@ export async function finishVnextMerge(context, input) {
218
339
  completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "merge_completed", nextAction: undefined });
219
340
  refreshRunWorkProjection(context, project.id, run.id);
220
341
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_completed", merge_request_id: request.merge_request_id, integration_commit: requireRequest(context, request.merge_request_id).integration_commit });
342
+ releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} completed`);
221
343
  return { ok: true, run_id: run.id, stage, outcome: "completed", merge_request_id: request.merge_request_id, work_id: request.executor_work_id, integration_commit: requireRequest(context, request.merge_request_id).integration_commit, report_path: path.join(root, "stage-report.json"), next_action: "merge_completed" };
222
344
  }
223
345
  export function getVnextMergeRequest(context, input) { const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)); const request = input.requestId ? requireRequest(context, input.requestId) : input.runId ? requestForRun(context, project.id, input.runId) : null; if (!request || request.project_id !== project.id)
@@ -230,36 +352,125 @@ function ensureMergeStageAttached(context, run, projectRoot) {
230
352
  function recoverCompletedMergeRun(context, run, projectRoot, request) {
231
353
  const root = path.join(requireHome(run), stageDir);
232
354
  const reportPath = path.join(root, "stage-report.json");
233
- if (!fs.existsSync(reportPath))
234
- throw new AppError("merge_report_missing", "Completed MERGE request has no stage report", 1, { merge_request_id: request.merge_request_id, report_path: reportPath });
355
+ if (!fs.existsSync(reportPath)) {
356
+ const semantic = request.result_json ? JSON.parse(request.result_json) : null;
357
+ const gate = readFrozenMergeGate(path.join(root, "merge-gate.json"), request.merge_request_id);
358
+ const accepted = readMergeGateAcceptance(context, path.join(root, "merge-gate-acceptance.json"), request, gate);
359
+ if (!semantic || typeof semantic.summary !== "string" || typeof semantic.verification_summary !== "string" || !Array.isArray(semantic.residual_risks) || !semantic.residual_risks.every(item => typeof item === "string") || !accepted.valid)
360
+ throw new AppError("merge_report_missing", "Completed MERGE has insufficient authoritative evidence to rebuild its report", 1, { merge_request_id: request.merge_request_id, report_path: reportPath });
361
+ verifyLocalDelivery(request);
362
+ performConfiguredCleanup(run, request);
363
+ writeStageReport(root, mergeReport(context, run, request, semantic, accepted.receipts));
364
+ }
235
365
  ensureMergeStageAttached(context, run, projectRoot);
236
366
  completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@2", report: "stage-report.md", stageReport: "stage-report.html" });
237
367
  const rootWork = findRootWork(context, run.project_id, run.id);
238
368
  const rootStatus = rootWork.status;
239
369
  if (rootStatus === "running")
240
370
  finishFanInWork(context, rootWork.work_id, fs.readFileSync(reportPath, "utf8"));
371
+ else if (rootStatus !== "completed")
372
+ throw new AppError("merge_root_settlement_mismatch", "Completed MERGE cannot recover a non-completed root Work", 1, { merge_request_id: request.merge_request_id, work_id: rootWork.work_id, status: rootStatus });
241
373
  const runStatus = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id])?.status;
242
374
  if (runStatus !== "done")
243
375
  completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "merge_completed", nextAction: undefined });
244
376
  refreshRunWorkProjection(context, run.project_id, run.id);
245
377
  }
246
- export function routeVnextMergeRequest(context, input) { const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)); const request = requireRequest(context, input.requestId); if (request.project_id !== project.id || request.status !== "queued")
247
- throw new AppError("invalid_merge_state", "Only a queued request without an active dispatch may change route", 2, { status: request.status }); context.db.exec("BEGIN IMMEDIATE"); try {
248
- context.db.run("UPDATE merge_requests SET execution_route = ?, last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = 'queued'", [input.mode, JSON.stringify({ route_reason: input.reason }), context.now(), request.merge_request_id]);
249
- context.db.run("UPDATE works SET launch_policy = ?, updated_at = ? WHERE work_id = ? AND status = 'created'", [input.mode === "server" ? "fresh_agent_required" : "reuse_allowed", context.now(), request.executor_work_id]);
250
- context.db.exec("COMMIT");
378
+ export function routeVnextMergeRequest(context, input) {
379
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
380
+ context.db.beginWriteTransaction();
381
+ let request;
382
+ try {
383
+ request = requireRequest(context, input.requestId);
384
+ if (request.project_id !== project.id || request.status !== "queued")
385
+ throw new AppError("invalid_merge_state", "Only a queued request without an active dispatch may change route", 2, { status: request.status });
386
+ assertRunMutationAllowed(context, request.project_id, request.run_id);
387
+ context.db.run("UPDATE merge_requests SET execution_route = ?, last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = 'queued'", [input.mode, JSON.stringify({ route_reason: input.reason }), context.now(), request.merge_request_id]);
388
+ context.db.run("UPDATE works SET launch_policy = ?, updated_at = ? WHERE work_id = ? AND status = 'created'", [input.mode === "server" ? "fresh_agent_required" : "reuse_allowed", context.now(), request.executor_work_id]);
389
+ context.db.exec("COMMIT");
390
+ }
391
+ catch (error) {
392
+ context.db.exec("ROLLBACK");
393
+ throw error;
394
+ }
395
+ refreshRunWorkProjection(context, request.project_id, request.run_id);
396
+ return requestView(context, requireRequest(context, request.merge_request_id));
251
397
  }
252
- catch (error) {
253
- context.db.exec("ROLLBACK");
254
- throw error;
255
- } refreshRunWorkProjection(context, request.project_id, request.run_id); return requestView(context, requireRequest(context, request.merge_request_id)); }
256
- 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"); }
257
- function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
258
- function effectiveMergeChecks(run, request) { return effectiveCheckDeclarations(request.target_workspace, planChecks(run.workspace_root, JSON.parse(request.protocol_ids_json)), ["merge"]); }
398
+ /** Move a failed integration gate back to the source CODE/CODE-REVIEW loop. */
399
+ export async function repairVnextMerge(context, input) {
400
+ const projectRoot = resolveProjectRoot(input.projectRoot);
401
+ const project = requireProjectByRoot(context, projectRoot);
402
+ const request = requireRequest(context, input.requestId);
403
+ const generation = assertRunMutationAllowed(context, project.id, request.run_id);
404
+ if (request.project_id === project.id && request.status === "superseded" && errorCode(request) === "merge_source_repair") {
405
+ const saved = JSON.parse(request.last_error_json);
406
+ releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
407
+ return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: { repair_work_id: saved.repair_work_id }, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } };
408
+ }
409
+ if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
410
+ 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) });
411
+ }
412
+ abortIntegration(request);
413
+ const originWorkIds = sourceCodeWorkIds(context, request.project_id, request.run_id);
414
+ if (!originWorkIds.length)
415
+ throw new AppError("runtime_missing", "MERGE source repair requires accepted CODE Work evidence", 1, { run_id: request.run_id });
416
+ const failed = failedReceiptIds(request);
417
+ if (!failed.length)
418
+ throw new AppError("runtime_missing", "MERGE failure has no durable failed receipt", 1, { merge_request_id: request.merge_request_id });
419
+ // Settle the child while its canonical result path still exists; the next
420
+ // step archives the entire MERGE attempt as immutable evidence.
421
+ const executor = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
422
+ if (executor?.status !== "failed")
423
+ await failWork(context, request.executor_work_id, `Superseded by source repair after failed integration gate ${failed[0]}.`);
424
+ assertRunMutationAllowed(context, project.id, request.run_id, generation);
425
+ const prepared = prepareVnextMergeSourceRepairAttempt(context, { projectRoot, runId: request.run_id, mergeRequestId: request.merge_request_id });
426
+ const repair = addVnextCodeRepair(context, { projectRoot, runId: request.run_id, stage: "code", checkReceiptId: failed[0], originWorkIds, reviewCycle: `MRR-${request.merge_request_id}`, objective: `Repair source behavior evidenced by failed integration check ${failed[0]}.` });
427
+ const now = context.now();
428
+ 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]);
429
+ releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
430
+ 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 });
431
+ 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: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } };
432
+ }
433
+ function mergePrompt(context, input, checks, settings) { const pause = stagePauseCommand(context, { runId: input.run.id, stage: "merge", workId: input.request.executor_work_id, projectRoot: input.projectRoot }); const repair = repairCommand(context, input.request, input.projectRoot); 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(settings.merge_delivery)}`, `- cleanup: ${JSON.stringify(settings.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>", ...checks.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, read its receipt and logs and determine the cause. For a product defect use ${repair}; it restores the target baseline and opens CODE → independent CODE-REVIEW → replacement MRG. For an environment failure restore the declared environment in this MERGE and write the returned recovery JSON file and use continuation.command_after_input. The CLI reruns the real gate and retains the old receipt. Never edit product code in the integration target to make a gate pass.`, "If a material conflict has no reasonable answer in accepted evidence, pause this same Work by writing the question file and then running the separate standalone command below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
434
+ 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" } }; }
435
+ function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
259
436
  function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
260
- return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.run_at === "merge").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
437
+ return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.availability === "available").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
261
438
  function validateMergeAcceptance(workspace, protocols, checks) { const known = new Set(checks.map((check) => check.canonical_ref)); const missing = mergeAcceptanceRefs(workspace, protocols).filter((ref) => !known.has(ref)); if (missing.length)
262
- throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a non-merge check", 2, { check_refs: missing }); }
439
+ throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a missing check", 2, { check_refs: missing }); }
440
+ function freezeMergeGate(file, input) { const canonical = JSON.stringify({ checks: input.checks, acceptance_refs: [...input.acceptanceRefs].sort() }); const hash = cryptoHash(canonical); if (fs.existsSync(file)) {
441
+ const existing = JSON.parse(fs.readFileSync(file, "utf8"));
442
+ if (existing.run_id !== input.runId || existing.gate_hash !== hash)
443
+ throw new AppError("merge_gate_freeze_conflict", "MERGE gate is already frozen with different checks", 2, { file });
444
+ return;
445
+ } fs.writeFileSync(file, `${JSON.stringify({ schema_id: "dd-flow/merge-gate@2", run_id: input.runId, protocols: input.protocols, checks: input.checks, acceptance_refs: [...input.acceptanceRefs].sort(), gate_hash: hash, profile_hash: input.profileHash, frozen_at: input.now }, null, 2)}\n`); }
446
+ function readFrozenMergeGate(file, requestId) { try {
447
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
448
+ if (value.schema_id !== "dd-flow/merge-gate@2" || !Array.isArray(value.checks) || !Array.isArray(value.acceptance_refs) || !value.acceptance_refs.every(ref => typeof ref === "string") || typeof value.gate_hash !== "string" || value.gate_hash !== cryptoHash(JSON.stringify({ checks: value.checks, acceptance_refs: [...value.acceptance_refs].sort() })))
449
+ throw new Error("invalid gate");
450
+ return { checks: value.checks, acceptanceRefs: [...value.acceptance_refs].sort(), profileHash: value.profile_hash ?? null, gateHash: value.gate_hash };
451
+ }
452
+ catch (cause) {
453
+ throw new AppError("merge_gate_missing", "MERGE gate is missing or invalid", 1, { merge_request_id: requestId, file, cause: String(cause) });
454
+ } }
455
+ function frozenGateRefs(gate) { return [...new Set([...gate.checks.map(check => check.canonical_ref ?? check.id), ...gate.acceptanceRefs])].sort(); }
456
+ function writeMergeGateAcceptance(context, file, request, gate, acceptedTree, receipts) { const persisted = new Map(checkReceipts(context, { projectId: request.project_id, runId: request.run_id }).map(receipt => [receipt.id, receipt])); const value = { schema_id: "dd-flow/merge-gate-acceptance@2", merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, gate_hash: gate.gateHash, profile_hash: gate.profileHash, accepted_tree: acceptedTree, receipts: receipts.map(receipt => ({ id: receipt.id, execution_refs: [...(persisted.get(receipt.id)?.check_refs ?? receipt.check_refs)].sort(), binding_refs: [...receipt.check_refs].sort(), input_hash: receipt.input_hash })) }; const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
457
+ function readMergeGateAcceptance(context, file, request, gate) { try {
458
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
459
+ if (value.schema_id !== "dd-flow/merge-gate-acceptance@2" || value.merge_request_id !== request.merge_request_id || value.run_id !== request.run_id || value.work_id !== request.executor_work_id || value.gate_hash !== gate.gateHash || value.profile_hash !== gate.profileHash || value.accepted_tree !== request.accepted_tree || !Array.isArray(value.receipts) || !value.receipts.every(receipt => receipt && typeof receipt.id === "string" && typeof receipt.input_hash === "string" && Array.isArray(receipt.execution_refs) && receipt.execution_refs.every(ref => typeof ref === "string") && Array.isArray(receipt.binding_refs) && receipt.binding_refs.every(ref => typeof ref === "string")))
460
+ return { valid: false, receipts: [] };
461
+ const all = checkReceipts(context, { projectId: request.project_id, runId: request.run_id });
462
+ const receipts = value.receipts.map(saved => all.find(receipt => receipt.id === saved.id && receipt.input_hash === saved.input_hash && JSON.stringify([...receipt.check_refs].sort()) === JSON.stringify(saved.execution_refs))).filter((receipt) => Boolean(receipt));
463
+ const bindingRefs = value.receipts.flatMap(receipt => receipt.binding_refs);
464
+ const bindings = new Set(bindingRefs);
465
+ const expected = frozenGateRefs(gate);
466
+ if (receipts.length !== value.receipts.length || bindingRefs.length !== bindings.size || !receipts.every(checkReceiptEvidenceValid) || JSON.stringify([...bindings].sort()) !== JSON.stringify(expected) || gitValue(request.target_workspace, ["write-tree"]) !== value.accepted_tree)
467
+ return { valid: false, receipts: [] };
468
+ return { valid: true, receipts: receipts.map((receipt, index) => ({ ...receipt, check_refs: value.receipts[index].binding_refs })) };
469
+ }
470
+ catch {
471
+ return { valid: false, receipts: [] };
472
+ } }
473
+ function cryptoHash(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
263
474
  function mergeAcceptanceRefs(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
264
475
  return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.acceptance ?? []).filter((entry) => entry.gate === "merge").flatMap((entry) => (entry.check_refs ?? []).map((ref) => `${protocol}/${ref}`)); }); }
265
476
  function protocolIds(home) { const root = path.join(home, "03-plan"); if (!fs.existsSync(root))
@@ -267,9 +478,14 @@ function protocolIds(home) { const root = path.join(home, "03-plan"); if (!fs.ex
267
478
  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)
268
479
  throw new AppError("workspace_route_missing", "MERGE requires the frozen integration branch", 1, { file }); return { integration_branch: value.policy.integration_branch }; }
269
480
  function executionSettings(run) { return JSON.parse(run.index_json).execution_profile?.settings ?? {}; }
270
- function runBootstrap(run, cwd) { const command = executionSettings(run).code_bootstrap?.command; if (!command)
271
- return; const result = spawnSync("/bin/sh", ["-lc", command], { cwd, encoding: "utf8" }); if (result.status !== 0)
272
- throw new AppError("merge_bootstrap_failed", "Integrated target bootstrap failed", 2, { command, stdout: result.stdout, stderr: result.stderr, status: result.status }); }
481
+ async function runBootstrap(context, run, cwd, mergeId, progress) {
482
+ const command = executionSettings(run).code_bootstrap?.command;
483
+ if (!command)
484
+ throw new AppError("execution_profile_invalid", "MERGE requires a frozen workspace bootstrap command", 2);
485
+ const result = await runWorkspaceBootstrap(context, { projectId: run.project_id, runId: run.id, runHome: requireHome(run), workspaceRoot: cwd, command, artifactDir: `${stageDir}/${mergeId}/readiness`, progress });
486
+ if (result.exit_code !== 0 || result.error)
487
+ throw new AppError("merge_bootstrap_failed", "Integrated target bootstrap failed; inspect its retained logs", 2, { command, ...result });
488
+ }
273
489
  function verifyLocalDelivery(request) { const head = gitValue(request.target_workspace, ["rev-parse", request.target_branch]); if (!request.integration_commit || head !== request.integration_commit)
274
490
  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 }); }
275
491
  function cleanupReceiptPath(run) { return path.join(requireHome(run), stageDir, "cleanup-receipt.json"); }
@@ -277,31 +493,47 @@ function performConfiguredCleanup(run, request) {
277
493
  const policy = executionSettings(run).merge_cleanup?.source ?? "retain";
278
494
  const receipt = cleanupReceiptPath(run);
279
495
  let action = "retained";
280
- if (policy === "delete_after_success" && path.resolve(request.source_workspace) !== path.resolve(request.target_workspace)) {
281
- git(request.target_workspace, ["worktree", "remove", request.source_workspace]);
282
- git(request.target_workspace, ["branch", "-d", request.source_branch]);
283
- action = "deleted";
496
+ let error = null;
497
+ try {
498
+ if (policy === "delete_after_success" && path.resolve(request.source_workspace) !== path.resolve(request.target_workspace)) {
499
+ git(request.target_workspace, ["worktree", "remove", request.source_workspace]);
500
+ git(request.target_workspace, ["branch", "-d", request.source_branch]);
501
+ action = "deleted";
502
+ }
503
+ }
504
+ catch (cause) {
505
+ // Cleanup is post-delivery hygiene. It must never invalidate an integration
506
+ // commit which already passed the merge gate and was delivered.
507
+ action = "action_required";
508
+ error = cause instanceof Error ? cause.message : String(cause);
509
+ }
510
+ fs.writeFileSync(receipt, `${JSON.stringify({ schema_id: "dd-flow/merge-cleanup-receipt@1", merge_request_id: request.merge_request_id, policy, action, source_workspace: request.source_workspace, source_branch: request.source_branch, ...(error ? { error } : {}), at: new Date().toISOString() }, null, 2)}\n`);
511
+ }
512
+ function releaseMergeLane(context, projectRoot, request, reason) {
513
+ try {
514
+ releaseLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, reason });
515
+ }
516
+ catch (error) {
517
+ // An absent/replaced lease is already no longer ours. Storage and other
518
+ // failures must remain visible so a repeated operation can release it.
519
+ if (!(error instanceof AppError) || !["lane_lock_not_active", "lane_lock_owned_by_other_worker"].includes(error.code))
520
+ throw error;
284
521
  }
285
- fs.writeFileSync(receipt, `${JSON.stringify({ schema_id: "dd-flow/merge-cleanup-receipt@1", merge_request_id: request.merge_request_id, policy, action, source_workspace: request.source_workspace, source_branch: request.source_branch, at: new Date().toISOString() }, null, 2)}\n`);
286
522
  }
287
- function waitForTurn(context, request, progress) { return new Promise((resolve) => { const poll = () => { const current = requireRequest(context, request.merge_request_id); const ahead = queueAhead(context, current); const active = context.db.get("SELECT merge_request_id FROM merge_requests WHERE project_id = ? AND status IN ('active','action_required','recovery_required') AND merge_request_id <> ? LIMIT 1", [current.project_id, current.merge_request_id]); if (ahead === 0 && !active)
288
- return resolve(); progress?.(`MERGE ${current.merge_request_id} is waiting: ${ahead} request(s) ahead; next update in 15 seconds`); setTimeout(poll, 15_000); }; poll(); }); }
289
- 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; }
523
+ 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; }
290
524
  function queueStatus(context, request) { return { position: queueAhead(context, request) + 1, requests_ahead: queueAhead(context, request), status: request.status, route: request.execution_route }; }
291
525
  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))
292
526
  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; }
293
527
  function assertExecutionBaseline(request) { const head = gitValue(request.target_workspace, ["rev-parse", "HEAD"], true); if (request.checkpoint === "baseline_locked" && head !== request.execution_target_head)
294
528
  throw new AppError("merge_target_changed", "Integration target changed after the lane baseline was locked", 1, { expected: request.execution_target_head, actual: head }); }
295
529
  function recovery(context, request, code, detail) { context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code, detail }), context.now(), request.merge_request_id]); throw new AppError("merge_recovery_required", "MERGE mutation could not be reconciled automatically", 1, { merge_request_id: request.merge_request_id, code, detail }); }
296
- function commitPendingSource(cwd, runId, ignored) { const paths = meaningfulStatus(cwd, ignored).map(statusPath); if (!paths.length)
530
+ function commitPendingSource(cwd, runId, ignored) { const paths = meaningfulStatus(cwd, ignored); if (!paths.length)
297
531
  return; git(cwd, ["add", "-A", "--", ...paths]); git(cwd, ["commit", "-m", `chore(${runId.toLowerCase()}): freeze accepted source`]); }
298
- function commitIntegration(cwd, runId, ignored) { const mergeHead = fs.existsSync(path.join(gitDirectory(cwd), "MERGE_HEAD")); const paths = meaningfulStatus(cwd, ignored).map(statusPath); if (!mergeHead && !paths.length)
532
+ function commitIntegration(cwd, runId, ignored) { const mergeHead = fs.existsSync(path.join(gitDirectory(cwd), "MERGE_HEAD")); const paths = meaningfulStatus(cwd, ignored); if (!mergeHead && !paths.length)
299
533
  return; if (paths.length)
300
534
  git(cwd, ["add", "-A", "--", ...paths]); git(cwd, ["commit", "-m", `merge(${runId.toLowerCase()}): integrate accepted source`]); }
301
535
  function ignoredGitPaths(context, projectId) { return [readProjectConfig(context, projectId).dashboard.markdown_path.replaceAll("\\", "/")]; }
302
- function meaningfulStatus(cwd, ignored) { const result = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd, encoding: "utf8" }); if (result.status !== 0)
303
- return []; return result.stdout.split(/\r?\n/).filter(Boolean).filter((line) => !ignored.includes(statusPath(line))); }
304
- function statusPath(line) { const value = line.slice(3); const renamed = value.includes(" -> ") ? value.slice(value.lastIndexOf(" -> ") + 4) : value; return renamed.replace(/^"|"$/g, ""); }
536
+ function meaningfulStatus(cwd, ignored) { return workspaceChangedPaths(cwd).filter((item) => !ignored.includes(item)); }
305
537
  function unmerged(cwd) { const value = gitValue(cwd, ["diff", "--name-only", "--diff-filter=U"], true); return value.split(/\r?\n/).filter(Boolean); }
306
538
  function gitDirectory(cwd) { const value = gitValue(cwd, ["rev-parse", "--git-dir"]); return path.resolve(cwd, value); }
307
539
  function git(cwd, args) { try {
@@ -313,18 +545,52 @@ catch (error) {
313
545
  function gitValue(cwd, args, allowFailure = false) { const result = spawnSync("git", args, { cwd, encoding: "utf8" }); if (result.status !== 0 && !allowFailure)
314
546
  throw new AppError("git_operation_failed", `git ${args[0]} failed`, 1, { cwd, args, stderr: result.stderr }); return result.status === 0 ? result.stdout.trim() : ""; }
315
547
  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"); }
316
- 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)
548
+ 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)
317
549
  throw new AppError("merge_request_missing", "MERGE request was not materialized by the prior terminal stage", 1, { run_id: runId }); return request; }
318
550
  function requireRequest(context, id) { const request = context.db.get("SELECT * FROM merge_requests WHERE merge_request_id = ?", [id]); if (!request)
319
551
  throw new AppError("not_found", "MERGE request is not registered", 1, { merge_request_id: id }); return request; }
320
- function requestView(context, request) { return { ok: true, merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source: { workspace: request.source_workspace, branch: request.source_branch, commit: request.source_commit }, target: { workspace: request.target_workspace, branch: request.target_branch, enqueue_head: request.enqueue_target_head, execution_head: request.execution_target_head, integration_commit: request.integration_commit }, route: request.execution_route, status: request.status, checkpoint: request.checkpoint, queue: queueStatus(context, request), created_at: request.created_at, completed_at: request.completed_at }; }
321
- function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_home_path, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
552
+ 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 }; }
553
+ function replacementOf(run) { try {
554
+ const variables = JSON.parse(run.index_json).variables;
555
+ const repair = variables?.["merge.source_repair"];
556
+ return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.merge_request_id === "string" ? String(repair.merge_request_id) : null;
557
+ }
558
+ catch {
559
+ return null;
560
+ } }
561
+ function errorCode(request) { try {
562
+ const value = JSON.parse(request.last_error_json ?? "{}");
563
+ return typeof value?.code === "string" ? value.code : null;
564
+ }
565
+ catch {
566
+ return null;
567
+ } }
568
+ function failedReceiptIds(request) { try {
569
+ const value = JSON.parse(request.last_error_json ?? "{}");
570
+ return (value.failures ?? []).flatMap((failure) => typeof failure === "object" && failure !== null && typeof failure.id === "string" ? [String(failure.id)] : []);
571
+ }
572
+ catch {
573
+ return [];
574
+ } }
575
+ 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 {
576
+ return JSON.parse(work.payload_json ?? "").schema_id === "dd-flow/code-work-packet@5" ? [work.work_id] : [];
577
+ }
578
+ catch {
579
+ return [];
580
+ } }); }
581
+ function abortIntegration(request) { if (gitValue(request.target_workspace, ["rev-parse", "-q", "--verify", "MERGE_HEAD"], true))
582
+ 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)
583
+ 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 }); }
584
+ 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)
322
585
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
323
- function requireHome(run) { if (!run.run_home_path)
324
- throw new AppError("runtime_missing", "RUN workspace is unavailable", 1); return run.run_home_path; }
586
+ function requireHome(run) { if (!run.run_root)
587
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
325
588
  function findRootWork(context, projectId, runId) { const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!work)
326
589
  throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return work; }
327
590
  function requireRootWork(context, projectId, runId) { const work = findRootWork(context, projectId, runId); if (work.status !== "running")
328
591
  throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
329
- function applyCommand(context, request, projectRoot) { return `${flowCommand(context)} merge apply ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`; }
330
- function finishCommand(context, runId, request, projectRoot) { return `${flowCommand(context)} stage finish ${runId} --stage merge --request ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`; }
592
+ function applyCommand(context, request, projectRoot) { return managedLifecycleCommand(context, runtimeCommand(context, ["merge", "apply", request.merge_request_id, "--work", request.executor_work_id, "--project-root", projectRoot, "--json", "--progress-jsonl"])); }
593
+ function repairCommand(context, request, projectRoot) { return managedLifecycleCommand(context, runtimeCommand(context, ["merge", "repair", request.merge_request_id, "--project-root", projectRoot, "--json"])); }
594
+ export function finishCommand(context, runId, request, projectRoot, retryCheckId) {
595
+ return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "merge", "--request", request.merge_request_id, "--work", request.executor_work_id, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
596
+ }