@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/build-info.json +6 -6
  3. package/dist/cli/run-cli.js +45 -4
  4. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -0
  5. package/dist/harness-runtime/lib/process-json.mjs +15 -6
  6. package/dist/services/cleanup.js +1 -1
  7. package/dist/services/code-checks.js +1 -1
  8. package/dist/services/eval-snapshots.js +2 -2
  9. package/dist/services/harness-adapter.js +61 -9
  10. package/dist/services/hooks.js +26 -0
  11. package/dist/services/lanes.js +1 -1
  12. package/dist/services/lifecycle-invocations.js +194 -22
  13. package/dist/services/managed-processes.js +2 -2
  14. package/dist/services/merge-queue.js +6 -6
  15. package/dist/services/merge-server.js +1 -1
  16. package/dist/services/migrations.js +1 -1
  17. package/dist/services/projects.js +1 -1
  18. package/dist/services/protocols.js +2 -2
  19. package/dist/services/run-control-worker.js +3 -3
  20. package/dist/services/run-control.js +1 -1
  21. package/dist/services/run-controller-adapter.js +8 -3
  22. package/dist/services/run-controller-capture.js +2 -2
  23. package/dist/services/run-controller-process.js +2 -2
  24. package/dist/services/run-controller-recovery.js +3 -3
  25. package/dist/services/run-controller.js +297 -180
  26. package/dist/services/run-recovery-runtime.js +1 -1
  27. package/dist/services/run-recovery.js +3 -3
  28. package/dist/services/runs.js +13 -14
  29. package/dist/services/runtime-budget.js +4 -4
  30. package/dist/services/runtime-scope-control.js +5 -5
  31. package/dist/services/runtime-scope-resume.js +7 -7
  32. package/dist/services/runtime-scope-worker.js +2 -2
  33. package/dist/services/stage-pause.js +2 -2
  34. package/dist/services/vnext-code-review.js +9 -4
  35. package/dist/services/vnext-code.js +10 -3
  36. package/dist/services/vnext-merge.js +16 -21
  37. package/dist/services/vnext-plan-review.js +4 -2
  38. package/dist/services/vnext-plan.js +1 -1
  39. package/dist/services/work-registry.js +198 -121
  40. package/dist/storage/database.js +112 -20
  41. package/dist/storage/writer-contract.js +2 -1
  42. package/dist/storage/writer-migration.js +2 -2
  43. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.64
4
+
5
+ ### Patch Changes
6
+
7
+ - Reserve SQLite writes explicitly, retain trusted lifecycle outcomes across harnesses,
8
+ continuously fence failed managed runs, and recover committed Work start packets and
9
+ replies without repeating effects. Revalidate stage inputs before acceptance and keep
10
+ the causal failure when adapter, publication or cleanup also fails. Writer contract 2
11
+ requires fresh campaign and resource homes; historical contract-1 stores are not
12
+ implicitly upgraded. Recovery-generation Work starts keep separate immutable packets
13
+ per WorkSession, preserving the interrupted attempt's evidence.
14
+
3
15
  ## 0.9.0-beta.63
4
16
 
5
17
  ### Patch Changes
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.63",
4
- "cli_commit": "4975ba184bd847fd34db995cd3d9e1feb3db9f9f",
5
- "built_at": "2026-09-14T10:36:19.793Z",
3
+ "cli_version": "0.9.0-beta.64",
4
+ "cli_commit": "53046a13b2a4370531d64b380974abbd6bf2383b",
5
+ "built_at": "2026-09-14T16:28:38.823Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.1",
8
8
  "commit": "97f811d33c212ae3497020178b1ed825c7c3ebac",
9
9
  "flow_contract": "dd-flow-canonical-2026-08",
10
- "repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
11
- "memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
12
- "flow_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank/dd-flow",
10
+ "repo_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank",
11
+ "memorybank_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank/.memory-bank",
12
+ "flow_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank/.memory-bank/dd-flow",
13
13
  "layout": "dot_memory_bank"
14
14
  }
15
15
  }
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { quote } from "shell-quote";
5
+ import { observeLifecycleCommand } from "../services/lifecycle-invocations.js";
4
6
  import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, issueLifecycleInvocation, lifecycleInvocationScope, settleLifecycleInvocation, settleLifecycleRejection } from "../services/lifecycle-invocations.js";
5
7
  import { createContext, createRouterContext } from "../runtime/context.js";
6
8
  import { migrateStoreWriter } from "../storage/writer-migration.js";
@@ -84,6 +86,8 @@ export async function runCli(args, io = defaultIo, env = process.env) {
84
86
  const progress = progressForCommand(output.args);
85
87
  let heartbeat;
86
88
  let invocationSettlement;
89
+ let settleHook;
90
+ let diagnosticHome;
87
91
  try {
88
92
  if (output.args.length === 1 && output.args[0] === "--version") {
89
93
  const payload = getCliVersionReport();
@@ -130,17 +134,22 @@ export async function runCli(args, io = defaultIo, env = process.env) {
130
134
  const dispatchContext = routed?.inProcess
131
135
  ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(output.args, classification))
132
136
  : createContext({ ...env }, databaseModeForCommand(output.args, classification));
137
+ diagnosticHome = dispatchContext.ddFlowHome;
133
138
  preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
134
139
  dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
135
140
  let dispatchArgs = output.args;
141
+ settleHook = observeLifecycleCommand(dispatchContext, quote(["dd-flow", ...dispatchArgs]));
136
142
  let retainedOutcome;
137
143
  const invocationId = optionalOption(parseArgs(output.args.slice(2)), "invocation-id");
138
144
  if (invocationId) {
139
145
  const command = quote(["dd-flow", ...output.args]);
140
146
  const admission = await awaitLifecycleInvocation(dispatchContext, { id: invocationId, command,
141
147
  assertCurrent: scope => assertLifecycleInvocationCurrent(dispatchContext, scope, command) });
142
- if (admission.replay)
148
+ if (admission.replay) {
143
149
  retainedOutcome = admission.outcome;
150
+ settleHook = undefined;
151
+ delete dispatchContext.observeLifecycleHook;
152
+ }
144
153
  else {
145
154
  invocationSettlement = { context: dispatchContext, id: invocationId };
146
155
  dispatchArgs = [...output.args, "--hook-event-id", admission.eventKey];
@@ -152,7 +161,12 @@ export async function runCli(args, io = defaultIo, env = process.env) {
152
161
  const error = retainedOutcome.error;
153
162
  throw new AppError(error.code, error.message, error.exitCode, error.details);
154
163
  }
164
+ if (!retainedOutcome)
165
+ settleHook = observeLifecycleCommand(dispatchContext, quote(["dd-flow", ...dispatchArgs]));
155
166
  const result = retainedOutcome ? retainedOutcome.result : await dispatch(dispatchArgs, dispatchContext, io, scopeProjectRoot, classification, output);
167
+ const completedHook = settleHook;
168
+ settleHook = undefined;
169
+ completedHook?.({ result });
156
170
  if (invocationSettlement) {
157
171
  const completed = invocationSettlement;
158
172
  invocationSettlement = undefined;
@@ -175,10 +189,23 @@ export async function runCli(args, io = defaultIo, env = process.env) {
175
189
  io.stdout.write(renderHumanResult(output.args, payload));
176
190
  return completedCommandExitCode(payload);
177
191
  }
178
- catch (error) {
192
+ catch (caught) {
193
+ const error = isAppError(caught) ? caught : new AppError("unexpected", caught instanceof Error ? caught.message : String(caught), 1, { effect: "unknown", cause: { code: caught?.code ?? null, sqlite_extended_code: caught?.errcode ?? null } });
194
+ if (diagnosticHome && (!isAppError(caught) || error.code === "storage_write_failed")) {
195
+ try {
196
+ const directory = path.join(diagnosticHome, "diagnostics", "cli");
197
+ fs.mkdirSync(directory, { recursive: true });
198
+ const artifact = path.join(directory, `${randomUUID()}.json`);
199
+ fs.writeFileSync(artifact, JSON.stringify({ code: error.code, message: error.message, details: error.details, stack: caught instanceof Error ? caught.stack : null }), { flag: "wx", mode: 0o600 });
200
+ error.details.diagnostic_artifact = artifact;
201
+ }
202
+ catch (secondary) {
203
+ io.stderr.write(`CLI diagnostic persistence failed: ${String(secondary)}\n`);
204
+ }
205
+ }
179
206
  if (heartbeat)
180
207
  clearInterval(heartbeat);
181
- if (invocationSettlement) {
208
+ if (invocationSettlement && error.details.effect !== "committed") {
182
209
  try {
183
210
  if (isAppError(error))
184
211
  settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, { code: error.code, message: error.message, exitCode: error.exitCode, details: error.details });
@@ -189,6 +216,14 @@ export async function runCli(args, io = defaultIo, env = process.env) {
189
216
  io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
190
217
  }
191
218
  }
219
+ if (settleHook) {
220
+ try {
221
+ settleHook({ error: isAppError(error) ? error : { code: "unexpected", message: String(error), details: {} } });
222
+ }
223
+ catch (persistence) {
224
+ io.stderr.write(`${JSON.stringify({ primary: String(error), lifecycle_persistence_error: String(persistence) })}\n`);
225
+ }
226
+ }
192
227
  const payload = isAppError(error)
193
228
  ? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
194
229
  : { ok: false, error: { code: "unexpected", message: String(error) } };
@@ -196,7 +231,13 @@ export async function runCli(args, io = defaultIo, env = process.env) {
196
231
  if (output.progressJsonl)
197
232
  writeProgressJsonl(io, "failed", payload.error.message, { code: payload.error.code });
198
233
  const errorPayload = attachProgress(payload, progress, output);
199
- const visible = writeResponseFile(errorPayload, output);
234
+ let visible = errorPayload;
235
+ try {
236
+ visible = writeResponseFile(errorPayload, output);
237
+ }
238
+ catch (publication) {
239
+ error.details.response_publication_error = { code: publication?.code ?? "response_publication_failed", message: publication instanceof Error ? publication.message : String(publication) };
240
+ }
200
241
  if (output.json && output.progressJsonl)
201
242
  writeJson(io.stdout, errorPayload);
202
243
  else if (output.json)
@@ -28,6 +28,7 @@ export async function invokeNativeHook({ bin, args, payload, home, timeoutMs = 1
28
28
  });
29
29
  let result;
30
30
  try { result = JSON.parse(output); } catch { throw Object.assign(new Error("Invalid native hook JSON response"), { code: "native_hook_response_invalid" }); }
31
+ if (result?.ok === false && typeof result.error?.code === "string") throw Object.assign(new Error(result.error.message ?? "Native hook failed"), { code: result.error.code, details: result.error.details });
31
32
  if (!result || typeof result !== "object" || Array.isArray(result) || result.ok === false) throw Object.assign(new Error("Native hook did not return a successful protocol object"), { code: "native_hook_response_invalid" });
32
33
  return result;
33
34
  }
@@ -30,22 +30,31 @@ export async function commandJson(bin, args, { cwd, env = {}, input = null, onPr
30
30
  const executable = bin === "dd-flow" && typeof env.DD_FLOW_BIN === "string" ? env.DD_FLOW_BIN : bin;
31
31
  const target = processTarget(executable, [...args, "--json"]);
32
32
  const child = spawn(target.command, target.args, { cwd, env: { ...process.env, ...env }, stdio: [input === null ? "ignore" : "pipe", "pipe", "pipe"] });
33
- let stdout = ""; let stderr = ""; let pendingStderr = "";
33
+ let stdout = ""; let stderr = ""; let pendingStderr = ""; let settled = false; let progressError = null;
34
+ const finish = (error, value) => { if (settled) return; settled = true; error ? reject(error) : resolve(value); };
35
+ const deliverProgress = (line) => {
36
+ let value; try { value = JSON.parse(line); } catch { return; }
37
+ if (typeof onProgress !== "function" || progressError) return;
38
+ try { const returned = onProgress(value); if (returned && typeof returned.then === "function") { Promise.resolve(returned).catch(() => {}); throw failure("commandJson onProgress must be synchronous", "progress_callback_async"); } }
39
+ catch (error) { progressError = error; child.kill("SIGKILL"); }
40
+ };
34
41
  child.stdout.setEncoding("utf8").on("data", (data) => { stdout += data; });
35
42
  child.stderr.setEncoding("utf8").on("data", (data) => {
36
43
  stderr += data; pendingStderr += data;
37
44
  for (;;) {
38
45
  const end = pendingStderr.indexOf("\n"); if (end < 0) break;
39
46
  const line = pendingStderr.slice(0, end); pendingStderr = pendingStderr.slice(end + 1);
40
- if (typeof onProgress === "function") { try { onProgress(JSON.parse(line)); } catch { /* Ordinary diagnostics stay in stderr evidence. */ } }
47
+ deliverProgress(line);
41
48
  }
42
49
  });
43
- child.on("error", reject);
50
+ child.once("error", (error) => finish(progressError ?? error));
44
51
  if (input !== null) child.stdin.end(input);
45
52
  child.on("close", (status) => {
46
- if (status !== 0) return reject(reportedFailure(stdout, stderr, `${executable} exited ${status}`));
47
- try { resolve(JSON.parse(stdout.trim())); }
48
- catch (error) { reject(failure(`${bin} returned invalid JSON: ${error.message}`, "flow_reconciliation_failed")); }
53
+ if (pendingStderr) deliverProgress(pendingStderr);
54
+ if (progressError) return finish(progressError);
55
+ if (status !== 0) return finish(reportedFailure(stdout, stderr, `${executable} exited ${status}`));
56
+ try { const value = JSON.parse(stdout.trim()); if (value?.ok === false) { const error = value.error && typeof value.error === "object" ? value.error : value; if (typeof error.code === "string") return finish(reportedError(error, `${bin} returned an error envelope`)); } finish(null, value); }
57
+ catch (error) { finish(failure(`${bin} returned invalid JSON: ${error.message}`, "flow_reconciliation_failed")); }
49
58
  });
50
59
  });
51
60
  }
@@ -184,7 +184,7 @@ export function cleanupApply(context, input) {
184
184
  validateNoDestructiveSurprises(context, plan, input.force);
185
185
  const now = context.now();
186
186
  const results = [];
187
- context.db.exec("BEGIN IMMEDIATE");
187
+ context.db.beginWriteTransaction();
188
188
  try {
189
189
  for (const action of plan.actions) {
190
190
  results.push(applyAction(context, action, reason, input.force, now));
@@ -291,7 +291,7 @@ export function reconcileUnfinishedChecks(context, input) {
291
291
  return reconciled;
292
292
  }
293
293
  function reserveOrReuseReceipt(context, input) {
294
- context.db.exec("BEGIN IMMEDIATE");
294
+ context.db.beginWriteTransaction();
295
295
  try {
296
296
  assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
297
297
  const prior = latestApplicableReceipt(context, input);
@@ -405,7 +405,7 @@ function detachSnapshotRuntimeOwners(context, snapshot, manifest) {
405
405
  fs.writeFileSync(file, `${JSON.stringify({ schema_id: "dd-flow/snapshot-runtime-lineage@1", source_snapshot: snapshot, source_manifest_sha256: fileHash(path.join(snapshot, "snapshot.json")), source_runtime: manifest.dd_flow_home, restored_runtime: context.ddFlowHome, run_id: manifest.run_id, run_tables: runTables, resource_tables: resourceTables }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
406
406
  syncPath(file);
407
407
  syncPath(directory);
408
- context.db.exec("BEGIN IMMEDIATE");
408
+ context.db.beginWriteTransaction();
409
409
  try {
410
410
  context.db.run("UPDATE sessions SET metadata_json = json_set(metadata_json, '$.snapshot_import', json(?))", [JSON.stringify({ lineage_file: file, lineage_sha256: fileHash(file) })]);
411
411
  for (const table of detachedTables)
@@ -439,7 +439,7 @@ function detachSnapshotRuntimeOwners(context, snapshot, manifest) {
439
439
  }
440
440
  function rebaseRuntime(context, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome, recovery = false) {
441
441
  context.db.exec("PRAGMA foreign_keys = OFF");
442
- context.db.exec("BEGIN IMMEDIATE");
442
+ context.db.beginWriteTransaction();
443
443
  try {
444
444
  context.db.run("UPDATE projects SET root = ?", [newRoot]);
445
445
  context.db.run("UPDATE runs SET project_root = ?, workspace_root = ?", [newRoot, newWorkspace]);
@@ -53,13 +53,19 @@ export function runHarnessAdapter(input) {
53
53
  env.DD_FLOW_RUNTIME_OWNER = JSON.stringify({ ...input.runtimeOwner, adapter_executable: input.executable });
54
54
  }
55
55
  return new Promise((resolve, reject) => {
56
+ if (input.signal?.aborted)
57
+ return reject(new AppError("harness_adapter_aborted", "Harness adapter call was cancelled before dispatch", 1));
56
58
  const detached = process.platform !== "win32";
57
59
  const child = spawn(input.executable, input.args, { env, ...(input.cwd ? { cwd: input.cwd } : {}), detached, stdio: ["ignore", "pipe", "pipe"] });
58
60
  let stdout = "";
59
61
  let stderr = "";
60
62
  let outputTooLarge = false;
61
63
  let timedOut = false;
64
+ let aborted = false;
62
65
  let killTimer;
66
+ let settled = false;
67
+ let callbackError;
68
+ let spawnError;
63
69
  const terminate = (signal) => { try {
64
70
  if (detached && child.pid)
65
71
  process.kill(-child.pid, signal);
@@ -67,39 +73,73 @@ export function runHarnessAdapter(input) {
67
73
  child.kill(signal);
68
74
  }
69
75
  catch { /* The process already settled. */ } };
70
- const heartbeat = input.progress && input.progressMessage ? setInterval(() => input.progress?.(`${input.progressMessage}; waiting for adapter completion`), 15_000) : undefined;
71
- const timer = setTimeout(() => { timedOut = true; terminate("SIGTERM"); killTimer = setTimeout(() => terminate("SIGKILL"), 5_000); }, timeoutMs);
76
+ const stopTransport = () => { terminate("SIGTERM"); killTimer ??= setTimeout(() => terminate("SIGKILL"), 5_000); };
77
+ const deliver = (callback) => {
78
+ try {
79
+ const result = callback();
80
+ if (result && typeof result.then === "function") {
81
+ void Promise.resolve(result).catch(() => { });
82
+ throw new AppError("progress_callback_async", "Harness callbacks must be synchronous", 1);
83
+ }
84
+ }
85
+ catch (error) {
86
+ callbackError ??= error;
87
+ if (!settled)
88
+ stopTransport();
89
+ }
90
+ };
91
+ const heartbeat = input.progress && input.progressMessage ? setInterval(() => deliver(() => input.progress?.(`${input.progressMessage}; waiting for adapter completion`)), 15_000) : undefined;
92
+ const timer = setTimeout(() => { timedOut = true; stopTransport(); }, timeoutMs);
93
+ const abort = () => { aborted = true; stopTransport(); };
94
+ if (input.signal?.aborted)
95
+ abort();
96
+ else
97
+ input.signal?.addEventListener("abort", abort, { once: true });
72
98
  const evidence = (value) => input.onEvidence?.({ operation_id: input.operationId, executable: input.executable, args: input.args, ...value, at: new Date().toISOString() });
73
99
  const append = (current, chunk) => {
74
100
  if (Buffer.byteLength(current) + Buffer.byteLength(chunk) > maxAdapterOutputBytes) {
75
101
  outputTooLarge = true;
76
- terminate("SIGTERM");
102
+ stopTransport();
77
103
  return current;
78
104
  }
79
105
  return current + chunk;
80
106
  };
81
107
  child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout = append(stdout, chunk); });
82
108
  child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr = append(stderr, chunk); });
83
- child.once("error", (error) => { clearTimeout(timer); if (heartbeat)
84
- clearInterval(heartbeat); if (killTimer)
85
- clearTimeout(killTimer); evidence({ status: null, error: error.message }); reject(new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { executable: input.executable, args: input.args.slice(0, 3), error: error.message })); });
109
+ child.once("error", (error) => { spawnError = error; });
86
110
  child.once("close", (status, signal) => {
111
+ if (settled)
112
+ return;
113
+ settled = true;
87
114
  clearTimeout(timer);
115
+ input.signal?.removeEventListener("abort", abort);
88
116
  if (heartbeat)
89
117
  clearInterval(heartbeat);
90
- if (killTimer)
118
+ if (killTimer) {
91
119
  clearTimeout(killTimer);
92
- evidence({ status, signal, timed_out: timedOut, stdout, stderr });
120
+ terminate("SIGKILL");
121
+ }
122
+ const progressFailure = callbackError;
123
+ deliver(() => evidence({ status, signal, timed_out: timedOut, aborted, stdout, stderr, ...(spawnError ? { error: spawnError.message } : {}) }));
124
+ if (spawnError)
125
+ return reject(new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { cause: adapterCause(spawnError.message), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
93
126
  if (outputTooLarge)
94
127
  return reject(new AppError("harness_adapter_output_limit", "Harness adapter exceeded the bounded reply limit", 1, { executable: input.executable, args: input.args.slice(0, 3), max_output_bytes: maxAdapterOutputBytes }));
95
128
  if (timedOut)
96
129
  return reject(new AppError("harness_adapter_timeout", "Harness adapter call timed out", 1, { executable: input.executable, args: input.args.slice(0, 3), timeout_ms: timeoutMs }));
130
+ if (aborted)
131
+ return reject(new AppError("harness_adapter_aborted", "Harness adapter call was cancelled", 1, { executable: input.executable, args: input.args.slice(0, 3) }));
97
132
  if (status !== 0)
98
- return reject(new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { executable: input.executable, args: input.args.slice(0, 3), status, signal, stderr }));
133
+ return reject(progressFailure ?? new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { executable: input.executable, args: input.args.slice(0, 3), status, signal, stderr, cause: adapterCause(stdout, stderr), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
134
+ if (callbackError)
135
+ return reject(callbackError);
99
136
  try {
100
137
  const reply = JSON.parse(stdout);
101
138
  if (!reply || typeof reply !== "object" || Array.isArray(reply))
102
139
  throw new Error("not object");
140
+ const value = reply;
141
+ if (value.ok === false && typeof value.error?.code === "string")
142
+ return reject(new AppError("harness_adapter_failed", "Harness adapter reported a runtime failure", 1, { cause: adapterCause(stdout) }));
103
143
  resolve(reply);
104
144
  }
105
145
  catch {
@@ -108,6 +148,18 @@ export function runHarnessAdapter(input) {
108
148
  });
109
149
  });
110
150
  }
151
+ function adapterCause(...outputs) {
152
+ for (const output of outputs)
153
+ for (const line of [output.trim(), ...output.trim().split("\n").reverse()]) {
154
+ try {
155
+ const value = JSON.parse(line);
156
+ if (typeof value.error?.code === "string")
157
+ return { ...value.error, message: typeof value.error.message === "string" ? value.error.message : value.error.code };
158
+ }
159
+ catch { /* Adapter diagnostics need not be JSON. */ }
160
+ }
161
+ return { message: outputs.map(output => output.trim()).filter(Boolean).join("\n") || "adapter process failed" };
162
+ }
111
163
  export function adapterSessionId(value) {
112
164
  if (!value || typeof value !== "object")
113
165
  return null;
@@ -879,6 +879,31 @@ function lifecycleMatchKey(facts, projectRoot) {
879
879
  return workLifecycleMatchKey("work_fail", workId, projectRoot);
880
880
  return null;
881
881
  }
882
+ /** Observe exact immutable command attribution before reading model-supplied
883
+ * result files. This does not claim or authorize the lifecycle mutation. */
884
+ export function observeCommandHook(context, command) {
885
+ const facts = lifecycleFacts(command);
886
+ if (!facts || facts.analysis.kind !== "standalone" || !facts.commandProjectRoot)
887
+ return;
888
+ const project = context.db.get("SELECT id FROM projects WHERE root = ?", [resolveProjectRoot(facts.commandProjectRoot)]);
889
+ if (!project)
890
+ return; // Bootstrap has not registered a project yet.
891
+ const matchKey = lifecycleMatchKey(facts, facts.commandProjectRoot);
892
+ if (!matchKey)
893
+ return;
894
+ let key = commandOption(facts.invocation, "hook-event-id");
895
+ if (!key) {
896
+ try {
897
+ key = findRecentMatchingHookEvent(context, { projectId: project.id, matchKey, errorCode: "trusted_session_binding_required", operation: facts.invocation.operation }).eventKey;
898
+ }
899
+ catch {
900
+ return;
901
+ } // Dispatch still owns its mandatory admission error.
902
+ }
903
+ const event = context.db.get("SELECT id FROM hook_events WHERE project_id = ? AND event_key = ? AND match_key = ? AND status = 'observed'", [project.id, key, matchKey]);
904
+ if (event)
905
+ context.observeLifecycleHook?.(project.id, event.id);
906
+ }
882
907
  export function stageLifecycleMatchKey(operation, runId, stage, projectRoot, workId) {
883
908
  return crypto.createHash("sha256").update(JSON.stringify({ operation, run_id: runId, stage, project_root: resolveProjectRoot(projectRoot), ...(workId ? { work_id: workId } : {}) })).digest("hex");
884
909
  }
@@ -935,6 +960,7 @@ export function hookSessionIdentity(context, projectId, eventKey) {
935
960
  WHERE he.project_id = ? AND he.event_key = ?`, [projectId, eventKey]);
936
961
  if (!event?.session_id)
937
962
  throw new AppError("hook_event_not_found", "stage start requires a trusted PreToolUse hook event", 1, { event_key: eventKey });
963
+ context.observeLifecycleHook?.(projectId, event.id);
938
964
  return {
939
965
  hookEventId: event.id,
940
966
  harness: event.harness,
@@ -515,7 +515,7 @@ function ensureQueuedLaneWaiter(context, input) {
515
515
  function tryAcquireQueuedLaneLock(context, input) {
516
516
  expireStaleLocks(context, input.projectId);
517
517
  expireStaleWaiters(context, input.projectId);
518
- context.db.exec("BEGIN IMMEDIATE");
518
+ context.db.beginWriteTransaction();
519
519
  try {
520
520
  const existingLock = activeLaneLock(context, input.projectId, input.lane);
521
521
  if (existingLock?.worker_id === input.workerId) {