@deksden-com/dd-flow-cli 0.9.0-beta.0 → 0.9.0-beta.10
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 +77 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +3 -3
- package/dist/cli/run-cli.js +127 -8
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +1 -1
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-decision.schema.json +1 -1
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/cleanup.js +18 -8
- package/dist/services/code-checks.js +194 -44
- package/dist/services/engines.js +4 -4
- package/dist/services/eval-snapshots.js +10 -5
- package/dist/services/harness-config.js +66 -0
- package/dist/services/hooks.js +25 -22
- package/dist/services/lanes.js +1 -0
- package/dist/services/managed-processes.js +169 -0
- package/dist/services/merge-server.js +8 -2
- package/dist/services/portable-refs.js +57 -0
- package/dist/services/prompts.js +4 -2
- package/dist/services/run-engine-bindings.js +19 -61
- package/dist/services/run-projection.js +10 -8
- package/dist/services/runs.js +71 -9
- package/dist/services/schema-validation.js +11 -11
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +26 -11
- package/dist/services/stage-lifecycle.js +15 -8
- package/dist/services/stage-pause.js +35 -20
- package/dist/services/usage.js +81 -57
- package/dist/services/vnext-code-review.js +82 -41
- package/dist/services/vnext-code.js +98 -34
- package/dist/services/vnext-fanout.js +5 -12
- package/dist/services/vnext-merge.js +144 -65
- package/dist/services/vnext-plan-review.js +50 -35
- package/dist/services/vnext-plan.js +69 -21
- package/dist/services/vnext-protocolize.js +6 -6
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +150 -40
- package/dist/storage/database.js +128 -2
- package/package.json +1 -1
- package/tools/audit-runtime-fix-boundaries.mjs +96 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
@@ -14,6 +15,7 @@ import { writeStageReport } from "./stage-report-renderer.js";
|
|
|
14
15
|
import { bindStageCoordinatorWork, createChildWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
15
16
|
import { checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
|
|
16
17
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
18
|
+
import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
|
|
17
19
|
const stage = "merge";
|
|
18
20
|
const stageDir = vnextStageDirectory(stage);
|
|
19
21
|
export function isVnextMergeRun(context, input) {
|
|
@@ -35,18 +37,51 @@ export function ensureVnextMergeRequest(context, input) {
|
|
|
35
37
|
const sourceBranch = gitValue(run.workspace_root, ["branch", "--show-current"]);
|
|
36
38
|
if (!sourceBranch)
|
|
37
39
|
throw new AppError("merge_source_invalid", "MERGE requires a named source branch", 1, { workspace_root: run.workspace_root });
|
|
38
|
-
commitPendingSource(run.workspace_root, run.id, ignoredGitPaths(context, project.id));
|
|
39
|
-
const sourceCommit = gitValue(run.workspace_root, ["rev-parse", "HEAD"]);
|
|
40
40
|
const targetBranch = workspaceRoute(home).integration_branch;
|
|
41
|
-
if (!sourceCommit || !targetBranch)
|
|
42
|
-
throw new AppError("merge_source_invalid", "MERGE could not freeze source commit or target branch", 1, { source_commit: sourceCommit, target_branch: targetBranch });
|
|
43
41
|
const targetWorkspace = projectRoot;
|
|
44
|
-
|
|
42
|
+
// Validate every immutable prerequisite before touching the source tree. A
|
|
43
|
+
// rejected MERGE handoff must not leave a new commit behind or let a caller
|
|
44
|
+
// subsequently mark CODE/CODE-REVIEW complete without a queue request.
|
|
45
|
+
if (!targetBranch)
|
|
46
|
+
throw new AppError("merge_source_invalid", "MERGE could not determine the target branch", 1, { target_branch: targetBranch });
|
|
45
47
|
const semantic = planChecks(run.workspace_root, protocols);
|
|
46
48
|
validateMergeAcceptance(run.workspace_root, protocols, semantic);
|
|
47
|
-
const effective = effectiveCheckDeclarations(
|
|
49
|
+
const effective = effectiveCheckDeclarations(targetWorkspace, semantic, ["work", "code", "readiness", "merge"]);
|
|
48
50
|
if (run.workspace_root !== targetWorkspace && effective.length === 0)
|
|
49
51
|
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 });
|
|
52
|
+
const ignored = ignoredGitPaths(context, project.id);
|
|
53
|
+
const accepted = input.acceptedPaths ? [...new Set(input.acceptedPaths.filter((item) => !ignored.includes(item)))].sort() : null;
|
|
54
|
+
const freezeRoot = path.join(home, stageDir);
|
|
55
|
+
const freezeFile = path.join(freezeRoot, "source-freeze.json");
|
|
56
|
+
const gateFile = path.join(freezeRoot, "merge-gate.json");
|
|
57
|
+
fs.mkdirSync(freezeRoot, { recursive: true });
|
|
58
|
+
freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, profileHash: readCodeCheckProfile(targetWorkspace).hash, now: context.now() });
|
|
59
|
+
let sourceCommit;
|
|
60
|
+
let sourcePaths;
|
|
61
|
+
if (fs.existsSync(freezeFile)) {
|
|
62
|
+
const frozen = JSON.parse(fs.readFileSync(freezeFile, "utf8"));
|
|
63
|
+
sourceCommit = frozen.run_id === run.id && typeof frozen.source_commit === "string" ? frozen.source_commit : "";
|
|
64
|
+
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)))
|
|
66
|
+
throw new AppError("merge_source_freeze_conflict", "Existing MERGE source freeze does not match the accepted source", 2, { freeze_file: freezeFile });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
sourcePaths = [...new Set(meaningfulStatus(run.workspace_root, ignored).map(statusPath))].sort();
|
|
70
|
+
if (accepted) {
|
|
71
|
+
const unexpected = sourcePaths.filter((item) => !accepted.includes(item));
|
|
72
|
+
const absent = accepted.filter((item) => !sourcePaths.includes(item));
|
|
73
|
+
if (unexpected.length || absent.length)
|
|
74
|
+
throw new AppError("merge_source_drift", "MERGE source differs from the accepted terminal-stage file set", 2, { unexpected, absent });
|
|
75
|
+
}
|
|
76
|
+
const sourceFingerprint = workspaceFingerprint(run.workspace_root);
|
|
77
|
+
commitPendingSource(run.workspace_root, run.id, ignored);
|
|
78
|
+
sourceCommit = gitValue(run.workspace_root, ["rev-parse", "HEAD"]);
|
|
79
|
+
if (sourceCommit)
|
|
80
|
+
fs.writeFileSync(freezeFile, `${JSON.stringify({ schema_id: "dd-flow/merge-source-freeze@1", run_id: run.id, source_workspace: run.workspace_root, source_branch: sourceBranch, source_commit: sourceCommit, precommit_workspace_fingerprint: sourceFingerprint, paths: sourcePaths, at: context.now() }, null, 2)}\n`);
|
|
81
|
+
}
|
|
82
|
+
if (!sourceCommit)
|
|
83
|
+
throw new AppError("merge_source_invalid", "MERGE could not freeze source commit", 1, { source_commit: sourceCommit, target_branch: targetBranch });
|
|
84
|
+
const enqueueTarget = gitValue(targetWorkspace, ["rev-parse", targetBranch], true);
|
|
50
85
|
const root = requireRootWork(context, project.id, run.id);
|
|
51
86
|
let child;
|
|
52
87
|
let requestId;
|
|
@@ -83,38 +118,50 @@ export async function startVnextMerge(context, input) {
|
|
|
83
118
|
fs.mkdirSync(workRoot, { recursive: true });
|
|
84
119
|
const resultPath = path.join(workRoot, "result.json");
|
|
85
120
|
const promptPath = path.join(workRoot, "prompt.md");
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
ensureLaneWorkspace(context, { projectRoot, lane: "merge", workspacePath: request.target_workspace, branch: request.target_branch });
|
|
122
|
+
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
|
+
if (!lane.acquired)
|
|
124
|
+
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
|
+
try {
|
|
126
|
+
if (["active", "action_required"].includes(request.status)) {
|
|
127
|
+
const pendingWork = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
128
|
+
const binding = pendingWork?.status === "created"
|
|
129
|
+
? startStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) })
|
|
130
|
+
: 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))
|
|
133
|
+
fs.writeFileSync(promptPath, prompt);
|
|
134
|
+
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
135
|
+
ensureMergeStageAttached(context, run, projectRoot);
|
|
136
|
+
return startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, externalContext);
|
|
137
|
+
}
|
|
138
|
+
const current = requireRequest(context, request.merge_request_id);
|
|
139
|
+
const targetHead = gitValue(current.target_workspace, ["rev-parse", current.target_branch], true);
|
|
140
|
+
const checkedOutTarget = gitValue(current.target_workspace, ["branch", "--show-current"], true);
|
|
141
|
+
if (checkedOutTarget !== current.target_branch)
|
|
142
|
+
throw new AppError("merge_target_branch_mismatch", "Integration workspace is not on the configured target branch", 1, { expected: current.target_branch, actual: checkedOutTarget, target_workspace: current.target_workspace });
|
|
143
|
+
const targetStatus = meaningfulStatus(current.target_workspace, ignoredGitPaths(context, project.id));
|
|
144
|
+
if (targetStatus.length)
|
|
145
|
+
throw new AppError("merge_target_dirty", "Integration workspace must be clean before MERGE starts", 1, { target_workspace: current.target_workspace, status: targetStatus });
|
|
146
|
+
const claimed = context.db.run("UPDATE merge_requests SET status = 'active', execution_target_head = ?, lock_acquired_at = ?, checkpoint = 'baseline_locked', updated_at = ? WHERE merge_request_id = ? AND status IN ('queued','dispatching','action_required')", [targetHead, context.now(), context.now(), current.merge_request_id]);
|
|
147
|
+
if (claimed.changes !== 1 && requireRequest(context, current.merge_request_id).status !== "active")
|
|
148
|
+
throw new AppError("merge_claim_conflict", "MERGE request could not acquire the integration lane", 1, { merge_request_id: current.merge_request_id });
|
|
149
|
+
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 });
|
|
151
|
+
fs.writeFileSync(promptPath, prompt);
|
|
94
152
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
95
|
-
|
|
96
|
-
|
|
153
|
+
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
|
+
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
|
|
155
|
+
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);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const latest = requireRequest(context, request.merge_request_id);
|
|
160
|
+
if (["active", "dispatching"].includes(latest.status))
|
|
161
|
+
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
|
+
releaseMergeLane(context, projectRoot, latest, `MERGE ${latest.merge_request_id} start failed`);
|
|
163
|
+
throw error;
|
|
97
164
|
}
|
|
98
|
-
await waitForTurn(context, request, input.progress);
|
|
99
|
-
const current = requireRequest(context, request.merge_request_id);
|
|
100
|
-
const targetHead = gitValue(current.target_workspace, ["rev-parse", current.target_branch], true);
|
|
101
|
-
const checkedOutTarget = gitValue(current.target_workspace, ["branch", "--show-current"], true);
|
|
102
|
-
if (checkedOutTarget !== current.target_branch)
|
|
103
|
-
throw new AppError("merge_target_branch_mismatch", "Integration workspace is not on the configured target branch", 1, { expected: current.target_branch, actual: checkedOutTarget, target_workspace: current.target_workspace });
|
|
104
|
-
const targetStatus = meaningfulStatus(current.target_workspace, ignoredGitPaths(context, project.id));
|
|
105
|
-
if (targetStatus.length)
|
|
106
|
-
throw new AppError("merge_target_dirty", "Integration workspace must be clean before MERGE starts", 1, { target_workspace: current.target_workspace, status: targetStatus });
|
|
107
|
-
const claimed = context.db.run("UPDATE merge_requests SET status = 'active', execution_target_head = ?, lock_acquired_at = ?, checkpoint = 'baseline_locked', updated_at = ? WHERE merge_request_id = ? AND status IN ('queued','dispatching','action_required')", [targetHead, context.now(), context.now(), current.merge_request_id]);
|
|
108
|
-
if (claimed.changes !== 1 && requireRequest(context, current.merge_request_id).status !== "active")
|
|
109
|
-
throw new AppError("merge_claim_conflict", "MERGE request could not acquire the integration lane", 1, { merge_request_id: current.merge_request_id });
|
|
110
|
-
const started = startStageCoordinatorWork(context, { workId: current.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
111
|
-
const prompt = mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot });
|
|
112
|
-
fs.writeFileSync(promptPath, prompt);
|
|
113
|
-
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
114
|
-
context.db.run("UPDATE work_sessions SET prompt_path = ?, result_path = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [promptPath, resultPath, context.now(), current.executor_work_id]);
|
|
115
|
-
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
|
|
116
|
-
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_started", merge_request_id: current.merge_request_id, work_id: current.executor_work_id, target_head: targetHead });
|
|
117
|
-
return startPacket(context, run, current, started, promptPath, resultPath, prompt, projectRoot, externalContext);
|
|
118
165
|
}
|
|
119
166
|
function startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, externalContext) {
|
|
120
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) } };
|
|
@@ -125,6 +172,7 @@ export function applyVnextMerge(context, input) {
|
|
|
125
172
|
if (existing.executor_work_id === input.workId && ["apply_recorded", "integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(existing.checkpoint) && fs.existsSync(receiptFile))
|
|
126
173
|
return JSON.parse(fs.readFileSync(receiptFile, "utf8"));
|
|
127
174
|
const request = requireOwnedActiveRequest(context, input);
|
|
175
|
+
heartbeatLaneLock(context, { projectRoot: input.projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, ttlSeconds: 7200 });
|
|
128
176
|
assertExecutionBaseline(request);
|
|
129
177
|
input.progress?.(`applying ${request.source_commit} to ${request.target_branch}`);
|
|
130
178
|
let outcome = "applied_clean";
|
|
@@ -157,6 +205,7 @@ export async function finishVnextMerge(context, input) {
|
|
|
157
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" };
|
|
158
206
|
}
|
|
159
207
|
let request = requireOwnedActiveRequest(context, input);
|
|
208
|
+
heartbeatLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, ttlSeconds: 7200 });
|
|
160
209
|
if (request.run_id !== run.id)
|
|
161
210
|
throw new AppError("merge_request_mismatch", "MRG does not belong to RUN", 2);
|
|
162
211
|
if (request.checkpoint === "baseline_locked" || !fs.existsSync(applyReceiptPath(context, request)))
|
|
@@ -164,22 +213,11 @@ export async function finishVnextMerge(context, input) {
|
|
|
164
213
|
if (unmerged(request.target_workspace).length)
|
|
165
214
|
throw new AppError("merge_conflicts_unresolved", "Resolve every unmerged path before finishing MERGE", 2, { paths: unmerged(request.target_workspace) });
|
|
166
215
|
const resultPath = path.join(requireHome(run), stageDir, "works", request.executor_work_id, "result.json");
|
|
167
|
-
validateSchema({ schemaName: "merge-result", file: resultPath, projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
216
|
+
validateSchema({ schemaName: "merge-result", file: resultPath, projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireHome(run) });
|
|
168
217
|
const semantic = JSON.parse(fs.readFileSync(resultPath, "utf8"));
|
|
169
218
|
if (semantic.outcome !== "completed")
|
|
170
219
|
throw new AppError("merge_semantic_blocked", "A blocked semantic result cannot complete MERGE", 2, { result_path: resultPath });
|
|
171
|
-
if (
|
|
172
|
-
commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
|
|
173
|
-
const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
|
|
174
|
-
context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [commit, context.now(), request.merge_request_id]);
|
|
175
|
-
request = requireRequest(context, request.merge_request_id);
|
|
176
|
-
}
|
|
177
|
-
else if (meaningfulStatus(request.target_workspace, ignoredGitPaths(context, project.id)).length) {
|
|
178
|
-
commitIntegration(request.target_workspace, `${run.id}-fix`, ignoredGitPaths(context, project.id));
|
|
179
|
-
context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [gitValue(request.target_workspace, ["rev-parse", "HEAD"]), context.now(), request.merge_request_id]);
|
|
180
|
-
request = requireRequest(context, request.merge_request_id);
|
|
181
|
-
}
|
|
182
|
-
if (request.checkpoint === "integration_committed") {
|
|
220
|
+
if (request.checkpoint === "apply_recorded") {
|
|
183
221
|
input.progress?.("bootstrapping integrated target");
|
|
184
222
|
runBootstrap(run, request.target_workspace);
|
|
185
223
|
context.db.run("UPDATE merge_requests SET checkpoint = 'bootstrap_ready', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
|
|
@@ -200,8 +238,19 @@ export async function finishVnextMerge(context, input) {
|
|
|
200
238
|
const missingRefs = requiredRefs.filter((ref) => !passedRefs.has(ref));
|
|
201
239
|
if (missingRefs.length)
|
|
202
240
|
throw new AppError("merge_acceptance_unproven", "Current MERGE receipts do not cover every merge acceptance reference", 2, { missing_check_refs: missingRefs });
|
|
203
|
-
|
|
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]);
|
|
204
243
|
request = requireRequest(context, request.merge_request_id);
|
|
244
|
+
if (!request.integration_commit) {
|
|
245
|
+
commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
|
|
246
|
+
const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
|
|
247
|
+
const committedTree = gitValue(request.target_workspace, ["rev-parse", "HEAD^{tree}"]);
|
|
248
|
+
if (!request.accepted_tree || committedTree !== request.accepted_tree) {
|
|
249
|
+
return recovery(context, request, "merge_commit_tree_drift", { accepted_tree: request.accepted_tree, committed_tree: committedTree, integration_commit: commit });
|
|
250
|
+
}
|
|
251
|
+
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
|
+
request = requireRequest(context, request.merge_request_id);
|
|
253
|
+
}
|
|
205
254
|
verifyLocalDelivery(request);
|
|
206
255
|
context.db.run("UPDATE merge_requests SET checkpoint = 'delivered', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
|
|
207
256
|
request = requireRequest(context, request.merge_request_id);
|
|
@@ -218,6 +267,7 @@ export async function finishVnextMerge(context, input) {
|
|
|
218
267
|
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "merge_completed", nextAction: undefined });
|
|
219
268
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
220
269
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_completed", merge_request_id: request.merge_request_id, integration_commit: requireRequest(context, request.merge_request_id).integration_commit });
|
|
270
|
+
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} completed`);
|
|
221
271
|
return { ok: true, run_id: run.id, stage, outcome: "completed", merge_request_id: request.merge_request_id, work_id: request.executor_work_id, integration_commit: requireRequest(context, request.merge_request_id).integration_commit, report_path: path.join(root, "stage-report.json"), next_action: "merge_completed" };
|
|
222
272
|
}
|
|
223
273
|
export function getVnextMergeRequest(context, input) { const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)); const request = input.requestId ? requireRequest(context, input.requestId) : input.runId ? requestForRun(context, project.id, input.runId) : null; if (!request || request.project_id !== project.id)
|
|
@@ -254,12 +304,28 @@ catch (error) {
|
|
|
254
304
|
throw error;
|
|
255
305
|
} refreshRunWorkProjection(context, request.project_id, request.run_id); return requestView(context, requireRequest(context, request.merge_request_id)); }
|
|
256
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"); }
|
|
257
|
-
function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
|
|
258
|
-
function effectiveMergeChecks(run, request) { return
|
|
307
|
+
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
|
+
function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
|
|
259
309
|
function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
260
|
-
return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.
|
|
310
|
+
return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.availability === "available").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
|
|
261
311
|
function validateMergeAcceptance(workspace, protocols, checks) { const known = new Set(checks.map((check) => check.canonical_ref)); const missing = mergeAcceptanceRefs(workspace, protocols).filter((ref) => !known.has(ref)); if (missing.length)
|
|
262
|
-
throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a
|
|
312
|
+
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)) {
|
|
314
|
+
const existing = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
315
|
+
if (existing.run_id !== input.runId || existing.checks_hash !== hash)
|
|
316
|
+
throw new AppError("merge_gate_freeze_conflict", "MERGE gate is already frozen with different checks", 2, { file });
|
|
317
|
+
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`); }
|
|
319
|
+
function readFrozenMergeGate(file, requestId) { try {
|
|
320
|
+
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)))
|
|
322
|
+
throw new Error("invalid gate");
|
|
323
|
+
return { checks: value.checks };
|
|
324
|
+
}
|
|
325
|
+
catch (cause) {
|
|
326
|
+
throw new AppError("merge_gate_missing", "MERGE gate is missing or invalid", 1, { merge_request_id: requestId, file, cause: String(cause) });
|
|
327
|
+
} }
|
|
328
|
+
function cryptoHash(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|
|
263
329
|
function mergeAcceptanceRefs(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
264
330
|
return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.acceptance ?? []).filter((entry) => entry.gate === "merge").flatMap((entry) => (entry.check_refs ?? []).map((ref) => `${protocol}/${ref}`)); }); }
|
|
265
331
|
function protocolIds(home) { const root = path.join(home, "03-plan"); if (!fs.existsSync(root))
|
|
@@ -277,15 +343,28 @@ function performConfiguredCleanup(run, request) {
|
|
|
277
343
|
const policy = executionSettings(run).merge_cleanup?.source ?? "retain";
|
|
278
344
|
const receipt = cleanupReceiptPath(run);
|
|
279
345
|
let action = "retained";
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
346
|
+
let error = null;
|
|
347
|
+
try {
|
|
348
|
+
if (policy === "delete_after_success" && path.resolve(request.source_workspace) !== path.resolve(request.target_workspace)) {
|
|
349
|
+
git(request.target_workspace, ["worktree", "remove", request.source_workspace]);
|
|
350
|
+
git(request.target_workspace, ["branch", "-d", request.source_branch]);
|
|
351
|
+
action = "deleted";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (cause) {
|
|
355
|
+
// Cleanup is post-delivery hygiene. It must never invalidate an integration
|
|
356
|
+
// commit which already passed the merge gate and was delivered.
|
|
357
|
+
action = "action_required";
|
|
358
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
359
|
+
}
|
|
360
|
+
fs.writeFileSync(receipt, `${JSON.stringify({ schema_id: "dd-flow/merge-cleanup-receipt@1", merge_request_id: request.merge_request_id, policy, action, source_workspace: request.source_workspace, source_branch: request.source_branch, ...(error ? { error } : {}), at: new Date().toISOString() }, null, 2)}\n`);
|
|
361
|
+
}
|
|
362
|
+
function releaseMergeLane(context, projectRoot, request, reason) {
|
|
363
|
+
try {
|
|
364
|
+
releaseLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, reason });
|
|
284
365
|
}
|
|
285
|
-
|
|
366
|
+
catch { /* An expired lease cannot invalidate an already completed MERGE. */ }
|
|
286
367
|
}
|
|
287
|
-
function waitForTurn(context, request, progress) { return new Promise((resolve) => { const poll = () => { const current = requireRequest(context, request.merge_request_id); const ahead = queueAhead(context, current); const active = context.db.get("SELECT merge_request_id FROM merge_requests WHERE project_id = ? AND status IN ('active','action_required','recovery_required') AND merge_request_id <> ? LIMIT 1", [current.project_id, current.merge_request_id]); if (ahead === 0 && !active)
|
|
288
|
-
return resolve(); progress?.(`MERGE ${current.merge_request_id} is waiting: ${ahead} request(s) ahead; next update in 15 seconds`); setTimeout(poll, 15_000); }; poll(); }); }
|
|
289
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; }
|
|
290
369
|
function queueStatus(context, request) { return { position: queueAhead(context, request) + 1, requests_ahead: queueAhead(context, request), status: request.status, route: request.execution_route }; }
|
|
291
370
|
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))
|
|
@@ -317,11 +396,11 @@ function requestForRun(context, projectId, runId) { const request = context.db.g
|
|
|
317
396
|
throw new AppError("merge_request_missing", "MERGE request was not materialized by the prior terminal stage", 1, { run_id: runId }); return request; }
|
|
318
397
|
function requireRequest(context, id) { const request = context.db.get("SELECT * FROM merge_requests WHERE merge_request_id = ?", [id]); if (!request)
|
|
319
398
|
throw new AppError("not_found", "MERGE request is not registered", 1, { merge_request_id: id }); return request; }
|
|
320
|
-
function requestView(context, request) { return { ok: true, merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source: { workspace: request.source_workspace, branch: request.source_branch, commit: request.source_commit }, target: { workspace: request.target_workspace, branch: request.target_branch, enqueue_head: request.enqueue_target_head, execution_head: request.execution_target_head, integration_commit: request.integration_commit }, route: request.execution_route, status: request.status, checkpoint: request.checkpoint, queue: queueStatus(context, request), created_at: request.created_at, completed_at: request.completed_at }; }
|
|
321
|
-
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, project_root, workspace_root,
|
|
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 }; }
|
|
400
|
+
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
|
|
322
401
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
323
|
-
function requireHome(run) { if (!run.
|
|
324
|
-
throw new AppError("runtime_missing", "RUN
|
|
402
|
+
function requireHome(run) { if (!run.run_root)
|
|
403
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
325
404
|
function findRootWork(context, projectId, runId) { const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!work)
|
|
326
405
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return work; }
|
|
327
406
|
function requireRootWork(context, projectId, runId) { const work = findRootWork(context, projectId, runId); if (work.status !== "running")
|