@deksden-com/dd-flow-cli 0.9.0-beta.63 → 0.9.0-beta.64
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 +12 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/run-cli.js +45 -4
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -0
- package/dist/harness-runtime/lib/process-json.mjs +15 -6
- package/dist/services/cleanup.js +1 -1
- package/dist/services/code-checks.js +1 -1
- package/dist/services/eval-snapshots.js +2 -2
- package/dist/services/harness-adapter.js +61 -9
- package/dist/services/hooks.js +26 -0
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-invocations.js +194 -22
- package/dist/services/managed-processes.js +2 -2
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +1 -1
- package/dist/services/migrations.js +1 -1
- package/dist/services/projects.js +1 -1
- package/dist/services/protocols.js +2 -2
- package/dist/services/run-control-worker.js +3 -3
- package/dist/services/run-control.js +1 -1
- package/dist/services/run-controller-adapter.js +8 -3
- package/dist/services/run-controller-capture.js +2 -2
- package/dist/services/run-controller-process.js +2 -2
- package/dist/services/run-controller-recovery.js +3 -3
- package/dist/services/run-controller.js +297 -180
- package/dist/services/run-recovery-runtime.js +1 -1
- package/dist/services/run-recovery.js +3 -3
- package/dist/services/runs.js +13 -14
- package/dist/services/runtime-budget.js +4 -4
- package/dist/services/runtime-scope-control.js +5 -5
- package/dist/services/runtime-scope-resume.js +7 -7
- package/dist/services/runtime-scope-worker.js +2 -2
- package/dist/services/stage-pause.js +2 -2
- package/dist/services/vnext-code-review.js +9 -4
- package/dist/services/vnext-code.js +10 -3
- package/dist/services/vnext-merge.js +16 -21
- package/dist/services/vnext-plan-review.js +4 -2
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/work-registry.js +198 -121
- package/dist/storage/database.js +112 -20
- package/dist/storage/writer-contract.js +2 -1
- package/dist/storage/writer-migration.js +2 -2
- package/package.json +1 -1
|
@@ -109,7 +109,7 @@ export function beginRunRecovery(context, input) {
|
|
|
109
109
|
const interruption = parseObject(input.interruption, "interruption");
|
|
110
110
|
const recoveryId = `RCV-${crypto.randomUUID()}`, now = context.now();
|
|
111
111
|
let generation;
|
|
112
|
-
context.db.
|
|
112
|
+
context.db.beginWriteTransaction();
|
|
113
113
|
try {
|
|
114
114
|
const prior = recoveryGuard(context, project.id, run.id);
|
|
115
115
|
const supersedes = prior?.status === "resuming" && interruption.recovery_parent_id === prior.recovery_id;
|
|
@@ -206,7 +206,7 @@ export function resumeRunRecovery(context, input) {
|
|
|
206
206
|
verifyRecoverySnapshot(guard.capture_path, { runId: run.id, recoveryId: guard.recovery_id, projectRoot: run.project_root, workspaceRoot: run.workspace_root });
|
|
207
207
|
const now = context.now();
|
|
208
208
|
let running = [];
|
|
209
|
-
context.db.
|
|
209
|
+
context.db.beginWriteTransaction();
|
|
210
210
|
try {
|
|
211
211
|
const current = recoveryGuard(context, project.id, run.id);
|
|
212
212
|
if (!current || current.recovery_id !== guard.recovery_id || current.generation !== guard.generation || current.status !== "sealed" || current.capture_path !== guard.capture_path)
|
|
@@ -261,7 +261,7 @@ export function acceptRunRecovery(context, input) {
|
|
|
261
261
|
const identity = hookSessionIdentity(context, project.id, eventKey);
|
|
262
262
|
const now = context.now();
|
|
263
263
|
let packetReceipt;
|
|
264
|
-
context.db.
|
|
264
|
+
context.db.beginWriteTransaction();
|
|
265
265
|
try {
|
|
266
266
|
const guard = recoveryGuard(context, project.id, run.id);
|
|
267
267
|
const binding = context.db.get("SELECT * FROM run_recovery_bindings WHERE recovery_id = ?", [input.recoveryId]);
|
package/dist/services/runs.js
CHANGED
|
@@ -414,7 +414,7 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
414
414
|
const run = resolveRun(context, project.id, input.runId);
|
|
415
415
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
416
416
|
const idempotencyKey = requiredPlain(input.idempotencyKey, "idempotency-key");
|
|
417
|
-
context.db.
|
|
417
|
+
context.db.beginWriteTransaction();
|
|
418
418
|
let committed = false;
|
|
419
419
|
try {
|
|
420
420
|
const existingMutation = context.db.get("SELECT result_json FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? AND idempotency_key = ?", [project.id, run.id, idempotencyKey]);
|
|
@@ -585,19 +585,18 @@ export function attachFlowRunStage(context, input) {
|
|
|
585
585
|
}
|
|
586
586
|
/** Keep synchronous stage settlement atomic; files are projections, SQLite is authoritative. */
|
|
587
587
|
export function withStageSettlement(context, operation) {
|
|
588
|
-
context.db.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
throw
|
|
600
|
-
}
|
|
588
|
+
return context.db.writeTransaction(operation);
|
|
589
|
+
}
|
|
590
|
+
/** Capture accepted inputs before asynchronous validation; compare under the
|
|
591
|
+
* settlement writer reservation instead of accepting a stale read snapshot. */
|
|
592
|
+
export function stageInputGuard(context, projectId, runId, readFiles) {
|
|
593
|
+
const read = () => JSON.stringify({
|
|
594
|
+
works: context.db.all("SELECT work_id, parent_work_id, task, status, result, payload_json, depends_on_json FROM works WHERE project_id = ? AND run_id = ? ORDER BY work_id", [projectId, runId]),
|
|
595
|
+
files: readFiles()
|
|
596
|
+
});
|
|
597
|
+
const accepted = read();
|
|
598
|
+
return () => { if (read() !== accepted)
|
|
599
|
+
throw new AppError("stage_inputs_changed", "Accepted stage inputs changed during validation; settle only newly validated inputs", 1, { run_id: runId, effect: "no_effect" }); };
|
|
601
600
|
}
|
|
602
601
|
export function completeFlowRunStage(context, input) {
|
|
603
602
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -69,7 +69,7 @@ export function requestRuntimeScopeControl(context, scopeId, requestId, mode, ma
|
|
|
69
69
|
throw new AppError("runtime_budget_invalid", "Scope control requires a bounded request identity and pause or stop", 2);
|
|
70
70
|
const db = getResourceDatabase(resourceHome(context));
|
|
71
71
|
const requestKey = digest(JSON.stringify([scopeId, requestId]));
|
|
72
|
-
db.
|
|
72
|
+
db.beginWriteTransaction();
|
|
73
73
|
try {
|
|
74
74
|
if (manifest !== undefined) {
|
|
75
75
|
const declaration = validateRuntimeScopeManifest(manifest, scopeId), payload = JSON.stringify(declaration);
|
|
@@ -131,7 +131,7 @@ export function scopeLocalProcessRole(record) {
|
|
|
131
131
|
/** Local checks and EVAL observers share the scope fence, not provider capacity. */
|
|
132
132
|
export function admitRuntimeCheck(context, id, leaseToken) {
|
|
133
133
|
const db = getResourceDatabase(resourceHome(context));
|
|
134
|
-
db.
|
|
134
|
+
db.beginWriteTransaction();
|
|
135
135
|
try {
|
|
136
136
|
const record = db.get("SELECT * FROM managed_processes WHERE id = ?", [id]);
|
|
137
137
|
if (!record || record.lease_token !== leaseToken || !["eval-baseline", "eval-observer", "check"].includes(record.kind) || record.state !== "running" || !processIsAlive(record))
|
|
@@ -189,7 +189,7 @@ export function fenceRuntimeBudget(context, scopeId, requestId) {
|
|
|
189
189
|
if (!requestId || requestId.length > 256)
|
|
190
190
|
throw new AppError("runtime_budget_invalid", "Scope fence requires a bounded request identity", 2);
|
|
191
191
|
const db = getResourceDatabase(resourceHome(context));
|
|
192
|
-
db.
|
|
192
|
+
db.beginWriteTransaction();
|
|
193
193
|
try {
|
|
194
194
|
const existing = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
|
|
195
195
|
const now = context.now();
|
|
@@ -248,7 +248,7 @@ export function reserveProviderTurn(context, record, input) {
|
|
|
248
248
|
const db = getResourceDatabase(resourceHome(context));
|
|
249
249
|
const key = digest(JSON.stringify([record.id, input.operationId]));
|
|
250
250
|
const policy = JSON.stringify(budget), now = context.now();
|
|
251
|
-
db.
|
|
251
|
+
db.beginWriteTransaction();
|
|
252
252
|
try {
|
|
253
253
|
const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
|
|
254
254
|
if (current?.lease_token !== record.lease_token || current.state !== "running")
|
|
@@ -23,7 +23,7 @@ export function retainScopeDrain(context, control, processes, runs, native, prob
|
|
|
23
23
|
const registry = getResourceDatabase(resourceHome(context));
|
|
24
24
|
const targets = new Map();
|
|
25
25
|
const captures = [];
|
|
26
|
-
registry.
|
|
26
|
+
registry.beginWriteTransaction();
|
|
27
27
|
try {
|
|
28
28
|
for (const run of [...runs].sort((a, b) => String(a.runtime_home).localeCompare(String(b.runtime_home)))) {
|
|
29
29
|
const home = String(run.runtime_home);
|
|
@@ -31,7 +31,7 @@ export function retainScopeDrain(context, control, processes, runs, native, prob
|
|
|
31
31
|
if (!target) {
|
|
32
32
|
target = createContext({ ...context.env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome(context), DD_FLOW_ENGINE_MODE: "1" });
|
|
33
33
|
try {
|
|
34
|
-
target.db.
|
|
34
|
+
target.db.beginWriteTransaction();
|
|
35
35
|
}
|
|
36
36
|
catch (error) {
|
|
37
37
|
target.db.close?.();
|
|
@@ -157,7 +157,7 @@ export async function controlRuntimeScopeLocalProbes(context, scopeId, generatio
|
|
|
157
157
|
if (mode === "pause" && processTreeIsAlive(current))
|
|
158
158
|
return { process_id: record.id, physical_settled: false, reason: "local_probe_running" };
|
|
159
159
|
const operationId = `scope-probe:${crypto.createHash("sha256").update(JSON.stringify([scopeId, generation, record.id, record.lease_token, record.pid_started_at])).digest("hex")}`;
|
|
160
|
-
registry.
|
|
160
|
+
registry.beginWriteTransaction();
|
|
161
161
|
try {
|
|
162
162
|
current = validate();
|
|
163
163
|
registry.run("INSERT OR IGNORE INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-operation',?,?,?,?,NULL,?,?,?)", [operationId, scopeId, record.lease_token, context.now(), JSON.stringify({ scope_id: scopeId, generation, process_id: record.id, operation: mode === "stop" ? "probe.stop" : "probe.observe", status: "dispatching" }), context.now(), context.now()]);
|
|
@@ -240,7 +240,7 @@ export async function controlRuntimeScopeNativeOwners(context, scopeId, generati
|
|
|
240
240
|
const operationId = `scope-cancel:${crypto.createHash("sha256").update(JSON.stringify([scopeId, generation, record.id, state.daemon_id, id])).digest("hex")}`;
|
|
241
241
|
const now = context.now();
|
|
242
242
|
let inserted = false;
|
|
243
|
-
registry.
|
|
243
|
+
registry.beginWriteTransaction();
|
|
244
244
|
try {
|
|
245
245
|
assertOwner();
|
|
246
246
|
inserted = registry.run("INSERT OR IGNORE INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-control-operation',?,?,?,?,NULL,?,?,?)", [operationId, scopeId, record.lease_token, now, JSON.stringify({ scope_id: scopeId, generation, process_id: record.id, daemon_id: state.daemon_id, session_id: id, status: "dispatching" }), now, now]).changes === 1;
|
|
@@ -331,7 +331,7 @@ export async function controlRuntimeScopeRuns(context, scopeId, requestId, mode,
|
|
|
331
331
|
let input = { projectRoot: project.root, runId: record.run_id, requestId: runRequestId, action: mode };
|
|
332
332
|
// Lock order is resource registry then RUN database. No provider or await
|
|
333
333
|
// occurs here; a newer scope request cannot interleave RUN intent creation.
|
|
334
|
-
registry.
|
|
334
|
+
registry.beginWriteTransaction();
|
|
335
335
|
try {
|
|
336
336
|
const current = registry.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
337
337
|
if (!current || JSON.parse(current.metadata_json).generation !== control.generation || registry.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
|
|
@@ -67,7 +67,7 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
|
|
|
67
67
|
if (!project || manifest?.budget?.scope_id !== scopeId || manifest.resource_home !== resourceHome(context))
|
|
68
68
|
throw new AppError("runtime_scope_owner_unknown", "Resume RUN no longer belongs to this retained scope", 1);
|
|
69
69
|
const input = { projectRoot: project.root, runId: source.run_id, requestId: source.resume_request_id, controlId: source.control_id };
|
|
70
|
-
db.
|
|
70
|
+
db.beginWriteTransaction();
|
|
71
71
|
try {
|
|
72
72
|
assertCurrent();
|
|
73
73
|
recordControllerResume(target, input);
|
|
@@ -111,10 +111,10 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
|
|
|
111
111
|
try {
|
|
112
112
|
for (const home of [...new Set(intent.runs.map((source) => source.runtime_home))].sort())
|
|
113
113
|
lockedRuns.set(home, createContext({ ...context.env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome(context), DD_FLOW_ENGINE_MODE: "1" }));
|
|
114
|
-
db.
|
|
114
|
+
db.beginWriteTransaction();
|
|
115
115
|
registryHeld = true;
|
|
116
116
|
for (const target of lockedRuns.values()) {
|
|
117
|
-
target.db.
|
|
117
|
+
target.db.beginWriteTransaction();
|
|
118
118
|
heldRuns.push(target);
|
|
119
119
|
}
|
|
120
120
|
assertCurrent();
|
|
@@ -316,7 +316,7 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
|
|
|
316
316
|
};
|
|
317
317
|
assertRetired();
|
|
318
318
|
let dispatch = false;
|
|
319
|
-
db.
|
|
319
|
+
db.beginWriteTransaction();
|
|
320
320
|
try {
|
|
321
321
|
assertRetired();
|
|
322
322
|
if (operation.status === "retired") {
|
|
@@ -370,7 +370,7 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
|
|
|
370
370
|
for (const session of source.sessions) {
|
|
371
371
|
const operationId = `scope-load:${hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id, session.session_id]))}`;
|
|
372
372
|
let load, dispatchLoad = false;
|
|
373
|
-
db.
|
|
373
|
+
db.beginWriteTransaction();
|
|
374
374
|
try {
|
|
375
375
|
assertReplacement();
|
|
376
376
|
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [operationId]);
|
|
@@ -439,7 +439,7 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
|
|
|
439
439
|
throw new AppError("runtime_scope_native_launch_required", "Native recovery requires explicit configuration presence before retiring its owner", 1);
|
|
440
440
|
let operation;
|
|
441
441
|
let dispatch = false;
|
|
442
|
-
db.
|
|
442
|
+
db.beginWriteTransaction();
|
|
443
443
|
try {
|
|
444
444
|
assertOwner();
|
|
445
445
|
const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
|
|
@@ -486,7 +486,7 @@ export function recordRuntimeScopeResume(context, input) {
|
|
|
486
486
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !Number.isSafeInteger(input.generation) || input.generation < 1 || !/^[a-f0-9]{64}$/.test(input.captureKey))
|
|
487
487
|
throw new AppError("runtime_scope_resume_invalid", "Scope resume requires its exact generation, capture and request identity", 2);
|
|
488
488
|
const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([input.scopeId, input.requestId]));
|
|
489
|
-
db.
|
|
489
|
+
db.beginWriteTransaction();
|
|
490
490
|
try {
|
|
491
491
|
const latest = runtimeBudgetStatus(context, input.scopeId);
|
|
492
492
|
const control = latest.control;
|
|
@@ -40,7 +40,7 @@ export async function reconcileRuntimeScopeControl(context, scopeId, generation,
|
|
|
40
40
|
export async function launchRuntimeScopeControl(context, scopeId, requestId, mode, manifest, reconcileId, resumeId) {
|
|
41
41
|
const control = requestRuntimeScopeControl(context, scopeId, requestId, mode, manifest), db = getResourceDatabase(resourceHome(context)), key = keyFor(scopeId, control.generation);
|
|
42
42
|
let worker;
|
|
43
|
-
db.
|
|
43
|
+
db.beginWriteTransaction();
|
|
44
44
|
try {
|
|
45
45
|
const current = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
46
46
|
const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ?", [key]);
|
|
@@ -108,7 +108,7 @@ export async function serveRuntimeScopeControl(context, scopeId, generation) {
|
|
|
108
108
|
throw new AppError("runtime_scope_owner_unknown", "Scope worker requires its launch token and generation", 1);
|
|
109
109
|
const db = getResourceDatabase(resourceHome(context)), key = keyFor(scopeId, generation), token = context.env.DD_FLOW_SCOPE_CONTROL_TOKEN;
|
|
110
110
|
let worker;
|
|
111
|
-
db.
|
|
111
|
+
db.beginWriteTransaction();
|
|
112
112
|
try {
|
|
113
113
|
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
114
114
|
if (!row)
|
|
@@ -27,7 +27,7 @@ export function pauseStageForUser(context, input) {
|
|
|
27
27
|
fs.mkdirSync(pauseRoot, { recursive: true });
|
|
28
28
|
fs.writeFileSync(questionPath, question);
|
|
29
29
|
const now = context.now();
|
|
30
|
-
context.db.
|
|
30
|
+
context.db.beginWriteTransaction();
|
|
31
31
|
try {
|
|
32
32
|
const paused = context.db.run("UPDATE works SET status = 'paused', updated_at = ? WHERE work_id = ? AND status = 'running'", [now, work.work_id]);
|
|
33
33
|
if (paused.changes !== 1)
|
|
@@ -78,7 +78,7 @@ export function resumeStageAfterUser(context, input) {
|
|
|
78
78
|
if (!workSession)
|
|
79
79
|
throw new AppError("runtime_missing", "Paused Work has no open Session link", 1, { work_id: work.work_id });
|
|
80
80
|
const answerPath = path.join(path.dirname(stage.pause.question_path), `answer-${crypto.createHash("sha256").update(`${stage.pause.id}\0${input.answer}`).digest("hex")}.md`);
|
|
81
|
-
context.db.
|
|
81
|
+
context.db.beginWriteTransaction();
|
|
82
82
|
let identity;
|
|
83
83
|
try {
|
|
84
84
|
identity = claimStageResumeHookEvent(context, { projectId: project.id, runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, eventKey: hookEventId });
|
|
@@ -14,12 +14,12 @@ import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
|
14
14
|
import { addVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
15
15
|
import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
16
16
|
import { addWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
17
|
-
import { withStageSettlement } from "./runs.js";
|
|
17
|
+
import { withStageSettlement, stageInputGuard } from "./runs.js";
|
|
18
18
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
19
19
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
20
20
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
21
21
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
22
|
-
import {
|
|
22
|
+
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
23
23
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
24
24
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
25
25
|
const stage = "code-review";
|
|
@@ -175,6 +175,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
175
175
|
validateDecision(context, { decision: canonical.decision, reviewers: canonical.reviewers, repairs, workspaceRoot: run.workspace_root });
|
|
176
176
|
}
|
|
177
177
|
const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
|
|
178
|
+
const assertInputs = stageInputGuard(context, project.id, run.id, () => [workspaceFingerprint(run.workspace_root), sha256File(decisionFile), sha256File(path.join(home, "05-code", "stage-report.json"))]);
|
|
178
179
|
const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
179
180
|
const finalChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }));
|
|
180
181
|
if (input.retryCheckId) {
|
|
@@ -200,10 +201,14 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
200
201
|
// The request must exist before CODE-REVIEW becomes terminal. Otherwise a
|
|
201
202
|
// merge-gate error creates an irrecoverable `done` report with no MRG-*.
|
|
202
203
|
const acceptedPaths = changedPaths(run.workspace_root);
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
let mergeRequest = null;
|
|
205
|
+
assertInputs();
|
|
206
|
+
const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
|
|
205
207
|
withStageSettlement(context, () => {
|
|
206
208
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
209
|
+
assertInputs();
|
|
210
|
+
mergeRequest = prepareMerge?.() ?? null;
|
|
211
|
+
writeReport(root, report);
|
|
207
212
|
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
208
213
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
|
|
209
214
|
if (nextAction === "start_merge")
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { stageInputGuard } from "./runs.js";
|
|
4
|
+
import { workspaceFingerprint } from "./code-checks.js";
|
|
3
5
|
import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
|
|
4
6
|
import fs from "node:fs";
|
|
5
7
|
import path from "node:path";
|
|
@@ -16,7 +18,7 @@ import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
|
16
18
|
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
|
|
17
19
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
18
20
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
19
|
-
import {
|
|
21
|
+
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
20
22
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
21
23
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
22
24
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
@@ -143,6 +145,7 @@ export async function finishVnextCode(context, input) {
|
|
|
143
145
|
throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
|
|
144
146
|
}
|
|
145
147
|
// Reject a malformed semantic receipt before running the expensive gate.
|
|
148
|
+
const assertInputs = stageInputGuard(context, project.id, run.id, () => [workspaceFingerprint(run.workspace_root), crypto.createHash("sha256").update(fs.readFileSync(input.verificationFile)).digest("hex")]);
|
|
146
149
|
const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
147
150
|
if (verification.verdict === "blocked") {
|
|
148
151
|
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, instruction: "CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again." };
|
|
@@ -233,10 +236,14 @@ export async function finishVnextCode(context, input) {
|
|
|
233
236
|
// Creating the MERGE request is part of accepting this terminal handoff.
|
|
234
237
|
// Do it before materialising a terminal stage report or changing RUN state,
|
|
235
238
|
// so an invalid merge policy leaves CODE safely running and retryable.
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
let mergeRequest = null;
|
|
240
|
+
assertInputs();
|
|
241
|
+
const prepareMerge = next === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
|
|
238
242
|
withStageSettlement(context, () => {
|
|
239
243
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
244
|
+
assertInputs();
|
|
245
|
+
mergeRequest = prepareMerge?.() ?? null;
|
|
246
|
+
writeReport(root, report);
|
|
240
247
|
completeFlowRunStage(context, {
|
|
241
248
|
projectRoot,
|
|
242
249
|
runId: run.id,
|
|
@@ -28,13 +28,17 @@ export function isVnextMergeRun(context, input) {
|
|
|
28
28
|
}
|
|
29
29
|
/** Terminal CODE/CODE-REVIEW calls this once; it freezes source and creates the child MERGE Work. */
|
|
30
30
|
export function ensureVnextMergeRequest(context, input) {
|
|
31
|
+
return prepareVnextMergeRequest(context, input)();
|
|
32
|
+
}
|
|
33
|
+
/** Freeze filesystem evidence outside the stage writer lock; return only the SQL settlement. */
|
|
34
|
+
export function prepareVnextMergeRequest(context, input) {
|
|
31
35
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
32
36
|
const project = requireProjectByRoot(context, projectRoot);
|
|
33
37
|
const run = requireRun(context, project.id, input.runId);
|
|
34
38
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
35
39
|
const existing = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') ORDER BY created_at DESC LIMIT 1", [project.id, run.id]);
|
|
36
40
|
if (existing)
|
|
37
|
-
return requestView(context, existing);
|
|
41
|
+
return () => requestView(context, requireRequest(context, existing.merge_request_id));
|
|
38
42
|
const settings = executionSettings(run);
|
|
39
43
|
const route = settings.merge_mode ?? "same_session";
|
|
40
44
|
const home = requireHome(run);
|
|
@@ -88,28 +92,19 @@ export function ensureVnextMergeRequest(context, input) {
|
|
|
88
92
|
if (!sourceCommit)
|
|
89
93
|
throw new AppError("merge_source_invalid", "MERGE could not freeze source commit", 1, { source_commit: sourceCommit, target_branch: targetBranch });
|
|
90
94
|
const enqueueTarget = gitValue(targetWorkspace, ["rev-parse", targetBranch], true);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
context.db.exec("BEGIN IMMEDIATE");
|
|
96
|
-
try {
|
|
95
|
+
return () => context.db.writeTransaction(() => {
|
|
96
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
97
|
+
const root = requireRootWork(context, project.id, run.id);
|
|
98
|
+
const now = context.now();
|
|
97
99
|
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]);
|
|
98
|
-
if (raced)
|
|
99
|
-
context.db.exec("COMMIT");
|
|
100
|
+
if (raced)
|
|
100
101
|
return requestView(context, raced);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
requestId = nextMergeRequestId(context);
|
|
102
|
+
const child = createChildWork(context, { parentWorkId: root.work_id, slug: "merge", task: `Integrate frozen source ${sourceCommit} into ${targetBranch}, resolve material conflicts, and preserve accepted behavior.`, launchPolicy: route === "server" ? "fresh_agent_required" : "reuse_allowed", resultSchema: "dd-flow/merge-result@1", payload: { kind: "merge", checks: semantic, protocol_ids: protocols } });
|
|
103
|
+
const requestId = nextMergeRequestId(context);
|
|
104
104
|
context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, replacement_of_merge_request_id, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route, readCodeCheckProfile(targetWorkspace).hash, replacementOf(run), now, now]);
|
|
105
|
-
context.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
context.db.exec("ROLLBACK");
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
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 });
|
|
112
|
-
return requestView(context, requireRequest(context, requestId));
|
|
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));
|
|
107
|
+
});
|
|
113
108
|
}
|
|
114
109
|
export async function startVnextMerge(context, input) {
|
|
115
110
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -346,7 +341,7 @@ function recoverCompletedMergeRun(context, run, projectRoot, request) {
|
|
|
346
341
|
}
|
|
347
342
|
export function routeVnextMergeRequest(context, input) {
|
|
348
343
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
349
|
-
context.db.
|
|
344
|
+
context.db.beginWriteTransaction();
|
|
350
345
|
let request;
|
|
351
346
|
try {
|
|
352
347
|
request = requireRequest(context, input.requestId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
-
import { withStageSettlement } from "./runs.js";
|
|
3
|
+
import { withStageSettlement, stageInputGuard } from "./runs.js";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
@@ -184,6 +184,7 @@ export function finishVnextPlanReview(context, input) {
|
|
|
184
184
|
if (!parent || parent.status !== "running")
|
|
185
185
|
throw new AppError("invalid_work_state", "PLAN-REVIEW has no running parent Work", 2);
|
|
186
186
|
const decisionPath = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
|
|
187
|
+
const assertInputs = stageInputGuard(context, project.id, run.id, () => [planSetChecksum(home, run.workspace_root), checksum(batch), checksum(decisionPath), checksum(path.join(root, "work-context.json"))]);
|
|
187
188
|
const decision = readDecision(context, projectRoot, run.id, home, decisionPath);
|
|
188
189
|
const children = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [project.id, run.id, parent.work_id]);
|
|
189
190
|
const contextFile = readJson(path.join(root, "work-context.json"));
|
|
@@ -198,7 +199,7 @@ export function finishVnextPlanReview(context, input) {
|
|
|
198
199
|
}
|
|
199
200
|
if (["failed", "cancelled"].includes(decision.outcome)) {
|
|
200
201
|
preserveDecisionReceipt(root, decisionPath);
|
|
201
|
-
return withStageSettlement(context, () => finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision }));
|
|
202
|
+
return withStageSettlement(context, () => { assertInputs(); return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision }); });
|
|
202
203
|
}
|
|
203
204
|
if (children.some((child) => child.status === "created" || child.status === "running"))
|
|
204
205
|
throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
|
|
@@ -246,6 +247,7 @@ export function finishVnextPlanReview(context, input) {
|
|
|
246
247
|
const finalPlanChecksum = planSetChecksum(home, run.workspace_root);
|
|
247
248
|
const finalBatchChecksum = checksum(batch);
|
|
248
249
|
return withStageSettlement(context, () => {
|
|
250
|
+
assertInputs();
|
|
249
251
|
const registered = registerCode(context, project.id, run.id, batch);
|
|
250
252
|
const now = context.now();
|
|
251
253
|
settleReviewParent(context, { projectRoot, projectId: project.id, runId: run.id, parentWorkId: parent.work_id, status: "completed", result: decision.summary, reason: "plan_review_completed", resultPath: path.join(root, "stage-report.json") });
|
|
@@ -52,7 +52,7 @@ export function startVnextPlan(context, input) {
|
|
|
52
52
|
if (!context.db.get("SELECT 1 FROM work_sessions WHERE work_id = ? LIMIT 1", [rootWork.work_id])) {
|
|
53
53
|
bindRunningWorkSession(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, promptPath: path.join(root, "fixture-root.prompt.md") });
|
|
54
54
|
}
|
|
55
|
-
context.db.
|
|
55
|
+
context.db.beginWriteTransaction();
|
|
56
56
|
let planWorkId;
|
|
57
57
|
try {
|
|
58
58
|
planWorkId = nextWorkId(context, project.id, "plan");
|