@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +404 -0
- package/README.md +65 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +81 -9
- package/dist/cli/run-cli.js +442 -46
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +121 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/cleanup.js +8 -5
- package/dist/services/cli-operation-classifier.js +11 -1
- package/dist/services/code-checks.js +543 -96
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +9 -0
- package/dist/services/engines.js +50 -19
- package/dist/services/eval-snapshots.js +639 -60
- package/dist/services/execution-policy.js +177 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +413 -168
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-command.js +74 -6
- package/dist/services/lifecycle-invocations.js +637 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +138 -22
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +75 -29
- package/dist/services/migrations.js +1 -1
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/projects.js +7 -3
- package/dist/services/prompts.js +4 -2
- package/dist/services/protocols.js +11 -2
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +211 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +31 -0
- package/dist/services/run-controller.js +733 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +327 -0
- package/dist/services/runs.js +280 -40
- package/dist/services/runtime-budget.js +362 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +543 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +210 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +12 -12
- package/dist/services/stage-context.js +2 -2
- package/dist/services/stage-lifecycle.js +32 -10
- package/dist/services/stage-pause.js +25 -13
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +149 -89
- package/dist/services/vnext-code.js +224 -216
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +135 -21
- package/dist/services/vnext-merge.js +258 -89
- package/dist/services/vnext-plan-review.js +75 -64
- package/dist/services/vnext-plan.js +77 -29
- package/dist/services/vnext-protocolize.js +21 -14
- package/dist/services/vnext-specify.js +26 -16
- package/dist/services/work-registry.js +605 -157
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +341 -31
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +20 -13
- 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,14 +10,16 @@ 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";
|
|
13
|
+
import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareVnextMergeSourceRepairAttempt } from "./runs.js";
|
|
11
14
|
import { flowCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
12
15
|
import { validateSchema } from "./schema-validation.js";
|
|
13
16
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
14
|
-
import { bindStageCoordinatorWork, createChildWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
15
|
-
import {
|
|
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";
|
|
16
20
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
17
21
|
import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
|
|
22
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
18
23
|
const stage = "merge";
|
|
19
24
|
const stageDir = vnextStageDirectory(stage);
|
|
20
25
|
export function isVnextMergeRun(context, input) {
|
|
@@ -23,12 +28,17 @@ export function isVnextMergeRun(context, input) {
|
|
|
23
28
|
}
|
|
24
29
|
/** Terminal CODE/CODE-REVIEW calls this once; it freezes source and creates the child MERGE Work. */
|
|
25
30
|
export function ensureVnextMergeRequest(context, input) {
|
|
31
|
+
return prepareVnextMergeRequest(context, input)();
|
|
32
|
+
}
|
|
33
|
+
/** Freeze filesystem evidence outside the stage writer lock; return only the SQL settlement. */
|
|
34
|
+
export function prepareVnextMergeRequest(context, input) {
|
|
26
35
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
27
36
|
const project = requireProjectByRoot(context, projectRoot);
|
|
28
37
|
const run = requireRun(context, project.id, input.runId);
|
|
29
|
-
|
|
38
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
39
|
+
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]);
|
|
30
40
|
if (existing)
|
|
31
|
-
return requestView(context, existing);
|
|
41
|
+
return () => requestView(context, requireRequest(context, existing.merge_request_id));
|
|
32
42
|
const settings = executionSettings(run);
|
|
33
43
|
const route = settings.merge_mode ?? "same_session";
|
|
34
44
|
const home = requireHome(run);
|
|
@@ -45,25 +55,30 @@ export function ensureVnextMergeRequest(context, input) {
|
|
|
45
55
|
throw new AppError("merge_source_invalid", "MERGE could not determine the target branch", 1, { target_branch: targetBranch });
|
|
46
56
|
const semantic = planChecks(run.workspace_root, protocols);
|
|
47
57
|
validateMergeAcceptance(run.workspace_root, protocols, semantic);
|
|
48
|
-
const
|
|
58
|
+
const acceptanceRefs = mergeAcceptanceRefs(run.workspace_root, protocols);
|
|
59
|
+
const effective = effectiveCheckDeclarations(targetWorkspace, semantic, ["work", "code", "readiness", "merge"]);
|
|
49
60
|
if (run.workspace_root !== targetWorkspace && effective.length === 0)
|
|
50
61
|
throw new AppError("merge_gate_missing", "A real source/target integration requires at least one semantic or policy merge check", 2, { run_id: run.id, protocol_ids: protocols });
|
|
51
62
|
const ignored = ignoredGitPaths(context, project.id);
|
|
52
63
|
const accepted = input.acceptedPaths ? [...new Set(input.acceptedPaths.filter((item) => !ignored.includes(item)))].sort() : null;
|
|
53
64
|
const freezeRoot = path.join(home, stageDir);
|
|
54
65
|
const freezeFile = path.join(freezeRoot, "source-freeze.json");
|
|
66
|
+
const gateFile = path.join(freezeRoot, "merge-gate.json");
|
|
55
67
|
fs.mkdirSync(freezeRoot, { recursive: true });
|
|
68
|
+
freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, acceptanceRefs, profileHash: readCodeCheckProfile(targetWorkspace).hash, now: context.now() });
|
|
56
69
|
let sourceCommit;
|
|
57
70
|
let sourcePaths;
|
|
58
71
|
if (fs.existsSync(freezeFile)) {
|
|
59
72
|
const frozen = JSON.parse(fs.readFileSync(freezeFile, "utf8"));
|
|
60
73
|
sourceCommit = frozen.run_id === run.id && typeof frozen.source_commit === "string" ? frozen.source_commit : "";
|
|
61
74
|
sourcePaths = Array.isArray(frozen.paths) ? frozen.paths : [];
|
|
62
|
-
|
|
75
|
+
// A retry sees a clean tree: the original paths are already in sourceCommit.
|
|
76
|
+
// Reuse that receipt, but never accept new edits merely because names match.
|
|
77
|
+
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)
|
|
63
78
|
throw new AppError("merge_source_freeze_conflict", "Existing MERGE source freeze does not match the accepted source", 2, { freeze_file: freezeFile });
|
|
64
79
|
}
|
|
65
80
|
else {
|
|
66
|
-
sourcePaths =
|
|
81
|
+
sourcePaths = meaningfulStatus(run.workspace_root, ignored);
|
|
67
82
|
if (accepted) {
|
|
68
83
|
const unexpected = sourcePaths.filter((item) => !accepted.includes(item));
|
|
69
84
|
const absent = accepted.filter((item) => !sourcePaths.includes(item));
|
|
@@ -79,28 +94,19 @@ export function ensureVnextMergeRequest(context, input) {
|
|
|
79
94
|
if (!sourceCommit)
|
|
80
95
|
throw new AppError("merge_source_invalid", "MERGE could not freeze source commit", 1, { source_commit: sourceCommit, target_branch: targetBranch });
|
|
81
96
|
const enqueueTarget = gitValue(targetWorkspace, ["rev-parse", targetBranch], true);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const raced = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ?", [project.id, run.id]);
|
|
89
|
-
if (raced) {
|
|
90
|
-
context.db.exec("COMMIT");
|
|
97
|
+
return () => context.db.writeTransaction(() => {
|
|
98
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
99
|
+
const root = requireRootWork(context, project.id, run.id);
|
|
100
|
+
const now = context.now();
|
|
101
|
+
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]);
|
|
102
|
+
if (raced)
|
|
91
103
|
return requestView(context, raced);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
requestId
|
|
95
|
-
context
|
|
96
|
-
context
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
context.db.exec("ROLLBACK");
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
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 });
|
|
103
|
-
return requestView(context, requireRequest(context, requestId));
|
|
104
|
+
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 } });
|
|
105
|
+
const requestId = nextMergeRequestId(context);
|
|
106
|
+
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]);
|
|
107
|
+
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 });
|
|
108
|
+
return requestView(context, requireRequest(context, requestId));
|
|
109
|
+
});
|
|
104
110
|
}
|
|
105
111
|
export async function startVnextMerge(context, input) {
|
|
106
112
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -108,6 +114,7 @@ export async function startVnextMerge(context, input) {
|
|
|
108
114
|
const run = requireRun(context, project.id, input.runId);
|
|
109
115
|
const home = requireHome(run);
|
|
110
116
|
const request = requestForRun(context, project.id, run.id);
|
|
117
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
111
118
|
if (request.status === "completed")
|
|
112
119
|
return requestView(context, request);
|
|
113
120
|
const root = path.join(home, stageDir);
|
|
@@ -120,6 +127,7 @@ export async function startVnextMerge(context, input) {
|
|
|
120
127
|
if (!lane.acquired)
|
|
121
128
|
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" });
|
|
122
129
|
try {
|
|
130
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
123
131
|
if (["active", "action_required"].includes(request.status)) {
|
|
124
132
|
const pendingWork = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
125
133
|
const binding = pendingWork?.status === "created"
|
|
@@ -154,6 +162,10 @@ export async function startVnextMerge(context, input) {
|
|
|
154
162
|
}
|
|
155
163
|
catch (error) {
|
|
156
164
|
const latest = requireRequest(context, request.merge_request_id);
|
|
165
|
+
if (error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale"].includes(error.code)) {
|
|
166
|
+
releaseMergeLane(context, projectRoot, latest, `MERGE ${latest.merge_request_id} interrupted`);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
157
169
|
if (["active", "dispatching"].includes(latest.status))
|
|
158
170
|
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]);
|
|
159
171
|
releaseMergeLane(context, projectRoot, latest, `MERGE ${latest.merge_request_id} start failed`);
|
|
@@ -166,9 +178,14 @@ function startPacket(context, run, request, binding, promptPath, resultPath, pro
|
|
|
166
178
|
export function applyVnextMerge(context, input) {
|
|
167
179
|
const existing = requireRequest(context, input.requestId);
|
|
168
180
|
const receiptFile = applyReceiptPath(context, existing);
|
|
169
|
-
if (existing.executor_work_id === input.workId && ["apply_recorded", "integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(existing.checkpoint) && fs.existsSync(receiptFile))
|
|
170
|
-
return JSON.parse(fs.readFileSync(receiptFile, "utf8"));
|
|
171
181
|
const request = requireOwnedActiveRequest(context, input);
|
|
182
|
+
assertRunMutationAllowed(context, request.project_id, request.run_id);
|
|
183
|
+
if (["apply_recorded", "integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(request.checkpoint) && fs.existsSync(receiptFile)) {
|
|
184
|
+
const receipt = JSON.parse(fs.readFileSync(receiptFile, "utf8"));
|
|
185
|
+
if (receipt.merge_request_id !== request.merge_request_id || receipt.work_id !== request.executor_work_id || receipt.source_commit !== request.source_commit)
|
|
186
|
+
throw new AppError("merge_apply_receipt_invalid", "Existing merge apply receipt does not match the active request", 1, { receipt_path: receiptFile });
|
|
187
|
+
return receipt;
|
|
188
|
+
}
|
|
172
189
|
heartbeatLaneLock(context, { projectRoot: input.projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, ttlSeconds: 7200 });
|
|
173
190
|
assertExecutionBaseline(request);
|
|
174
191
|
input.progress?.(`applying ${request.source_commit} to ${request.target_branch}`);
|
|
@@ -195,10 +212,12 @@ export async function finishVnextMerge(context, input) {
|
|
|
195
212
|
const project = requireProjectByRoot(context, projectRoot);
|
|
196
213
|
const run = requireRun(context, project.id, input.runId);
|
|
197
214
|
const existing = requireRequest(context, input.requestId);
|
|
215
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
198
216
|
if (existing.project_id !== project.id || existing.run_id !== run.id || existing.executor_work_id !== input.workId)
|
|
199
217
|
throw new AppError("merge_request_mismatch", "MRG/Work/RUN/project identity does not match", 2);
|
|
200
218
|
if (existing.status === "completed") {
|
|
201
219
|
recoverCompletedMergeRun(context, run, projectRoot, existing);
|
|
220
|
+
releaseMergeLane(context, projectRoot, existing, `MERGE ${existing.merge_request_id} completion recovered`);
|
|
202
221
|
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" };
|
|
203
222
|
}
|
|
204
223
|
let request = requireOwnedActiveRequest(context, input);
|
|
@@ -210,49 +229,71 @@ export async function finishVnextMerge(context, input) {
|
|
|
210
229
|
if (unmerged(request.target_workspace).length)
|
|
211
230
|
throw new AppError("merge_conflicts_unresolved", "Resolve every unmerged path before finishing MERGE", 2, { paths: unmerged(request.target_workspace) });
|
|
212
231
|
const resultPath = path.join(requireHome(run), stageDir, "works", request.executor_work_id, "result.json");
|
|
213
|
-
validateSchema({ schemaName: "merge-result", file: resultPath, projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
232
|
+
validateSchema({ schemaName: "merge-result", file: resultPath, projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireHome(run) });
|
|
214
233
|
const semantic = JSON.parse(fs.readFileSync(resultPath, "utf8"));
|
|
215
234
|
if (semantic.outcome !== "completed")
|
|
216
235
|
throw new AppError("merge_semantic_blocked", "A blocked semantic result cannot complete MERGE", 2, { result_path: resultPath });
|
|
217
|
-
if (
|
|
218
|
-
commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
|
|
219
|
-
const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
|
|
220
|
-
context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [commit, context.now(), request.merge_request_id]);
|
|
221
|
-
request = requireRequest(context, request.merge_request_id);
|
|
222
|
-
}
|
|
223
|
-
else if (meaningfulStatus(request.target_workspace, ignoredGitPaths(context, project.id)).length) {
|
|
224
|
-
commitIntegration(request.target_workspace, `${run.id}-fix`, ignoredGitPaths(context, project.id));
|
|
225
|
-
context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [gitValue(request.target_workspace, ["rev-parse", "HEAD"]), context.now(), request.merge_request_id]);
|
|
226
|
-
request = requireRequest(context, request.merge_request_id);
|
|
227
|
-
}
|
|
228
|
-
if (request.checkpoint === "integration_committed") {
|
|
236
|
+
if (request.checkpoint === "apply_recorded") {
|
|
229
237
|
input.progress?.("bootstrapping integrated target");
|
|
230
|
-
runBootstrap(run, request.target_workspace);
|
|
238
|
+
await runBootstrap(context, run, request.target_workspace, request.merge_request_id, input.progress);
|
|
239
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
231
240
|
context.db.run("UPDATE merge_requests SET checkpoint = 'bootstrap_ready', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
|
|
232
241
|
request = requireRequest(context, request.merge_request_id);
|
|
233
242
|
}
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
const gate = readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id), checks = gate.checks;
|
|
244
|
+
if (input.retryCheckId) {
|
|
245
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
|
|
246
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
247
|
+
}
|
|
248
|
+
const acceptancePath = path.join(requireHome(run), stageDir, "merge-gate-acceptance.json");
|
|
249
|
+
const recovered = readMergeGateAcceptance(context, acceptancePath, request, gate);
|
|
250
|
+
const resumingGate = ["checks_passed", "integration_committed", "delivered"].includes(request.checkpoint);
|
|
251
|
+
if (resumingGate && !recovered.valid)
|
|
252
|
+
return recovery(context, request, "merge_evidence_missing", { checkpoint: request.checkpoint });
|
|
253
|
+
if (request.checkpoint === "checks_passed" && request.accepted_tree && gitValue(request.target_workspace, ["write-tree"]) !== request.accepted_tree)
|
|
254
|
+
return recovery(context, request, "merge_workspace_drift_after_checks", { accepted_tree: request.accepted_tree });
|
|
255
|
+
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 } : {}) });
|
|
256
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
239
257
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
240
258
|
if (failed.length) {
|
|
241
259
|
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]);
|
|
242
|
-
throw new AppError("merge_gate_failed", "Integrated target checks failed
|
|
260
|
+
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) });
|
|
243
261
|
}
|
|
244
|
-
const requiredRefs = mergeAcceptanceRefs(run.workspace_root, JSON.parse(request.protocol_ids_json));
|
|
245
262
|
const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
|
|
246
|
-
const missingRefs =
|
|
263
|
+
const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
|
|
247
264
|
if (missingRefs.length)
|
|
248
|
-
throw new AppError("merge_acceptance_unproven", "Current MERGE receipts do not cover every merge
|
|
249
|
-
|
|
250
|
-
|
|
265
|
+
throw new AppError("merge_acceptance_unproven", "Current MERGE receipts do not cover every frozen merge-gate reference", 2, { missing_check_refs: missingRefs });
|
|
266
|
+
if (!request.integration_commit) {
|
|
267
|
+
const acceptedTree = gitValue(request.target_workspace, ["write-tree"]);
|
|
268
|
+
writeMergeGateAcceptance(context, acceptancePath, request, gate, acceptedTree, receipts);
|
|
269
|
+
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]);
|
|
270
|
+
request = requireRequest(context, request.merge_request_id);
|
|
271
|
+
}
|
|
272
|
+
if (!request.integration_commit) {
|
|
273
|
+
commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
|
|
274
|
+
const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
|
|
275
|
+
const committedTree = gitValue(request.target_workspace, ["rev-parse", "HEAD^{tree}"]);
|
|
276
|
+
if (!request.accepted_tree || committedTree !== request.accepted_tree) {
|
|
277
|
+
return recovery(context, request, "merge_commit_tree_drift", { accepted_tree: request.accepted_tree, committed_tree: committedTree, integration_commit: commit });
|
|
278
|
+
}
|
|
279
|
+
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]);
|
|
280
|
+
request = requireRequest(context, request.merge_request_id);
|
|
281
|
+
}
|
|
282
|
+
if (request.integration_commit && request.accepted_tree && gitValue(request.target_workspace, ["rev-parse", `${request.integration_commit}^{tree}`]) !== request.accepted_tree)
|
|
283
|
+
return recovery(context, request, "merge_committed_tree_drift", { accepted_tree: request.accepted_tree, integration_commit: request.integration_commit });
|
|
251
284
|
verifyLocalDelivery(request);
|
|
252
285
|
context.db.run("UPDATE merge_requests SET checkpoint = 'delivered', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
|
|
253
286
|
request = requireRequest(context, request.merge_request_id);
|
|
254
287
|
performConfiguredCleanup(run, request);
|
|
255
|
-
|
|
288
|
+
const acceptedResult = fs.readFileSync(resultPath, "utf8");
|
|
289
|
+
const executor = context.db.get("SELECT status, result FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
290
|
+
if (!executor)
|
|
291
|
+
return recovery(context, request, "merge_executor_missing", { work_id: request.executor_work_id });
|
|
292
|
+
if (executor.status === "running")
|
|
293
|
+
await finishWork(context, request.executor_work_id, acceptedResult);
|
|
294
|
+
else if (executor.status !== "completed" || executor.result !== acceptedResult)
|
|
295
|
+
return recovery(context, request, "merge_executor_settlement_mismatch", { work_id: request.executor_work_id, status: executor.status });
|
|
296
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
256
297
|
const now = context.now();
|
|
257
298
|
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]);
|
|
258
299
|
const report = mergeReport(context, run, requireRequest(context, request.merge_request_id), semantic, receipts);
|
|
@@ -277,36 +318,125 @@ function ensureMergeStageAttached(context, run, projectRoot) {
|
|
|
277
318
|
function recoverCompletedMergeRun(context, run, projectRoot, request) {
|
|
278
319
|
const root = path.join(requireHome(run), stageDir);
|
|
279
320
|
const reportPath = path.join(root, "stage-report.json");
|
|
280
|
-
if (!fs.existsSync(reportPath))
|
|
281
|
-
|
|
321
|
+
if (!fs.existsSync(reportPath)) {
|
|
322
|
+
const semantic = request.result_json ? JSON.parse(request.result_json) : null;
|
|
323
|
+
const gate = readFrozenMergeGate(path.join(root, "merge-gate.json"), request.merge_request_id);
|
|
324
|
+
const accepted = readMergeGateAcceptance(context, path.join(root, "merge-gate-acceptance.json"), request, gate);
|
|
325
|
+
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)
|
|
326
|
+
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 });
|
|
327
|
+
verifyLocalDelivery(request);
|
|
328
|
+
performConfiguredCleanup(run, request);
|
|
329
|
+
writeStageReport(root, mergeReport(context, run, request, semantic, accepted.receipts));
|
|
330
|
+
}
|
|
282
331
|
ensureMergeStageAttached(context, run, projectRoot);
|
|
283
332
|
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" });
|
|
284
333
|
const rootWork = findRootWork(context, run.project_id, run.id);
|
|
285
334
|
const rootStatus = rootWork.status;
|
|
286
335
|
if (rootStatus === "running")
|
|
287
336
|
finishFanInWork(context, rootWork.work_id, fs.readFileSync(reportPath, "utf8"));
|
|
337
|
+
else if (rootStatus !== "completed")
|
|
338
|
+
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 });
|
|
288
339
|
const runStatus = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id])?.status;
|
|
289
340
|
if (runStatus !== "done")
|
|
290
341
|
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "merge_completed", nextAction: undefined });
|
|
291
342
|
refreshRunWorkProjection(context, run.project_id, run.id);
|
|
292
343
|
}
|
|
293
|
-
export function routeVnextMergeRequest(context, input) {
|
|
294
|
-
|
|
295
|
-
context.db.
|
|
296
|
-
|
|
297
|
-
|
|
344
|
+
export function routeVnextMergeRequest(context, input) {
|
|
345
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
346
|
+
context.db.beginWriteTransaction();
|
|
347
|
+
let request;
|
|
348
|
+
try {
|
|
349
|
+
request = requireRequest(context, input.requestId);
|
|
350
|
+
if (request.project_id !== project.id || request.status !== "queued")
|
|
351
|
+
throw new AppError("invalid_merge_state", "Only a queued request without an active dispatch may change route", 2, { status: request.status });
|
|
352
|
+
assertRunMutationAllowed(context, request.project_id, request.run_id);
|
|
353
|
+
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]);
|
|
354
|
+
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]);
|
|
355
|
+
context.db.exec("COMMIT");
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
context.db.exec("ROLLBACK");
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
refreshRunWorkProjection(context, request.project_id, request.run_id);
|
|
362
|
+
return requestView(context, requireRequest(context, request.merge_request_id));
|
|
298
363
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
364
|
+
/** Move a failed integration gate back to the source CODE/CODE-REVIEW loop. */
|
|
365
|
+
export async function repairVnextMerge(context, input) {
|
|
366
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
367
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
368
|
+
const request = requireRequest(context, input.requestId);
|
|
369
|
+
const generation = assertRunMutationAllowed(context, project.id, request.run_id);
|
|
370
|
+
if (request.project_id === project.id && request.status === "superseded" && errorCode(request) === "merge_source_repair") {
|
|
371
|
+
const saved = JSON.parse(request.last_error_json);
|
|
372
|
+
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
|
|
373
|
+
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`) } };
|
|
374
|
+
}
|
|
375
|
+
if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
|
|
376
|
+
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) });
|
|
377
|
+
}
|
|
378
|
+
abortIntegration(request);
|
|
379
|
+
const originWorkIds = sourceCodeWorkIds(context, request.project_id, request.run_id);
|
|
380
|
+
if (!originWorkIds.length)
|
|
381
|
+
throw new AppError("runtime_missing", "MERGE source repair requires accepted CODE Work evidence", 1, { run_id: request.run_id });
|
|
382
|
+
const failed = failedReceiptIds(request);
|
|
383
|
+
if (!failed.length)
|
|
384
|
+
throw new AppError("runtime_missing", "MERGE failure has no durable failed receipt", 1, { merge_request_id: request.merge_request_id });
|
|
385
|
+
// Settle the child while its canonical result path still exists; the next
|
|
386
|
+
// step archives the entire MERGE attempt as immutable evidence.
|
|
387
|
+
const executor = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
388
|
+
if (executor?.status !== "failed")
|
|
389
|
+
await failWork(context, request.executor_work_id, `Superseded by source repair after failed integration gate ${failed[0]}.`);
|
|
390
|
+
assertRunMutationAllowed(context, project.id, request.run_id, generation);
|
|
391
|
+
const prepared = prepareVnextMergeSourceRepairAttempt(context, { projectRoot, runId: request.run_id, mergeRequestId: request.merge_request_id });
|
|
392
|
+
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]}.` });
|
|
393
|
+
const now = context.now();
|
|
394
|
+
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]);
|
|
395
|
+
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
|
|
396
|
+
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 });
|
|
397
|
+
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`) } };
|
|
398
|
+
}
|
|
399
|
+
function mergePrompt(context, input) { 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(executionSettings(input.run).merge_delivery)}`, `- cleanup: ${JSON.stringify(executionSettings(input.run).merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...effectiveMergeChecks(input.run, input.request).map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails, 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"); }
|
|
400
|
+
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" } }; }
|
|
401
|
+
function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
|
|
306
402
|
function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
307
|
-
return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.
|
|
403
|
+
return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.availability === "available").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
|
|
308
404
|
function validateMergeAcceptance(workspace, protocols, checks) { const known = new Set(checks.map((check) => check.canonical_ref)); const missing = mergeAcceptanceRefs(workspace, protocols).filter((ref) => !known.has(ref)); if (missing.length)
|
|
309
|
-
throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a
|
|
405
|
+
throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a missing check", 2, { check_refs: missing }); }
|
|
406
|
+
function freezeMergeGate(file, input) { const canonical = JSON.stringify({ checks: input.checks, acceptance_refs: [...input.acceptanceRefs].sort() }); const hash = cryptoHash(canonical); if (fs.existsSync(file)) {
|
|
407
|
+
const existing = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
408
|
+
if (existing.run_id !== input.runId || existing.gate_hash !== hash)
|
|
409
|
+
throw new AppError("merge_gate_freeze_conflict", "MERGE gate is already frozen with different checks", 2, { file });
|
|
410
|
+
return;
|
|
411
|
+
} 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`); }
|
|
412
|
+
function readFrozenMergeGate(file, requestId) { try {
|
|
413
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
414
|
+
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() })))
|
|
415
|
+
throw new Error("invalid gate");
|
|
416
|
+
return { checks: value.checks, acceptanceRefs: [...value.acceptance_refs].sort(), profileHash: value.profile_hash ?? null, gateHash: value.gate_hash };
|
|
417
|
+
}
|
|
418
|
+
catch (cause) {
|
|
419
|
+
throw new AppError("merge_gate_missing", "MERGE gate is missing or invalid", 1, { merge_request_id: requestId, file, cause: String(cause) });
|
|
420
|
+
} }
|
|
421
|
+
function frozenGateRefs(gate) { return [...new Set([...gate.checks.map(check => check.canonical_ref ?? check.id), ...gate.acceptanceRefs])].sort(); }
|
|
422
|
+
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); }
|
|
423
|
+
function readMergeGateAcceptance(context, file, request, gate) { try {
|
|
424
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
425
|
+
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")))
|
|
426
|
+
return { valid: false, receipts: [] };
|
|
427
|
+
const all = checkReceipts(context, { projectId: request.project_id, runId: request.run_id });
|
|
428
|
+
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));
|
|
429
|
+
const bindingRefs = value.receipts.flatMap(receipt => receipt.binding_refs);
|
|
430
|
+
const bindings = new Set(bindingRefs);
|
|
431
|
+
const expected = frozenGateRefs(gate);
|
|
432
|
+
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)
|
|
433
|
+
return { valid: false, receipts: [] };
|
|
434
|
+
return { valid: true, receipts: receipts.map((receipt, index) => ({ ...receipt, check_refs: value.receipts[index].binding_refs })) };
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return { valid: false, receipts: [] };
|
|
438
|
+
} }
|
|
439
|
+
function cryptoHash(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|
|
310
440
|
function mergeAcceptanceRefs(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
311
441
|
return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.acceptance ?? []).filter((entry) => entry.gate === "merge").flatMap((entry) => (entry.check_refs ?? []).map((ref) => `${protocol}/${ref}`)); }); }
|
|
312
442
|
function protocolIds(home) { const root = path.join(home, "03-plan"); if (!fs.existsSync(root))
|
|
@@ -314,9 +444,14 @@ function protocolIds(home) { const root = path.join(home, "03-plan"); if (!fs.ex
|
|
|
314
444
|
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)
|
|
315
445
|
throw new AppError("workspace_route_missing", "MERGE requires the frozen integration branch", 1, { file }); return { integration_branch: value.policy.integration_branch }; }
|
|
316
446
|
function executionSettings(run) { return JSON.parse(run.index_json).execution_profile?.settings ?? {}; }
|
|
317
|
-
function runBootstrap(run, cwd
|
|
318
|
-
|
|
319
|
-
|
|
447
|
+
async function runBootstrap(context, run, cwd, mergeId, progress) {
|
|
448
|
+
const command = executionSettings(run).code_bootstrap?.command;
|
|
449
|
+
if (!command)
|
|
450
|
+
throw new AppError("execution_profile_invalid", "MERGE requires a frozen workspace bootstrap command", 2);
|
|
451
|
+
const result = await runWorkspaceBootstrap(context, { projectId: run.project_id, runId: run.id, runHome: requireHome(run), workspaceRoot: cwd, command, artifactDir: `${stageDir}/${mergeId}/readiness`, progress });
|
|
452
|
+
if (result.exit_code !== 0 || result.error)
|
|
453
|
+
throw new AppError("merge_bootstrap_failed", "Integrated target bootstrap failed; inspect its retained logs", 2, { command, ...result });
|
|
454
|
+
}
|
|
320
455
|
function verifyLocalDelivery(request) { const head = gitValue(request.target_workspace, ["rev-parse", request.target_branch]); if (!request.integration_commit || head !== request.integration_commit)
|
|
321
456
|
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 }); }
|
|
322
457
|
function cleanupReceiptPath(run) { return path.join(requireHome(run), stageDir, "cleanup-receipt.json"); }
|
|
@@ -344,24 +479,27 @@ function releaseMergeLane(context, projectRoot, request, reason) {
|
|
|
344
479
|
try {
|
|
345
480
|
releaseLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, reason });
|
|
346
481
|
}
|
|
347
|
-
catch {
|
|
482
|
+
catch (error) {
|
|
483
|
+
// An absent/replaced lease is already no longer ours. Storage and other
|
|
484
|
+
// failures must remain visible so a repeated operation can release it.
|
|
485
|
+
if (!(error instanceof AppError) || !["lane_lock_not_active", "lane_lock_owned_by_other_worker"].includes(error.code))
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
348
488
|
}
|
|
349
|
-
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; }
|
|
489
|
+
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; }
|
|
350
490
|
function queueStatus(context, request) { return { position: queueAhead(context, request) + 1, requests_ahead: queueAhead(context, request), status: request.status, route: request.execution_route }; }
|
|
351
491
|
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))
|
|
352
492
|
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; }
|
|
353
493
|
function assertExecutionBaseline(request) { const head = gitValue(request.target_workspace, ["rev-parse", "HEAD"], true); if (request.checkpoint === "baseline_locked" && head !== request.execution_target_head)
|
|
354
494
|
throw new AppError("merge_target_changed", "Integration target changed after the lane baseline was locked", 1, { expected: request.execution_target_head, actual: head }); }
|
|
355
495
|
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 }); }
|
|
356
|
-
function commitPendingSource(cwd, runId, ignored) { const paths = meaningfulStatus(cwd, ignored)
|
|
496
|
+
function commitPendingSource(cwd, runId, ignored) { const paths = meaningfulStatus(cwd, ignored); if (!paths.length)
|
|
357
497
|
return; git(cwd, ["add", "-A", "--", ...paths]); git(cwd, ["commit", "-m", `chore(${runId.toLowerCase()}): freeze accepted source`]); }
|
|
358
|
-
function commitIntegration(cwd, runId, ignored) { const mergeHead = fs.existsSync(path.join(gitDirectory(cwd), "MERGE_HEAD")); const paths = meaningfulStatus(cwd, ignored)
|
|
498
|
+
function commitIntegration(cwd, runId, ignored) { const mergeHead = fs.existsSync(path.join(gitDirectory(cwd), "MERGE_HEAD")); const paths = meaningfulStatus(cwd, ignored); if (!mergeHead && !paths.length)
|
|
359
499
|
return; if (paths.length)
|
|
360
500
|
git(cwd, ["add", "-A", "--", ...paths]); git(cwd, ["commit", "-m", `merge(${runId.toLowerCase()}): integrate accepted source`]); }
|
|
361
501
|
function ignoredGitPaths(context, projectId) { return [readProjectConfig(context, projectId).dashboard.markdown_path.replaceAll("\\", "/")]; }
|
|
362
|
-
function meaningfulStatus(cwd, ignored) {
|
|
363
|
-
return []; return result.stdout.split(/\r?\n/).filter(Boolean).filter((line) => !ignored.includes(statusPath(line))); }
|
|
364
|
-
function statusPath(line) { const value = line.slice(3); const renamed = value.includes(" -> ") ? value.slice(value.lastIndexOf(" -> ") + 4) : value; return renamed.replace(/^"|"$/g, ""); }
|
|
502
|
+
function meaningfulStatus(cwd, ignored) { return workspaceChangedPaths(cwd).filter((item) => !ignored.includes(item)); }
|
|
365
503
|
function unmerged(cwd) { const value = gitValue(cwd, ["diff", "--name-only", "--diff-filter=U"], true); return value.split(/\r?\n/).filter(Boolean); }
|
|
366
504
|
function gitDirectory(cwd) { const value = gitValue(cwd, ["rev-parse", "--git-dir"]); return path.resolve(cwd, value); }
|
|
367
505
|
function git(cwd, args) { try {
|
|
@@ -373,18 +511,49 @@ catch (error) {
|
|
|
373
511
|
function gitValue(cwd, args, allowFailure = false) { const result = spawnSync("git", args, { cwd, encoding: "utf8" }); if (result.status !== 0 && !allowFailure)
|
|
374
512
|
throw new AppError("git_operation_failed", `git ${args[0]} failed`, 1, { cwd, args, stderr: result.stderr }); return result.status === 0 ? result.stdout.trim() : ""; }
|
|
375
513
|
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"); }
|
|
376
|
-
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)
|
|
514
|
+
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)
|
|
377
515
|
throw new AppError("merge_request_missing", "MERGE request was not materialized by the prior terminal stage", 1, { run_id: runId }); return request; }
|
|
378
516
|
function requireRequest(context, id) { const request = context.db.get("SELECT * FROM merge_requests WHERE merge_request_id = ?", [id]); if (!request)
|
|
379
517
|
throw new AppError("not_found", "MERGE request is not registered", 1, { merge_request_id: id }); return request; }
|
|
380
|
-
function requestView(context, request) { return { ok: true, merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source: { workspace: request.source_workspace, branch: request.source_branch, commit: request.source_commit }, target: { workspace: request.target_workspace, branch: request.target_branch, enqueue_head: request.enqueue_target_head, execution_head: request.execution_target_head, integration_commit: request.integration_commit }, route: request.execution_route, status: request.status, checkpoint: request.checkpoint, queue: queueStatus(context, request), created_at: request.created_at, completed_at: request.completed_at }; }
|
|
381
|
-
function
|
|
518
|
+
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 }; }
|
|
519
|
+
function replacementOf(run) { try {
|
|
520
|
+
const variables = JSON.parse(run.index_json).variables;
|
|
521
|
+
const repair = variables?.["merge.source_repair"];
|
|
522
|
+
return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.merge_request_id === "string" ? String(repair.merge_request_id) : null;
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
return null;
|
|
526
|
+
} }
|
|
527
|
+
function errorCode(request) { try {
|
|
528
|
+
const value = JSON.parse(request.last_error_json ?? "{}");
|
|
529
|
+
return typeof value?.code === "string" ? value.code : null;
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return null;
|
|
533
|
+
} }
|
|
534
|
+
function failedReceiptIds(request) { try {
|
|
535
|
+
const value = JSON.parse(request.last_error_json ?? "{}");
|
|
536
|
+
return (value.failures ?? []).flatMap((failure) => typeof failure === "object" && failure !== null && typeof failure.id === "string" ? [String(failure.id)] : []);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return [];
|
|
540
|
+
} }
|
|
541
|
+
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 {
|
|
542
|
+
return JSON.parse(work.payload_json ?? "").schema_id === "dd-flow/code-work-packet@5" ? [work.work_id] : [];
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return [];
|
|
546
|
+
} }); }
|
|
547
|
+
function abortIntegration(request) { if (gitValue(request.target_workspace, ["rev-parse", "-q", "--verify", "MERGE_HEAD"], true))
|
|
548
|
+
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)
|
|
549
|
+
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 }); }
|
|
550
|
+
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
|
|
382
551
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
383
|
-
function requireHome(run) { if (!run.
|
|
384
|
-
throw new AppError("runtime_missing", "RUN
|
|
552
|
+
function requireHome(run) { if (!run.run_root)
|
|
553
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
385
554
|
function findRootWork(context, projectId, runId) { const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!work)
|
|
386
555
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return work; }
|
|
387
556
|
function requireRootWork(context, projectId, runId) { const work = findRootWork(context, projectId, runId); if (work.status !== "running")
|
|
388
557
|
throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
|
|
389
558
|
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`; }
|
|
390
|
-
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
|
|
559
|
+
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}`); }
|