@deksden-com/dd-flow-cli 0.9.0-beta.49 → 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.
@@ -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
- const registered = registerCode(context, project.id, run.id, batch);
74
- const report = reportFor({ run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
75
- writeReport(root, report);
76
- 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" });
77
- advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
78
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
79
- 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) } };
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
- const registered = registerCode(context, project.id, run.id, batch);
245
- const now = context.now();
246
- 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") });
247
- 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) });
248
- applyReviewResults(home, latestChildren, decision);
249
- writeReport(root, report);
250
- refreshRunWorkProjection(context, project.id, run.id);
251
- 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" });
252
- advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
253
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
254
- 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) } };
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
- const owner = context.db.get(`SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id
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("BEGIN IMMEDIATE");
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
- export function finishWork(context, id, result, progress) { return settle(context, id, "completed", result, progress); }
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
- 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, ...(progress ? { progress } : {}) });
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
- const operationId = `bootstrap:${crypto.createHash("sha256").update(JSON.stringify([input.runId, input.workspaceRoot, input.artifactDir, input.command])).digest("hex")}`;
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
- const completionPath = path.join(path.dirname(pending.stdout_path), "completion.json");
18
- let completion = null;
22
+ let pendingIdentity = null;
19
23
  try {
20
- completion = JSON.parse(fs.readFileSync(completionPath, "utf8"));
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, ownerPid: process.pid, operationId, uniqueActiveOperation: true, projectId: input.projectId, runId: input.runId, stdoutPath, stderrPath, metadata: { command: input.command, workspace_root: input.workspaceRoot } });
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
  }
@@ -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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.49",
3
+ "version": "0.9.0-beta.51",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {