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