@deksden-com/dd-flow-cli 0.9.0-beta.48 → 0.9.0-beta.51
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 +22 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +4 -1
- package/dist/schemas/code-work-batch.schema.json +1 -1
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/code-checks.js +237 -32
- package/dist/services/runs.js +16 -0
- package/dist/services/usage.js +4 -3
- package/dist/services/vnext-code-review.js +45 -21
- package/dist/services/vnext-code.js +63 -123
- package/dist/services/vnext-merge.js +73 -24
- package/dist/services/vnext-plan-review.js +24 -19
- package/dist/services/vnext-plan.js +4 -0
- package/dist/services/work-registry.js +32 -8
- package/dist/services/workspace-bootstrap.js +29 -12
- package/dist/storage/database.js +6 -0
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ import { validateSchema } from "./schema-validation.js";
|
|
|
15
15
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
16
16
|
import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
17
17
|
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
18
|
-
import { checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks,
|
|
18
|
+
import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
19
19
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
20
20
|
import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
|
|
21
21
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
@@ -50,6 +50,7 @@ export function ensureVnextMergeRequest(context, input) {
|
|
|
50
50
|
throw new AppError("merge_source_invalid", "MERGE could not determine the target branch", 1, { target_branch: targetBranch });
|
|
51
51
|
const semantic = planChecks(run.workspace_root, protocols);
|
|
52
52
|
validateMergeAcceptance(run.workspace_root, protocols, semantic);
|
|
53
|
+
const acceptanceRefs = mergeAcceptanceRefs(run.workspace_root, protocols);
|
|
53
54
|
const effective = effectiveCheckDeclarations(targetWorkspace, semantic, ["work", "code", "readiness", "merge"]);
|
|
54
55
|
if (run.workspace_root !== targetWorkspace && effective.length === 0)
|
|
55
56
|
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 });
|
|
@@ -59,7 +60,7 @@ export function ensureVnextMergeRequest(context, input) {
|
|
|
59
60
|
const freezeFile = path.join(freezeRoot, "source-freeze.json");
|
|
60
61
|
const gateFile = path.join(freezeRoot, "merge-gate.json");
|
|
61
62
|
fs.mkdirSync(freezeRoot, { recursive: true });
|
|
62
|
-
freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, profileHash: readCodeCheckProfile(targetWorkspace).hash, now: context.now() });
|
|
63
|
+
freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, acceptanceRefs, profileHash: readCodeCheckProfile(targetWorkspace).hash, now: context.now() });
|
|
63
64
|
let sourceCommit;
|
|
64
65
|
let sourcePaths;
|
|
65
66
|
if (fs.existsSync(freezeFile)) {
|
|
@@ -179,10 +180,14 @@ function startPacket(context, run, request, binding, promptPath, resultPath, pro
|
|
|
179
180
|
export function applyVnextMerge(context, input) {
|
|
180
181
|
const existing = requireRequest(context, input.requestId);
|
|
181
182
|
const receiptFile = applyReceiptPath(context, existing);
|
|
182
|
-
if (existing.executor_work_id === input.workId && ["apply_recorded", "integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(existing.checkpoint) && fs.existsSync(receiptFile))
|
|
183
|
-
return JSON.parse(fs.readFileSync(receiptFile, "utf8"));
|
|
184
183
|
const request = requireOwnedActiveRequest(context, input);
|
|
185
184
|
assertRunMutationAllowed(context, request.project_id, request.run_id);
|
|
185
|
+
if (["apply_recorded", "integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(request.checkpoint) && fs.existsSync(receiptFile)) {
|
|
186
|
+
const receipt = JSON.parse(fs.readFileSync(receiptFile, "utf8"));
|
|
187
|
+
if (receipt.merge_request_id !== request.merge_request_id || receipt.work_id !== request.executor_work_id || receipt.source_commit !== request.source_commit)
|
|
188
|
+
throw new AppError("merge_apply_receipt_invalid", "Existing merge apply receipt does not match the active request", 1, { receipt_path: receiptFile });
|
|
189
|
+
return receipt;
|
|
190
|
+
}
|
|
186
191
|
heartbeatLaneLock(context, { projectRoot: input.projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, ttlSeconds: 7200 });
|
|
187
192
|
assertExecutionBaseline(request);
|
|
188
193
|
input.progress?.(`applying ${request.source_commit} to ${request.target_branch}`);
|
|
@@ -214,6 +219,7 @@ export async function finishVnextMerge(context, input) {
|
|
|
214
219
|
throw new AppError("merge_request_mismatch", "MRG/Work/RUN/project identity does not match", 2);
|
|
215
220
|
if (existing.status === "completed") {
|
|
216
221
|
recoverCompletedMergeRun(context, run, projectRoot, existing);
|
|
222
|
+
releaseMergeLane(context, projectRoot, existing, `MERGE ${existing.merge_request_id} completion recovered`);
|
|
217
223
|
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" };
|
|
218
224
|
}
|
|
219
225
|
let request = requireOwnedActiveRequest(context, input);
|
|
@@ -236,29 +242,35 @@ export async function finishVnextMerge(context, input) {
|
|
|
236
242
|
context.db.run("UPDATE merge_requests SET checkpoint = 'bootstrap_ready', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
|
|
237
243
|
request = requireRequest(context, request.merge_request_id);
|
|
238
244
|
}
|
|
239
|
-
const
|
|
245
|
+
const gate = readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id), checks = gate.checks;
|
|
240
246
|
if (input.retryCheckId) {
|
|
241
|
-
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks,
|
|
247
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
|
|
242
248
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
243
249
|
}
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
250
|
+
const acceptancePath = path.join(requireHome(run), stageDir, "merge-gate-acceptance.json");
|
|
251
|
+
const recovered = readMergeGateAcceptance(context, acceptancePath, request, gate);
|
|
252
|
+
const resumingGate = ["checks_passed", "integration_committed", "delivered"].includes(request.checkpoint);
|
|
253
|
+
if (resumingGate && !recovered.valid)
|
|
254
|
+
return recovery(context, request, "merge_evidence_missing", { checkpoint: request.checkpoint });
|
|
255
|
+
if (request.checkpoint === "checks_passed" && request.accepted_tree && gitValue(request.target_workspace, ["write-tree"]) !== request.accepted_tree)
|
|
256
|
+
return recovery(context, request, "merge_workspace_drift_after_checks", { accepted_tree: request.accepted_tree });
|
|
257
|
+
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 } : {}) });
|
|
248
258
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
249
259
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
250
260
|
if (failed.length) {
|
|
251
261
|
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]);
|
|
252
262
|
throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: `${flowCommand(context)} merge repair ${request.merge_request_id} --project-root ${JSON.stringify(projectRoot)} --json`, retry_command: `${finishCommand(context, run.id, request, projectRoot)} --retry-check ${failed[0].id} --reason "<environment recovery evidence>"` });
|
|
253
263
|
}
|
|
254
|
-
const requiredRefs = mergeAcceptanceRefs(run.workspace_root, JSON.parse(request.protocol_ids_json));
|
|
255
264
|
const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
|
|
256
|
-
const missingRefs =
|
|
265
|
+
const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
|
|
257
266
|
if (missingRefs.length)
|
|
258
|
-
throw new AppError("merge_acceptance_unproven", "Current MERGE receipts do not cover every merge
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
267
|
+
throw new AppError("merge_acceptance_unproven", "Current MERGE receipts do not cover every frozen merge-gate reference", 2, { missing_check_refs: missingRefs });
|
|
268
|
+
if (!request.integration_commit) {
|
|
269
|
+
const acceptedTree = gitValue(request.target_workspace, ["write-tree"]);
|
|
270
|
+
writeMergeGateAcceptance(context, acceptancePath, request, gate, acceptedTree, receipts);
|
|
271
|
+
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]);
|
|
272
|
+
request = requireRequest(context, request.merge_request_id);
|
|
273
|
+
}
|
|
262
274
|
if (!request.integration_commit) {
|
|
263
275
|
commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
|
|
264
276
|
const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
|
|
@@ -269,11 +281,20 @@ export async function finishVnextMerge(context, input) {
|
|
|
269
281
|
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]);
|
|
270
282
|
request = requireRequest(context, request.merge_request_id);
|
|
271
283
|
}
|
|
284
|
+
if (request.integration_commit && request.accepted_tree && gitValue(request.target_workspace, ["rev-parse", `${request.integration_commit}^{tree}`]) !== request.accepted_tree)
|
|
285
|
+
return recovery(context, request, "merge_committed_tree_drift", { accepted_tree: request.accepted_tree, integration_commit: request.integration_commit });
|
|
272
286
|
verifyLocalDelivery(request);
|
|
273
287
|
context.db.run("UPDATE merge_requests SET checkpoint = 'delivered', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
|
|
274
288
|
request = requireRequest(context, request.merge_request_id);
|
|
275
289
|
performConfiguredCleanup(run, request);
|
|
276
|
-
|
|
290
|
+
const acceptedResult = fs.readFileSync(resultPath, "utf8");
|
|
291
|
+
const executor = context.db.get("SELECT status, result FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
292
|
+
if (!executor)
|
|
293
|
+
return recovery(context, request, "merge_executor_missing", { work_id: request.executor_work_id });
|
|
294
|
+
if (executor.status === "running")
|
|
295
|
+
await finishWork(context, request.executor_work_id, acceptedResult);
|
|
296
|
+
else if (executor.status !== "completed" || executor.result !== acceptedResult)
|
|
297
|
+
return recovery(context, request, "merge_executor_settlement_mismatch", { work_id: request.executor_work_id, status: executor.status });
|
|
277
298
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
278
299
|
const now = context.now();
|
|
279
300
|
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]);
|
|
@@ -299,14 +320,24 @@ function ensureMergeStageAttached(context, run, projectRoot) {
|
|
|
299
320
|
function recoverCompletedMergeRun(context, run, projectRoot, request) {
|
|
300
321
|
const root = path.join(requireHome(run), stageDir);
|
|
301
322
|
const reportPath = path.join(root, "stage-report.json");
|
|
302
|
-
if (!fs.existsSync(reportPath))
|
|
303
|
-
|
|
323
|
+
if (!fs.existsSync(reportPath)) {
|
|
324
|
+
const semantic = request.result_json ? JSON.parse(request.result_json) : null;
|
|
325
|
+
const gate = readFrozenMergeGate(path.join(root, "merge-gate.json"), request.merge_request_id);
|
|
326
|
+
const accepted = readMergeGateAcceptance(context, path.join(root, "merge-gate-acceptance.json"), request, gate);
|
|
327
|
+
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)
|
|
328
|
+
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 });
|
|
329
|
+
verifyLocalDelivery(request);
|
|
330
|
+
performConfiguredCleanup(run, request);
|
|
331
|
+
writeStageReport(root, mergeReport(context, run, request, semantic, accepted.receipts));
|
|
332
|
+
}
|
|
304
333
|
ensureMergeStageAttached(context, run, projectRoot);
|
|
305
334
|
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@2", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
306
335
|
const rootWork = findRootWork(context, run.project_id, run.id);
|
|
307
336
|
const rootStatus = rootWork.status;
|
|
308
337
|
if (rootStatus === "running")
|
|
309
338
|
finishFanInWork(context, rootWork.work_id, fs.readFileSync(reportPath, "utf8"));
|
|
339
|
+
else if (rootStatus !== "completed")
|
|
340
|
+
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 });
|
|
310
341
|
const runStatus = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id])?.status;
|
|
311
342
|
if (runStatus !== "done")
|
|
312
343
|
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "merge_completed", nextAction: undefined });
|
|
@@ -374,21 +405,39 @@ function planChecks(workspace, protocols) { return protocols.flatMap((protocol)
|
|
|
374
405
|
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}` })); }); }
|
|
375
406
|
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)
|
|
376
407
|
throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a missing check", 2, { check_refs: missing }); }
|
|
377
|
-
function freezeMergeGate(file, input) { const canonical = JSON.stringify(input.checks); const hash = cryptoHash(canonical); if (fs.existsSync(file)) {
|
|
408
|
+
function freezeMergeGate(file, input) { const canonical = JSON.stringify({ checks: input.checks, acceptance_refs: [...input.acceptanceRefs].sort() }); const hash = cryptoHash(canonical); if (fs.existsSync(file)) {
|
|
378
409
|
const existing = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
379
|
-
if (existing.run_id !== input.runId || existing.
|
|
410
|
+
if (existing.run_id !== input.runId || existing.gate_hash !== hash)
|
|
380
411
|
throw new AppError("merge_gate_freeze_conflict", "MERGE gate is already frozen with different checks", 2, { file });
|
|
381
412
|
return;
|
|
382
|
-
} fs.writeFileSync(file, `${JSON.stringify({ schema_id: "dd-flow/merge-gate@
|
|
413
|
+
} 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`); }
|
|
383
414
|
function readFrozenMergeGate(file, requestId) { try {
|
|
384
415
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
385
|
-
if (value.schema_id !== "dd-flow/merge-gate@
|
|
416
|
+
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() })))
|
|
386
417
|
throw new Error("invalid gate");
|
|
387
|
-
return { checks: value.checks };
|
|
418
|
+
return { checks: value.checks, acceptanceRefs: [...value.acceptance_refs].sort(), profileHash: value.profile_hash ?? null, gateHash: value.gate_hash };
|
|
388
419
|
}
|
|
389
420
|
catch (cause) {
|
|
390
421
|
throw new AppError("merge_gate_missing", "MERGE gate is missing or invalid", 1, { merge_request_id: requestId, file, cause: String(cause) });
|
|
391
422
|
} }
|
|
423
|
+
function frozenGateRefs(gate) { return [...new Set([...gate.checks.map(check => check.canonical_ref ?? check.id), ...gate.acceptanceRefs])].sort(); }
|
|
424
|
+
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); }
|
|
425
|
+
function readMergeGateAcceptance(context, file, request, gate) { try {
|
|
426
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
427
|
+
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")))
|
|
428
|
+
return { valid: false, receipts: [] };
|
|
429
|
+
const all = checkReceipts(context, { projectId: request.project_id, runId: request.run_id });
|
|
430
|
+
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));
|
|
431
|
+
const bindingRefs = value.receipts.flatMap(receipt => receipt.binding_refs);
|
|
432
|
+
const bindings = new Set(bindingRefs);
|
|
433
|
+
const expected = frozenGateRefs(gate);
|
|
434
|
+
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)
|
|
435
|
+
return { valid: false, receipts: [] };
|
|
436
|
+
return { valid: true, receipts: receipts.map((receipt, index) => ({ ...receipt, check_refs: value.receipts[index].binding_refs })) };
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return { valid: false, receipts: [] };
|
|
440
|
+
} }
|
|
392
441
|
function cryptoHash(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|
|
393
442
|
function mergeAcceptanceRefs(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
394
443
|
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}`)); }); }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { withStageSettlement } from "./runs.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { AppError } from "../shared/errors.js";
|
|
@@ -70,13 +71,15 @@ export function startVnextPlanReview(context, input) {
|
|
|
70
71
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
71
72
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
72
73
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
return withStageSettlement(context, () => {
|
|
75
|
+
const registered = registerCode(context, project.id, run.id, batch);
|
|
76
|
+
const report = reportFor({ run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
|
|
77
|
+
writeReport(root, report);
|
|
78
|
+
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" });
|
|
79
|
+
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
|
|
80
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
|
|
81
|
+
return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
|
|
82
|
+
});
|
|
80
83
|
}
|
|
81
84
|
const rootWork = rootWorkId(context, project.id, run.id);
|
|
82
85
|
const workId = nextWorkId(context, project.id, "plan-review");
|
|
@@ -194,7 +197,7 @@ export function finishVnextPlanReview(context, input) {
|
|
|
194
197
|
}
|
|
195
198
|
if (["failed", "cancelled"].includes(decision.outcome)) {
|
|
196
199
|
preserveDecisionReceipt(root, decisionPath);
|
|
197
|
-
return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision });
|
|
200
|
+
return withStageSettlement(context, () => finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision }));
|
|
198
201
|
}
|
|
199
202
|
if (children.some((child) => child.status === "created" || child.status === "running"))
|
|
200
203
|
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 })) });
|
|
@@ -241,17 +244,19 @@ export function finishVnextPlanReview(context, input) {
|
|
|
241
244
|
preserveDecisionReceipt(root, decisionPath);
|
|
242
245
|
const finalPlanChecksum = planSetChecksum(home, run.workspace_root);
|
|
243
246
|
const finalBatchChecksum = checksum(batch);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
247
|
+
return withStageSettlement(context, () => {
|
|
248
|
+
const registered = registerCode(context, project.id, run.id, batch);
|
|
249
|
+
const now = context.now();
|
|
250
|
+
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") });
|
|
251
|
+
const report = reportFor({ run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context) });
|
|
252
|
+
applyReviewResults(home, latestChildren, decision);
|
|
253
|
+
writeReport(root, report);
|
|
254
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
255
|
+
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" });
|
|
256
|
+
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
|
|
257
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
|
|
258
|
+
return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
|
|
259
|
+
});
|
|
255
260
|
}
|
|
256
261
|
function preparedExisting(context, input) {
|
|
257
262
|
const promptPath = path.join(input.root, "stage-prompt.md");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { withStageSettlement } from "./runs.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { AppError } from "../shared/errors.js";
|
|
@@ -106,6 +107,9 @@ export function startVnextPlan(context, input) {
|
|
|
106
107
|
return { ok: true, run_id: run.id, work_id: planWorkId, id: workSessionId, artifact_materialization: artifactMaterialization, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), prompt_path: promptPath, next_command: finishCommand, ...(externalContext ? { external_context: externalContext } : {}) };
|
|
107
108
|
}
|
|
108
109
|
export function finishVnextPlan(context, input) {
|
|
110
|
+
return withStageSettlement(context, () => finishVnextPlanOwned(context, input));
|
|
111
|
+
}
|
|
112
|
+
function finishVnextPlanOwned(context, input) {
|
|
109
113
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
110
114
|
const project = requireProjectByRoot(context, projectRoot);
|
|
111
115
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -6,7 +6,7 @@ import { findRecentMatchingHookEvent, claimStageLifecycleHookEvent, claimStageSt
|
|
|
6
6
|
import { validateSchema } from "./schema-validation.js";
|
|
7
7
|
import { resolveProjectRoot, resolveRunReferences } from "../storage/paths.js";
|
|
8
8
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
9
|
-
import { readCodeCheckProfile, runCodeChecks, workspaceFingerprint } from "./code-checks.js";
|
|
9
|
+
import { readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
10
10
|
import { nextWorkId, nextWorkIds } from "./ids.js";
|
|
11
11
|
import { appendFlowRunTimelineEvent } from "./runs.js";
|
|
12
12
|
import { resolveStageTransition } from "./execution-policy.js";
|
|
@@ -39,7 +39,7 @@ export function assertStageLifecycleOwner(context, input) {
|
|
|
39
39
|
if (!project)
|
|
40
40
|
throw new AppError("not_found", "Project is not registered", 1, { project_root: projectRoot });
|
|
41
41
|
requireRun(context, project.id, input.runId);
|
|
42
|
-
|
|
42
|
+
let owner = context.db.get(`SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id
|
|
43
43
|
FROM work_sessions ws
|
|
44
44
|
JOIN works w ON w.work_id = ws.work_id
|
|
45
45
|
JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id
|
|
@@ -47,6 +47,13 @@ export function assertStageLifecycleOwner(context, input) {
|
|
|
47
47
|
AND w.status IN ('running', 'paused') AND (w.parent_work_id IS NULL OR s.current_stage = ?)
|
|
48
48
|
${input.workId ? "AND w.work_id = ?" : ""}
|
|
49
49
|
ORDER BY ws.created_at DESC LIMIT 1`, input.workId ? [project.id, input.runId, input.stage, input.workId] : [project.id, input.runId, input.stage]);
|
|
50
|
+
if (!owner && input.operation === "stage_finish" && ["code", "code-review"].includes(input.stage)) {
|
|
51
|
+
const row = context.db.get("SELECT run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
52
|
+
const index = row ? JSON.parse(row.index_json) : null;
|
|
53
|
+
const completed = index?.stage_runs.find(entry => entry.stage === input.stage && entry.status === "done");
|
|
54
|
+
if (row && completed && (index?.current_stage ?? index?.stage_runs.at(-1)?.stage) === input.stage)
|
|
55
|
+
owner = context.db.get("SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND w.parent_work_id IS NULL AND w.status = 'completed' AND ws.status = 'completed' AND ws.result_path = ? ORDER BY ws.created_at DESC LIMIT 1", [project.id, input.runId, path.join(row.run_root, completed.dir, "stage-report.json")]);
|
|
56
|
+
}
|
|
50
57
|
if (!owner)
|
|
51
58
|
throw new AppError("trusted_session_binding_required", "Stage lifecycle command has no running coordinator Work/Session binding", 1, { run_id: input.runId, stage: input.stage, ...(input.workId ? { work_id: input.workId } : {}) });
|
|
52
59
|
const identity = claimStageLifecycleHookEvent(context, {
|
|
@@ -135,7 +142,7 @@ export function addWorkBatch(context, input) {
|
|
|
135
142
|
if (item.parent && !keys.has(item.parent) && !existingIds.has(item.parent))
|
|
136
143
|
throw new AppError("validation", "work parent is unknown", 2, { key: item.key, parent: item.parent });
|
|
137
144
|
const now = context.now();
|
|
138
|
-
context.db.exec("
|
|
145
|
+
context.db.exec("SAVEPOINT work_batch");
|
|
139
146
|
try {
|
|
140
147
|
const allocated = nextWorkIds(context, parent.project_id, items.map((item) => item.key));
|
|
141
148
|
const ids = new Map(items.map((item, index) => [item.key, allocated[index]]));
|
|
@@ -146,14 +153,15 @@ export function addWorkBatch(context, input) {
|
|
|
146
153
|
for (const item of proposed)
|
|
147
154
|
context.db.run(`INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, started_at, created_at, updated_at, completed_at)
|
|
148
155
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'created', NULL, NULL, ?, ?, NULL)`, [item.id, parent.project_id, parent.run_id, item.parentId, item.task, item.launch_policy ?? "reuse_allowed", item.result_schema ?? null, item.payload ? JSON.stringify(item.payload) : null, JSON.stringify(item.dependencies), now, now]);
|
|
149
|
-
context.db.exec("COMMIT");
|
|
150
156
|
for (const item of proposed)
|
|
151
157
|
appendFlowRunTimelineEvent(context, parent.project_id, parent.run_id, { type: "work_materialized", work_id: item.id, parent_work_id: item.parentId, depends_on: item.dependencies, launch_policy: item.launch_policy ?? "reuse_allowed" });
|
|
152
158
|
refreshRunWorkProjection(context, parent.project_id, parent.run_id);
|
|
159
|
+
context.db.exec("RELEASE work_batch");
|
|
153
160
|
return { ok: true, work_ids: Object.fromEntries(ids) };
|
|
154
161
|
}
|
|
155
162
|
catch (error) {
|
|
156
|
-
context.db.exec("ROLLBACK");
|
|
163
|
+
context.db.exec("ROLLBACK TO work_batch");
|
|
164
|
+
context.db.exec("RELEASE work_batch");
|
|
157
165
|
throw error;
|
|
158
166
|
}
|
|
159
167
|
}
|
|
@@ -496,7 +504,21 @@ export function startBoundWork(context, work, run, identity, hookEventId) {
|
|
|
496
504
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
497
505
|
return { ok: true, work_id: work.work_id, work_session_id: linkId, worker_prompt_markdown: prompt, prompt_path: promptPath, ...(recovery ? { recovery } : {}), session_binding: { source: "PreToolUse", session: { harness_id: identity.harness, session_id: identity.nativeSessionId } } };
|
|
498
506
|
}
|
|
499
|
-
|
|
507
|
+
/** Root coordinators have no worker checks: settle them inside the stage transaction. */
|
|
508
|
+
export function finishStageCoordinatorWork(context, id, result) {
|
|
509
|
+
const work = requireWork(context, id);
|
|
510
|
+
if (work.parent_work_id !== null || codePacket(work) || parsePayload(work)?.read_only)
|
|
511
|
+
throw new AppError("invalid_work_state", "Stage settlement requires a root coordinator", 2);
|
|
512
|
+
const run = requireRun(context, work.project_id, work.run_id);
|
|
513
|
+
const link = context.db.get("SELECT * FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [id]);
|
|
514
|
+
if (!link)
|
|
515
|
+
throw new AppError("runtime_missing", "Coordinator has no open Work/Session link", 1);
|
|
516
|
+
validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link.result_path);
|
|
517
|
+
finishFanInWork(context, id, result);
|
|
518
|
+
context.db.run("UPDATE sessions SET status = 'idle', updated_at = ? WHERE project_id = ? AND session_id = ? AND NOT EXISTS (SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running')", [context.now(), work.project_id, link.session_id, link.session_id]);
|
|
519
|
+
settleRecoveryWork(context, id, "completed");
|
|
520
|
+
}
|
|
521
|
+
export function finishWork(context, id, result, progress, retry) { return settle(context, id, "completed", result, progress, retry); }
|
|
500
522
|
/** Structured fan-in closes a parent after its Session was handed to a child. */
|
|
501
523
|
export function finishFanInWork(context, id, result) { const work = requireWork(context, id); assertRunMutationAllowed(context, work.project_id, work.run_id); if (work.status !== "running")
|
|
502
524
|
throw new AppError("invalid_work_state", "Fan-in Work is not running", 2, { work_id: work.work_id, status: work.status }); if (context.db.get("SELECT 1 FROM works WHERE parent_work_id = ? AND status IN ('created','running','paused') LIMIT 1", [work.work_id]))
|
|
@@ -571,7 +593,7 @@ export function retryWork(context, id, reason) {
|
|
|
571
593
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
572
594
|
return { ok: true, work_id: work.work_id, archived_attempt: path.relative(requireRunHome(run), archive).split(path.sep).join("/"), reason };
|
|
573
595
|
}
|
|
574
|
-
async function settle(context, id, status, result, progress) {
|
|
596
|
+
async function settle(context, id, status, result, progress, retry) {
|
|
575
597
|
const work = requireWork(context, id);
|
|
576
598
|
assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
|
|
577
599
|
id = work.work_id;
|
|
@@ -605,7 +627,9 @@ async function settle(context, id, status, result, progress) {
|
|
|
605
627
|
const requiredChecks = packet.repair?.verification_check_refs?.length
|
|
606
628
|
? uniqueChecks([...packet.checks.filter((check) => check.run_at === "work"), ...packet.repair.verification_check_refs])
|
|
607
629
|
: packet.checks.filter((check) => check.run_at === "work");
|
|
608
|
-
|
|
630
|
+
if (retry)
|
|
631
|
+
validateFailedCheckRetry(context, { projectId: work.project_id, runId: work.run_id, receiptId: retry.receiptId, declarations: requiredChecks, reason: retry.reason });
|
|
632
|
+
receipts = await runCodeChecks(context, { projectId: work.project_id, runId: work.run_id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, workId: work.work_id, artifactDir, scope: "work", checks: requiredChecks, ...(retry ? { retryReceiptId: retry.receiptId, retryReason: retry.reason } : {}), ...(progress ? { progress } : {}) });
|
|
609
633
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
610
634
|
if (failed.length)
|
|
611
635
|
throw new AppError("work_checks_failed", "Work remains running because required checks failed", 2, { work_id: id, failures: failed, all_receipts: receipts });
|
|
@@ -3,33 +3,49 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
6
|
-
import { codeExecutionEnvironment, runCheck } from "./code-checks.js";
|
|
6
|
+
import { codeExecutionEnvironment, reconcileWorkspaceOwnership, runCheck, workspaceOperationId } from "./code-checks.js";
|
|
7
7
|
import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedOwnerIsAlive, managedProcessStatus, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
|
|
8
8
|
/** CODE and MERGE prepare their own checkout through the same managed executor.
|
|
9
9
|
* Bootstrap is a project operation, not an alias classified by the engine as a test. */
|
|
10
10
|
export async function runWorkspaceBootstrap(context, input) {
|
|
11
|
-
|
|
11
|
+
reconcileWorkspaceOwnership(context, input.workspaceRoot);
|
|
12
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
13
|
+
const operationId = workspaceOperationId(input.workspaceRoot);
|
|
14
|
+
const identity = crypto.createHash("sha256").update(JSON.stringify([input.projectId, input.runId, generation, path.resolve(input.workspaceRoot), input.command])).digest("hex");
|
|
12
15
|
const pending = managedProcessStatus(context).find(item => item.operation_id === operationId && ["starting", "running", "stopping"].includes(item.state));
|
|
13
16
|
if (pending) {
|
|
17
|
+
if (pending.kind !== "workspace-bootstrap")
|
|
18
|
+
throw new AppError("bootstrap_in_progress", "A check currently owns this workspace; wait for it before bootstrapping", 2, { process_id: pending.id, stdout_path: pending.stdout_path, stderr_path: pending.stderr_path });
|
|
14
19
|
if (processTreeIsAlive(pending) || (pending.state === "starting" && managedOwnerIsAlive(pending) !== false)) {
|
|
15
20
|
throw new AppError("bootstrap_in_progress", "Workspace preparation is still running; inspect its logs and wait for the existing process, do not launch another copy", 2, { process_id: pending.id, stdout_path: pending.stdout_path, stderr_path: pending.stderr_path });
|
|
16
21
|
}
|
|
17
|
-
|
|
18
|
-
let completion = null;
|
|
22
|
+
let pendingIdentity = null;
|
|
19
23
|
try {
|
|
20
|
-
|
|
24
|
+
const metadata = JSON.parse(pending.metadata_json);
|
|
25
|
+
pendingIdentity = typeof metadata.bootstrap_identity === "string" ? metadata.bootstrap_identity : null;
|
|
26
|
+
}
|
|
27
|
+
catch { /* Legacy records have no usable identity. */ }
|
|
28
|
+
if (pendingIdentity !== identity) {
|
|
29
|
+
finishManagedProcess(context, { id: pending.id, leaseToken: pending.lease_token, state: "failed", reason: "bootstrap_identity_superseded" });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const completionPath = path.join(path.dirname(pending.stdout_path), "completion.json");
|
|
33
|
+
let completion = null;
|
|
34
|
+
try {
|
|
35
|
+
completion = JSON.parse(fs.readFileSync(completionPath, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
catch { /* Interrupted shell has no terminal marker. */ }
|
|
38
|
+
const exitCode = typeof completion?.exit_code === "number" ? completion.exit_code : null;
|
|
39
|
+
const error = exitCode === null ? "bootstrap ended without a completion marker; inspect retained logs before retrying" : null;
|
|
40
|
+
finishManagedProcess(context, { id: pending.id, leaseToken: pending.lease_token, state: exitCode === 0 ? "stopped" : "failed", reason: error ?? "bootstrap_completion_reconciled" });
|
|
41
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
42
|
+
return result(pending.id, pending.stdout_path, pending.stderr_path, completionPath, exitCode, error);
|
|
21
43
|
}
|
|
22
|
-
catch { /* Interrupted shell has no terminal marker. */ }
|
|
23
|
-
const exitCode = typeof completion?.exit_code === "number" ? completion.exit_code : null;
|
|
24
|
-
const error = exitCode === null ? "bootstrap ended without a completion marker; inspect retained logs before retrying" : null;
|
|
25
|
-
finishManagedProcess(context, { id: pending.id, leaseToken: pending.lease_token, state: exitCode === 0 ? "stopped" : "failed", reason: error ?? "bootstrap_completion_reconciled" });
|
|
26
|
-
return result(pending.id, pending.stdout_path, pending.stderr_path, completionPath, exitCode, error);
|
|
27
44
|
}
|
|
28
|
-
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
29
45
|
const directory = path.join(input.runHome, input.artifactDir, `bootstrap-${crypto.randomUUID()}`);
|
|
30
46
|
fs.mkdirSync(directory, { recursive: true });
|
|
31
47
|
const stdoutPath = path.join(directory, "stdout.log"), stderrPath = path.join(directory, "stderr.log"), completionPath = path.join(directory, "completion.json");
|
|
32
|
-
const managed = registerManagedProcess(context, { kind: "workspace-bootstrap", ownerId: operationId
|
|
48
|
+
const managed = registerManagedProcess(context, { kind: "workspace-bootstrap", ownerId: `${operationId}:${identity}`, ownerPid: process.pid, operationId, uniqueActiveOperation: true, projectId: input.projectId, runId: input.runId, stdoutPath, stderrPath, metadata: { command: input.command, workspace_root: input.workspaceRoot, bootstrap_identity: identity } });
|
|
33
49
|
const stdout = fs.openSync(stdoutPath, "a"), stderr = fs.openSync(stderrPath, "a");
|
|
34
50
|
try {
|
|
35
51
|
try {
|
|
@@ -44,6 +60,7 @@ export async function runWorkspaceBootstrap(context, input) {
|
|
|
44
60
|
const current = managedProcessStatus(context).find(item => item.id === managed.id);
|
|
45
61
|
if (processTreeIsAlive(current))
|
|
46
62
|
throw new AppError("bootstrap_tree_not_settled", "Bootstrap shell exited but still owns live processes; resources were retained for inspection", 2, { process_id: managed.id, stdout_path: stdoutPath, stderr_path: stderrPath });
|
|
63
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
47
64
|
finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: completed.exitCode === 0 && !completed.error ? "stopped" : "failed", reason: completed.error });
|
|
48
65
|
return result(managed.id, stdoutPath, stderrPath, completionPath, completed.exitCode, completed.error);
|
|
49
66
|
}
|
package/dist/storage/database.js
CHANGED
|
@@ -176,6 +176,9 @@ export function ensureVnextWorkStorage(db) {
|
|
|
176
176
|
command TEXT NOT NULL,
|
|
177
177
|
input_hash TEXT NOT NULL DEFAULT '',
|
|
178
178
|
verification_epoch TEXT NOT NULL DEFAULT '',
|
|
179
|
+
reservation_sequence INTEGER NOT NULL DEFAULT 0,
|
|
180
|
+
retry_of_receipt_id TEXT,
|
|
181
|
+
retry_reason TEXT,
|
|
179
182
|
status TEXT NOT NULL,
|
|
180
183
|
exit_code INTEGER,
|
|
181
184
|
stdout_path TEXT NOT NULL,
|
|
@@ -1030,6 +1033,9 @@ function migrate(db) {
|
|
|
1030
1033
|
ensureColumn(db, "check_receipts", "gate", "ALTER TABLE check_receipts ADD COLUMN gate TEXT NOT NULL DEFAULT 'work'");
|
|
1031
1034
|
ensureColumn(db, "check_receipts", "input_hash", "ALTER TABLE check_receipts ADD COLUMN input_hash TEXT NOT NULL DEFAULT ''");
|
|
1032
1035
|
ensureColumn(db, "check_receipts", "verification_epoch", "ALTER TABLE check_receipts ADD COLUMN verification_epoch TEXT NOT NULL DEFAULT ''");
|
|
1036
|
+
ensureColumn(db, "check_receipts", "reservation_sequence", "ALTER TABLE check_receipts ADD COLUMN reservation_sequence INTEGER NOT NULL DEFAULT 0");
|
|
1037
|
+
ensureColumn(db, "check_receipts", "retry_of_receipt_id", "ALTER TABLE check_receipts ADD COLUMN retry_of_receipt_id TEXT");
|
|
1038
|
+
ensureColumn(db, "check_receipts", "retry_reason", "ALTER TABLE check_receipts ADD COLUMN retry_reason TEXT");
|
|
1033
1039
|
ensureColumn(db, "check_receipts", "before_fingerprint", "ALTER TABLE check_receipts ADD COLUMN before_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
1034
1040
|
ensureColumn(db, "check_receipts", "after_fingerprint", "ALTER TABLE check_receipts ADD COLUMN after_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
1035
1041
|
ensureColumn(db, "check_receipts", "profile_hash", "ALTER TABLE check_receipts ADD COLUMN profile_hash TEXT");
|