@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81

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