@deksden-com/dd-flow-cli 0.9.0-beta.124 → 0.9.0-beta.127

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 (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  14. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  17. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  18. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  19. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  20. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  21. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  22. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  23. package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
  24. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
  25. package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
  26. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
  27. package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
  28. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
  29. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  30. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
  31. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
  33. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  34. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
  35. package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
  36. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  37. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  38. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  39. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  40. package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
  41. package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
  42. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  43. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  44. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  45. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  46. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  47. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  48. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  49. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  50. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  51. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  52. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  53. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  54. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  55. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  56. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  57. package/dist/runtime/context.js +8 -4
  58. package/dist/schemas/run-control-receipt.schema.json +19 -0
  59. package/dist/services/code-checks.js +79 -9
  60. package/dist/services/codex-capacity.js +117 -41
  61. package/dist/services/codex-hook-delivery.js +2 -2
  62. package/dist/services/compatibility-preflight.js +4 -1
  63. package/dist/services/controller-fanout.js +9 -76
  64. package/dist/services/engines.js +70 -20
  65. package/dist/services/eval-snapshots.js +179 -40
  66. package/dist/services/external-work-launch.js +13 -8
  67. package/dist/services/harness-adapter.js +175 -11
  68. package/dist/services/hooks.js +65 -21
  69. package/dist/services/lifecycle-invocations.js +404 -32
  70. package/dist/services/managed-lease-monitor.js +128 -0
  71. package/dist/services/managed-processes.js +103 -47
  72. package/dist/services/merge-server.js +58 -21
  73. package/dist/services/recovery-observation-budget.js +67 -7
  74. package/dist/services/recovery-snapshot-database.js +7 -4
  75. package/dist/services/run-control-receipt.js +1 -1
  76. package/dist/services/run-control-worker.js +177 -118
  77. package/dist/services/run-control.js +99 -8
  78. package/dist/services/run-controller-adapter.js +169 -46
  79. package/dist/services/run-controller-capture.js +14 -2
  80. package/dist/services/run-controller-process.js +11 -4
  81. package/dist/services/run-controller-state.js +68 -7
  82. package/dist/services/run-controller.js +166 -94
  83. package/dist/services/run-fork.js +8 -3
  84. package/dist/services/run-recovery-guard.js +75 -0
  85. package/dist/services/run-recovery-runtime.js +1 -56
  86. package/dist/services/run-recovery.js +17 -13
  87. package/dist/services/runs.js +39 -6
  88. package/dist/services/runtime-budget.js +32 -15
  89. package/dist/services/runtime-scope-control.js +8 -0
  90. package/dist/services/runtime-scope-resume.js +30 -8
  91. package/dist/services/runtime-scope-worker.js +118 -73
  92. package/dist/services/runtime-service.js +6 -7
  93. package/dist/services/scope-writer-cleanup.js +3 -2
  94. package/dist/services/stage-context.js +135 -0
  95. package/dist/services/stage-lifecycle.js +3 -15
  96. package/dist/services/stage-pause.js +3 -2
  97. package/dist/services/usage.js +14 -3
  98. package/dist/services/vnext-code-review.js +10 -9
  99. package/dist/services/vnext-code.js +10 -8
  100. package/dist/services/vnext-contracts.js +10 -1
  101. package/dist/services/vnext-execution-profile.js +75 -2
  102. package/dist/services/vnext-fanout.js +5 -0
  103. package/dist/services/vnext-merge.js +10 -9
  104. package/dist/services/vnext-plan-review.js +21 -17
  105. package/dist/services/vnext-plan.js +9 -7
  106. package/dist/services/vnext-protocolize.js +15 -9
  107. package/dist/services/vnext-specify.js +6 -6
  108. package/dist/services/work-registry.js +34 -4
  109. package/dist/services/workspace-bootstrap.js +14 -3
  110. package/dist/shared/errors.js +11 -6
  111. package/dist/storage/agent-output.js +26 -0
  112. package/dist/storage/database.js +80 -21
  113. package/package.json +7 -4
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { createContext } from "../runtime/context.js";
6
- import { AppError } from "../shared/errors.js";
6
+ import { AppError, errorRecord } from "../shared/errors.js";
7
7
  import { getResourceDatabase } from "../storage/database.js";
8
8
  import { managedProcessBinding, processIsAlive, processTreeIsAlive, resourceHome } from "./managed-processes.js";
9
9
  import { runtimeBudgetStatus, scopeLocalProcessRole, validateRetainedRuntimeScopeManifest, prepareRuntimeScopeFence } from "./runtime-budget.js";
@@ -13,6 +13,9 @@ import { prepareControllerResume, resumeRunController } from "./run-controller-r
13
13
  import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
14
14
  import { assertHistoricalDaemonSettlement } from "./native-daemon-history.js";
15
15
  import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
16
+ import { harnessKey } from "./harness-config.js";
17
+ import { canonicalNativeOperation } from "../harness-runtime/lib/adapter-timeouts.mjs";
18
+ import { retainedDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
16
19
  const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
17
20
  const nativeLaunchEnvironmentKeys = ["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"];
18
21
  function nativeLaunchEnvironment(context, saved) {
@@ -36,6 +39,7 @@ function nativeLaunchEnvironment(context, saved) {
36
39
  export async function prepareRuntimeScopeResume(context, scopeId, generation, releaseRequestId) {
37
40
  const db = getResourceDatabase(resourceHome(context));
38
41
  const assertCurrent = () => {
42
+ context.assertControlOwner?.();
39
43
  const latest = runtimeBudgetStatus(context, scopeId);
40
44
  if (latest.fence || latest.control?.generation !== generation)
41
45
  throw new AppError("runtime_scope_control_superseded", "Scope resume preparation lost its generation", 1);
@@ -76,7 +80,7 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
76
80
  runs.push({ ...source, receipt });
77
81
  }
78
82
  catch (error) {
79
- runs.push({ ...source, pending: true, reason: error instanceof AppError ? error.code : "runtime_scope_resume_failed", message: String(error) });
83
+ runs.push({ ...source, pending: true, reason: error instanceof AppError ? error.code : "runtime_scope_resume_failed", message: String(error), error: errorRecord(error) });
80
84
  }
81
85
  finally {
82
86
  target?.db.close?.();
@@ -92,7 +96,8 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
92
96
  native.push(restarting || retired?.status === "retired" ? await restartScopeNative(context, intent, source, assertCurrent) : retired);
93
97
  }
94
98
  catch (error) {
95
- native.push({ ...source, pending: true, reason: error instanceof AppError ? error.code : "runtime_scope_resume_failed", message: String(error) });
99
+ const failure = errorRecord(error);
100
+ native.push({ ...source, pending: true, reason: failure.code === "unexpected" ? "runtime_scope_resume_failed" : failure.code, message: String(error), error: failure });
96
101
  }
97
102
  }
98
103
  assertCurrent();
@@ -101,6 +106,7 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
101
106
  const lockedRuns = new Map(), heldRuns = [];
102
107
  let registryHeld = false, runPreparationConsistent = false;
103
108
  let released = null, releaseError = null, releaseMessage = null;
109
+ let releaseFailure = null;
104
110
  let cleanupErrors = [];
105
111
  try {
106
112
  for (const home of [...new Set(intent.runs.map((source) => source.runtime_home))].sort())
@@ -134,8 +140,9 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
134
140
  catch (error) {
135
141
  releaseError = error instanceof AppError ? error.code : "scope_writer_boundary_unavailable";
136
142
  releaseMessage = error instanceof Error ? error.message : String(error);
143
+ releaseFailure = errorRecord(error);
137
144
  for (const [index, run] of runs.entries())
138
- runs[index] = { ...run, prepared: false, pending: true, reason: error instanceof AppError ? error.code : "scope_writer_boundary_unavailable" };
145
+ runs[index] = { ...run, prepared: false, pending: true, reason: error instanceof AppError ? error.code : "scope_writer_boundary_unavailable", error: releaseFailure };
139
146
  }
140
147
  finally {
141
148
  cleanupErrors = cleanupScopeWriters([
@@ -146,9 +153,9 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
146
153
  if (released)
147
154
  return { ...released, status: "released", dispatch_blocked: false, current: true, settled: false, reused: false, ...(cleanupErrors.length ? { cleanup_errors: cleanupErrors } : {}) };
148
155
  if (cleanupErrors.length)
149
- throw new AppError(releaseError ?? "scope_writer_cleanup_failed", releaseMessage ?? "Scope preparation cleanup failed", 1, { scope_cleanup_errors: cleanupErrors });
156
+ throw new AppError(releaseError ?? "scope_writer_cleanup_failed", releaseMessage ?? "Scope preparation cleanup failed", 1, { ...(releaseFailure ? { cause: releaseFailure } : {}), scope_cleanup_errors: cleanupErrors });
150
157
  assertCurrent();
151
- return { scope_id: scopeId, generation, request_id: intent.request_id, status: "preparing", dispatch_blocked: true, settled: false, release_error: releaseError ? { code: releaseError, message: releaseMessage } : null, run_preparation_consistent: runPreparationConsistent, runs, native, probes: intent.probes, pending_reasons: [...new Set(["scope_release_required", ...(releaseError ? [releaseError] : []), ...(!runPreparationConsistent ? ["scope_writer_boundary_unavailable"] : []), ...(runs.some(item => item.prepared !== true) ? ["run_recovery_required"] : []), ...(native.some(item => item?.prepared !== true) ? ["non_work_recovery_required"] : []), ...(intent.probes.length ? ["probe_reconciliation_required"] : [])])] };
158
+ return { scope_id: scopeId, generation, request_id: intent.request_id, status: "preparing", dispatch_blocked: true, settled: false, release_error: releaseError ? { ...releaseFailure, code: releaseError, message: releaseMessage } : null, run_preparation_consistent: runPreparationConsistent, runs, native, probes: intent.probes, pending_reasons: [...new Set(["scope_release_required", ...(releaseError ? [releaseError] : []), ...(!runPreparationConsistent ? ["scope_writer_boundary_unavailable"] : []), ...(runs.some(item => item.prepared !== true) ? ["run_recovery_required"] : []), ...(native.some(item => item?.prepared !== true) ? ["non_work_recovery_required"] : []), ...(intent.probes.length ? ["probe_reconciliation_required"] : [])])] };
152
159
  }
153
160
  /** Read filesystem/native evidence before reserving writers. The returned
154
161
  * SQL-only commit is called with registry -> all-RUN barriers held. */
@@ -357,6 +364,11 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
357
364
  if (!replacement || !replacement.kind.endsWith("-daemon") || replacement.id === source.process_id || !processIsAlive(replacement) || replacement.pid !== daemon.pid || replacement.lease_token !== daemon.resource_process?.lease_token || replacement.owner_id !== source.runtime_owner.owner_id || replacement.run_id || replacement.work_id || replacement.project_id !== (source.runtime_owner.project_id ?? null) || metadata?.role !== source.role || metadata?.state_dir !== source.state_dir || metadata?.dd_flow_home !== source.runtime_owner.dd_flow_home || metadata?.adapter_executable !== launch.executable || JSON.stringify(metadata?.budget) !== JSON.stringify(source.runtime_owner.budget) || daemon.daemon_id === source.daemon_id || daemon.daemon_id !== operation.restart_receipt.daemon_id || operation.restart_receipt.already_running === true || JSON.stringify(daemon.config?.runtime_owner) !== JSON.stringify(source.runtime_owner))
358
365
  throw new AppError("runtime_scope_owner_unknown", "Native restart has no fresh bound physical owner", 1);
359
366
  const binding = managedProcessBinding(replacement, resourceHome(context));
367
+ const retired = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]);
368
+ if (replacement.kind !== retired.kind)
369
+ throw new AppError("runtime_scope_owner_unknown", "Native replacement changed its frozen harness", 1);
370
+ const harness = harnessKey(replacement.kind.replace(/-daemon$/, ""));
371
+ const loadMethod = canonicalNativeOperation(harness, "session.resume");
360
372
  if (operation.replacement_binding && JSON.stringify(operation.replacement_binding) !== JSON.stringify(binding))
361
373
  throw new AppError("runtime_scope_owner_unknown", "Native replacement registration changed", 1);
362
374
  operation.status = "restarted";
@@ -393,10 +405,10 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
393
405
  return load;
394
406
  });
395
407
  if (!load.receipt) {
396
- if (dispatchLoad) {
408
+ if (dispatchLoad || loadMethod === "session.inspect") {
397
409
  assertReplacement();
398
410
  const args = launch.args.slice(2).filter((value, index, all) => value !== "--session-id" && all[index - 1] !== "--session-id");
399
- load.receipt = await runHarnessAdapter({ executable: launch.executable, args: ["session", "resume", ...args, "--session-id", session.session_id, "--json"], cwd: launch.cwd, env: nativeLaunchEnvironment(context, launch.env), runtimeOwner: source.runtime_owner, operationId, timeoutMs: 30_000 });
411
+ load.receipt = await runHarnessAdapter({ executable: launch.executable, args: ["session", dispatchLoad ? "resume" : "inspect", ...args, "--session-id", session.session_id, "--json"], harness, cwd: launch.cwd, env: nativeLaunchEnvironment(context, launch.env), runtimeOwner: source.runtime_owner, operationId, timeoutMs: 30_000 });
400
412
  }
401
413
  else {
402
414
  const retained = retainedDaemonOperations(source.state_dir).find(item => item.operation_id === operationId && item.operation === "session.resume" && item.daemon_id === daemon.daemon_id && item.session_id === session.session_id && item.state === "completed");
@@ -406,6 +418,16 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
406
418
  }
407
419
  }
408
420
  assertReplacement();
421
+ if (loadMethod !== "session.inspect") {
422
+ const retained = await retainedDaemonReply(source.state_dir, operationId, { operation: loadMethod, daemon_id: daemon.daemon_id, owner_id: source.runtime_owner.owner_id, session_id: session.session_id });
423
+ assertReplacement();
424
+ if (!retained?.ready) {
425
+ load.status = "receipt_observed";
426
+ db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [JSON.stringify(load), context.now(), operationId]);
427
+ return { ...source, ...operation, sessions, pending: true, reason: retained?.reason ?? (retained ? "native_session_load_settlement_required" : "native_session_load_outcome_unknown"), native_outcome: retained ? "completed" : "unknown", native_result: retained?.outcome, settlement: retained?.settlement };
428
+ }
429
+ load.receipt = retained.value;
430
+ }
409
431
  if (adapterSessionId(load.receipt) !== session.session_id || load.receipt.settled !== true)
410
432
  throw new AppError("runtime_scope_session_unsettled", "Native load did not confirm the same settled Session", 1);
411
433
  load.status = "prepared";
@@ -4,9 +4,10 @@ import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { setTimeout as delay } from "node:timers/promises";
7
- import { AppError } from "../shared/errors.js";
7
+ import { AppError, errorRecord } from "../shared/errors.js";
8
8
  import { getResourceDatabase } from "../storage/database.js";
9
- import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, processIsAlive, processStartedAt, registerManagedProcess, resourceHome } from "./managed-processes.js";
9
+ import { assertManagedProcessLease, confirmManagedProcess, finishManagedProcess, renewManagedProcess, processIsAlive, processStartedAt, registerManagedProcess, resourceHome } from "./managed-processes.js";
10
+ import { managedLeaseMonitor } from "./managed-lease-monitor.js";
10
11
  import { publicRuntimeScopeWorker, requestRuntimeScopeControl, runtimeBudgetStatus, prepareRuntimeScopeFence, parseRuntimeBudget } from "./runtime-budget.js";
11
12
  import { controlRuntimeScopeRuns } from "./runtime-scope-control.js";
12
13
  import { prepareRuntimeScopeResume, recordRuntimeScopeResume } from "./runtime-scope-resume.js";
@@ -158,85 +159,129 @@ export async function serveRuntimeScopeControl(context, scopeId, generation) {
158
159
  return worker;
159
160
  });
160
161
  let heartbeat;
162
+ let lease;
161
163
  // A fence must never be held indefinitely by an observer that cannot obtain
162
164
  // fresh provider evidence. A later operator request continues the retained
163
165
  // budget; this owner records why it stopped without claiming settlement.
164
166
  const observation = recoveryObservationBudget(worker.snapshot);
165
- try {
166
- const record = registerManagedProcess(context, { kind: "scope-control", ownerId: key, operationId: `scope-control:${key}:${token}`, ownerPid: process.pid, metadata: { role: "control", scope_id: scopeId, generation } });
167
- worker.process_id = record.id;
168
- worker.process_lease_token = record.lease_token;
169
- confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
170
- db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
171
- let leaseLost = false;
172
- heartbeat = setInterval(() => { try {
173
- if (!heartbeatManagedProcess(context, { id: record.id, leaseToken: record.lease_token }))
174
- leaseLost = true;
167
+ worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
168
+ let cleanupFailure;
169
+ const result = await (async () => {
170
+ try {
171
+ const record = registerManagedProcess(context, { kind: "scope-control", ownerId: key, operationId: `scope-control:${key}:${token}`, ownerPid: process.pid, metadata: { role: "control", scope_id: scopeId, generation } });
172
+ worker.process_id = record.id;
173
+ worker.process_lease_token = record.lease_token;
174
+ confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
175
+ db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
176
+ lease = managedLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: record.id, leaseToken: record.lease_token }));
177
+ let timerFailure;
178
+ const assertScopeCurrent = () => {
179
+ const latest = runtimeBudgetStatus(context, scopeId);
180
+ if (latest.control?.generation !== generation)
181
+ throw new AppError("runtime_scope_control_superseded", "Scope control was superseded during renewal", 1);
182
+ };
183
+ const ownedContext = { ...context, assertControlOwner: () => {
184
+ lease.assertNoRenewal();
185
+ assertManagedProcessLease(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid });
186
+ assertScopeCurrent();
187
+ } };
188
+ heartbeat = setInterval(() => { lease.background(assertScopeCurrent, error => { timerFailure = error; }); }, 10_000);
189
+ heartbeat.unref();
190
+ for (;;) {
191
+ if (timerFailure)
192
+ throw timerFailure;
193
+ await lease.ensureConfirmed(assertScopeCurrent);
194
+ const status = runtimeBudgetStatus(context, scopeId);
195
+ const release = status.release;
196
+ if (release?.current === true && release.generation === generation) {
197
+ worker.status = "completed";
198
+ worker.snapshot = release;
199
+ break;
200
+ }
201
+ if (status.control?.generation !== generation || status.fence) {
202
+ worker.status = "superseded";
203
+ break;
204
+ }
205
+ observation.observe(status);
206
+ if (observation.exhausted()) {
207
+ const final = runtimeBudgetStatus(context, scopeId);
208
+ const terminal = final.release;
209
+ if (terminal?.current === true && terminal.generation === generation) {
210
+ worker.status = "completed";
211
+ worker.snapshot = terminal;
212
+ break;
213
+ }
214
+ if (final.control?.generation !== generation || final.fence) {
215
+ worker.status = "superseded";
216
+ break;
217
+ }
218
+ worker.status = "failed";
219
+ worker.error = "recovery_observation_budget_exhausted";
220
+ worker.snapshot = {
221
+ ...(worker.snapshot ?? {}),
222
+ status: "recovery_blocked",
223
+ settled: false,
224
+ pending_reasons: ["recovery_observation_budget_exhausted"],
225
+ error: { code: "recovery_observation_budget_exhausted", message: "Scope recovery observation exceeded its 120 second budget" },
226
+ recovery_observation: observation.state(),
227
+ };
228
+ return { scope_id: scopeId, generation, status: "recovery_blocked", settled: false, error: worker.error };
229
+ }
230
+ const resume = status.resume;
231
+ const outcome = await withRecoveryObservation(observation, async () => (worker.reconcile_id ? null : await prepareRuntimeScopeResume(ownedContext, scopeId, generation, resume?.generation === generation ? resume.request_id : undefined))
232
+ ?? await controlRuntimeScopeRuns(ownedContext, scopeId, worker.request_id, worker.mode, worker.reconcile_id));
233
+ await lease.ensureConfirmed(assertScopeCurrent);
234
+ observation.observe(outcome);
235
+ worker.snapshot = { ...outcome, recovery_observation: observation.state() };
236
+ db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
237
+ if (worker.snapshot.drain?.physical_settled === true) {
238
+ worker.status = "completed";
239
+ break;
240
+ }
241
+ await delay(observation.nextDelay());
242
+ }
243
+ return { scope_id: scopeId, generation, status: worker.status };
175
244
  }
176
- catch {
177
- leaseLost = true;
178
- } }, 10_000);
179
- heartbeat.unref();
180
- for (;;) {
181
- if (leaseLost)
182
- throw new AppError("runtime_scope_owner_changed", "Scope worker lost its managed lease", 1);
183
- if (observation.exhausted()) {
184
- worker.status = "failed";
185
- worker.error = "recovery_observation_budget_exhausted";
186
- worker.snapshot = {
187
- ...(worker.snapshot ?? {}),
188
- status: "recovery_blocked",
189
- settled: false,
190
- pending_reasons: ["recovery_observation_budget_exhausted"],
191
- error: { code: "recovery_observation_budget_exhausted", message: "Scope recovery observation exceeded its 120 second budget" },
192
- recovery_observation: observation.state(),
193
- };
194
- return { scope_id: scopeId, generation, status: "recovery_blocked", settled: false, error: worker.error };
245
+ catch (error) {
246
+ if (error instanceof AppError && error.code === "runtime_scope_control_superseded") {
247
+ const release = runtimeBudgetStatus(context, scopeId).release;
248
+ worker.status = release?.current === true && release.generation === generation ? "completed" : "superseded";
249
+ if (worker.status === "completed")
250
+ worker.snapshot = release;
251
+ return { scope_id: scopeId, generation, status: worker.status };
195
252
  }
196
- const status = runtimeBudgetStatus(context, scopeId);
197
- const release = status.release;
198
- if (release?.current === true && release.generation === generation) {
199
- worker.status = "completed";
200
- worker.snapshot = release;
201
- break;
253
+ worker.status = "failed";
254
+ worker.error = String(error);
255
+ worker.snapshot = { ...(worker.snapshot ?? {}), error: errorRecord(error) };
256
+ throw error;
257
+ }
258
+ finally {
259
+ lease?.close();
260
+ if (heartbeat)
261
+ clearInterval(heartbeat);
262
+ observation.exhausted();
263
+ worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
264
+ let persistenceFailure;
265
+ try {
266
+ db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
202
267
  }
203
- if (status.control?.generation !== generation || status.fence) {
204
- worker.status = "superseded";
205
- break;
268
+ catch (error) {
269
+ persistenceFailure = error;
270
+ process.stderr.write(`${JSON.stringify({ primary: worker.error, primary_error: worker.snapshot.error, phase: "persist_scope_owner", secondary: errorRecord(error) })}\n`);
206
271
  }
207
- const resume = status.resume;
208
- const outcome = await withRecoveryObservation(observation, async () => (worker.reconcile_id ? null : await prepareRuntimeScopeResume(context, scopeId, generation, resume?.generation === generation ? resume.request_id : undefined))
209
- ?? await controlRuntimeScopeRuns(context, scopeId, worker.request_id, worker.mode, worker.reconcile_id));
210
- observation.exhausted();
211
- worker.snapshot = { ...outcome, recovery_observation: observation.state() };
212
- db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
213
- if (worker.snapshot.drain?.physical_settled === true) {
214
- worker.status = "completed";
215
- break;
272
+ try {
273
+ if (worker.process_id && worker.process_lease_token)
274
+ finishManagedProcess(context, { id: worker.process_id, leaseToken: worker.process_lease_token, state: worker.status === "failed" ? "failed" : "stopped", reason: "scope_owner_exited" });
216
275
  }
217
- await delay(observation.nextDelay());
218
- }
219
- return { scope_id: scopeId, generation, status: worker.status };
220
- }
221
- catch (error) {
222
- if (error instanceof AppError && error.code === "runtime_scope_control_superseded") {
223
- const release = runtimeBudgetStatus(context, scopeId).release;
224
- worker.status = release?.current === true && release.generation === generation ? "completed" : "superseded";
225
- if (worker.status === "completed")
226
- worker.snapshot = release;
227
- return { scope_id: scopeId, generation, status: worker.status };
276
+ catch (error) {
277
+ cleanupFailure = error;
278
+ process.stderr.write(`${JSON.stringify({ primary: worker.error ?? String(persistenceFailure), primary_error: worker.snapshot.error ?? (persistenceFailure === undefined ? undefined : errorRecord(persistenceFailure)), phase: "finish_scope_owner", secondary: errorRecord(error) })}\n`);
279
+ }
280
+ if (persistenceFailure)
281
+ cleanupFailure = persistenceFailure;
228
282
  }
229
- worker.status = "failed";
230
- worker.error = String(error);
231
- throw error;
232
- }
233
- finally {
234
- if (heartbeat)
235
- clearInterval(heartbeat);
236
- observation.exhausted();
237
- worker.snapshot = { ...(worker.snapshot ?? {}), recovery_observation: observation.state() };
238
- db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
239
- if (worker.process_id && worker.process_lease_token)
240
- finishManagedProcess(context, { id: worker.process_id, leaseToken: worker.process_lease_token, state: worker.status === "failed" ? "failed" : "stopped", reason: "scope_owner_exited" });
241
- }
283
+ })();
284
+ if (cleanupFailure !== undefined && !worker.error)
285
+ throw cleanupFailure;
286
+ return result;
242
287
  }
@@ -2,14 +2,14 @@ import fs from "node:fs";
2
2
  import crypto from "node:crypto";
3
3
  import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
+ import { validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
5
6
  import { assertRunMutationAllowed } from "./run-recovery.js";
6
7
  import { requireProjectByRoot } from "./projects.js";
7
8
  import { resolveRun } from "./runs.js";
8
9
  import { codeExecutionEnvironment, runCheck } from "./code-checks.js";
9
- import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, processTreeIsAlive, registerManagedProcess, reservePorts, resourceHome, stopManagedProcess } from "./managed-processes.js";
10
+ import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessStatus, processTreeIsAlive, registerManagedProcess, reservePorts, resourceHome, stopManagedProcess } from "./managed-processes.js";
10
11
  export function prepareRuntimeService(context, input) {
11
- if (!Number.isFinite(input.timeoutMs) || input.timeoutMs <= 0)
12
- throw new AppError("validation", "Readiness timeout must be positive and finite", 2);
12
+ validateObservationDuration(input.timeoutMs);
13
13
  if (typeof input.command !== "string" || !input.command.trim() || !Array.isArray(input.ports) || !input.ports.length || input.ports.some(name => typeof name !== "string" || !/^[a-z][a-z0-9_]*$/.test(name)) || new Set(input.ports).size !== input.ports.length || !input.ports.includes(input.readyPort) || typeof input.readyPath !== "string" || !input.readyPath.startsWith("/") || input.readyPath.startsWith("//"))
14
14
  throw new AppError("validation", "Declare unique lowercase port names, a ready-port from that list and a local /ready-path", 2);
15
15
  const project = requireProjectByRoot(context, path.resolve(input.projectRoot));
@@ -41,16 +41,15 @@ export async function startRuntimeService(context, input) {
41
41
  assertRunMutationAllowed(context, project.id, run.id, generation);
42
42
  stdout = fs.openSync(String(receipt.stdout_path), "a");
43
43
  stderr = fs.openSync(String(receipt.stderr_path), "a");
44
- const running = runCheck(input.command, run.workspace_root, environment, stdout, stderr, elapsed => input.progress?.(`service ${record.id} alive (${elapsed}s); receipt: ${receiptPath}`), pid => confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), () => heartbeatManagedProcess(context, { id: record.id, leaseToken: record.lease_token }));
44
+ const running = runCheck(input.command, run.workspace_root, environment, stdout, stderr, elapsed => input.progress?.(`service ${record.id} alive (${elapsed}s); receipt: ${receiptPath}`), pid => confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: record.id, leaseToken: record.lease_token }), { inactivityTimeoutMs: input.timeoutMs, phase: () => ready ? "serving" : "startup" });
45
45
  void running.then(() => { settled = true; });
46
- const deadline = performance.now() + input.timeoutMs;
47
- while (!settled && performance.now() < deadline) {
46
+ while (!settled) {
48
47
  try {
49
48
  const response = await fetch(`http://127.0.0.1:${allocation.ports[input.readyPort]}${input.readyPath}`, { signal: AbortSignal.timeout(1_000), redirect: "error" });
50
49
  await response.body?.cancel();
51
50
  ready = response.ok;
52
51
  }
53
- catch { /* Startup is observed until the declared deadline. */ }
52
+ catch { /* Only actual startup output advances the owned command clock. */ }
54
53
  if (ready)
55
54
  break;
56
55
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -1,3 +1,4 @@
1
+ import { errorRecord } from "../shared/errors.js";
1
2
  /** Cleanup is secondary to the arbiter's known commit outcome. Always visit
2
3
  * every connection, including after a failed rollback or close. */
3
4
  export function cleanupScopeWriters(targets) {
@@ -10,7 +11,7 @@ export function cleanupScopeWriters(targets) {
10
11
  }
11
12
  catch (error) {
12
13
  failed = true;
13
- errors.push({ target: target.name, phase: "rollback", message: String(error) });
14
+ errors.push({ target: target.name, phase: "rollback", message: String(error), error: errorRecord(error) });
14
15
  }
15
16
  }
16
17
  if (target.close === "always" || failed) {
@@ -18,7 +19,7 @@ export function cleanupScopeWriters(targets) {
18
19
  target.db.close?.();
19
20
  }
20
21
  catch (error) {
21
- errors.push({ target: target.name, phase: "close", message: String(error) });
22
+ errors.push({ target: target.name, phase: "close", message: String(error), error: errorRecord(error) });
22
23
  }
23
24
  }
24
25
  }
@@ -3,8 +3,134 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { prepareFileBytes, prepareJsonText, prepareOutputFile } from "../cli/input-preparation.js";
6
+ export function runContextRelocations(context, projectId, runId) {
7
+ return context.db.all("SELECT id, payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.publication_relocation' ORDER BY id", [projectId]).flatMap(row => {
8
+ const receipt = JSON.parse(row.payload_json);
9
+ if (receipt.run_id !== runId || !receipt.path_mapping)
10
+ return [];
11
+ return [{ receipt_id: `${row.id}:${receipt.import_id}`, source_manifest_sha256: receipt.source_manifest_sha256, path_mapping: receipt.path_mapping }];
12
+ });
13
+ }
14
+ function assertContextRelocation(receipt) {
15
+ if (!receipt.receipt_id || !/^[a-f0-9]{64}$/.test(receipt.source_manifest_sha256)
16
+ || !Array.isArray(receipt.path_mapping) || receipt.path_mapping.some(item => !item || typeof item.from !== "string" || typeof item.to !== "string" || !path.isAbsolute(item.from) || !path.isAbsolute(item.to)))
17
+ throw new AppError("context_relocation_invalid", "Stage context relocation lacks a verified import binding", 2);
18
+ }
19
+ /** Physical packet locators may move; accepted decision/provenance blocks may not. */
20
+ export function projectPacketLocators(text, receipts) {
21
+ receipts.forEach(assertContextRelocation);
22
+ return text.split(/(<run_operational_contract>[\s\S]*?<\/run_operational_contract>|<external_stage_context>[\s\S]*?<\/source_precedence>|DD_FLOW_(?:INVOCATION_SCOPE|CURRENT_INVOCATION|DAEMON_ID)=(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s'"])+)/g).map(part => {
23
+ if (part.startsWith("<run_operational_contract>") || part.startsWith("<external_stage_context>") || part.startsWith("DD_FLOW_INVOCATION_SCOPE=") || part.startsWith("DD_FLOW_CURRENT_INVOCATION=") || part.startsWith("DD_FLOW_DAEMON_ID="))
24
+ return part;
25
+ for (const receipt of receipts) {
26
+ const mappings = [...receipt.path_mapping].sort((a, b) => b.from.length - a.from.length);
27
+ if (!mappings.length)
28
+ continue;
29
+ const prefixes = mappings.map(item => item.from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
30
+ // One longest-prefix replacement per receipt: a new root may itself sit
31
+ // below an old prefix and must not be mapped twice by that receipt.
32
+ const locator = new RegExp(`(^|[^A-Za-z0-9_./~:\\\\-])(${prefixes})(?=[/\\\\]|$|[\\s"'\x60<>{}\\[\\](),;!?])`, "g");
33
+ part = part.replace(locator, (_match, boundary, prefix) => `${boundary}${mappings.find(item => item.from === prefix).to}`);
34
+ }
35
+ return part;
36
+ }).join("");
37
+ }
38
+ /** Current delivery packet is derived; imported prompt/context bytes remain provenance. */
39
+ export function currentStagePrompt(context, promptPath, runHome = path.dirname(path.dirname(promptPath)), sourceText) {
40
+ let original = sourceText ?? fs.readFileSync(promptPath, "utf8");
41
+ let sourcePromptPath = promptPath;
42
+ if (original.startsWith("<current_prompt_projection>\n")) {
43
+ const prior = JSON.parse(original.split("\n")[1]);
44
+ const source = path.resolve(runHome, prior.source_prompt);
45
+ const relative = path.relative(runHome, source);
46
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || !fs.existsSync(source))
47
+ throw new AppError("context_checksum_mismatch", "Current packet has no retained source prompt", 2);
48
+ original = fs.readFileSync(source, "utf8");
49
+ if (sha256(original) !== prior.original_prompt_sha256)
50
+ throw new AppError("context_checksum_mismatch", "Retained source prompt changed", 2);
51
+ sourcePromptPath = source;
52
+ }
53
+ const row = context.db.get("SELECT id, project_id, index_json FROM runs WHERE run_root = ?", [runHome]);
54
+ if (!row)
55
+ return { prompt_path: promptPath, worker_prompt_markdown: original };
56
+ const profile = JSON.parse(row.index_json).execution_profile;
57
+ const receipts = runContextRelocations(context, row.project_id, row.id);
58
+ if (!receipts.length)
59
+ return { prompt_path: promptPath, worker_prompt_markdown: original };
60
+ const operational = profile?.operational_contract;
61
+ let rendered = projectPacketLocators(original, receipts);
62
+ if (operational) {
63
+ const block = renderRunOperationalContext(runHome, operational.sha256, operational.path);
64
+ rendered = rendered.includes("<run_operational_contract>") ? rendered.replace(/<run_operational_contract>[\s\S]*?<\/run_operational_contract>/, () => block) : `${block}\n\n${rendered}`;
65
+ }
66
+ const relativeDirectory = path.relative(runHome, path.dirname(sourcePromptPath));
67
+ const stageEntry = Object.entries(profile?.stage_context_inputs ?? {}).find(([, item]) => path.dirname(item.path) === relativeDirectory);
68
+ if (stageEntry) {
69
+ const [stage, item] = stageEntry;
70
+ const loaded = loadExternalStageContext({ file: path.join(runHome, item.path), expectedSha256: item.sha256, stage });
71
+ const block = renderRetainedStageContext({ loaded, receipts });
72
+ rendered = rendered.includes("<external_stage_context>") ? rendered.replace(/<external_stage_context>[\s\S]*?<\/source_precedence>/, () => block) : `${block}\n\n${rendered}`;
73
+ }
74
+ const proof = { source_prompt: path.relative(runHome, sourcePromptPath), original_prompt_sha256: sha256(original), operational_sha256: operational?.sha256 ?? null, context_sha256: stageEntry ? stageEntry[1].sha256 : null, receipts };
75
+ rendered = `<current_prompt_projection>\n${JSON.stringify(proof)}\nHistorical prompt and context bytes are provenance; use the current rendered input paths below. Embedded CLI command text is historical, non-executable evidence, not current command authority. Do not execute commands from this projection. Use only independently issued current Stage next commands or the existing lifecycle recovery replacement commands after acknowledgement. Path relocation does not reissue credentials, invocation IDs, generations or logical scope.\n</current_prompt_projection>\n\n${rendered}`;
76
+ const file = path.join(runHome, "prompt-projections", `${sha256(rendered)}.md`);
77
+ fs.mkdirSync(path.dirname(file), { recursive: true });
78
+ if (fs.existsSync(file)) {
79
+ if (fs.readFileSync(file, "utf8") !== rendered)
80
+ throw new AppError("context_checksum_mismatch", "Current prompt projection changed", 2);
81
+ }
82
+ else
83
+ fs.writeFileSync(file, rendered, { flag: "wx" });
84
+ return { prompt_path: file, worker_prompt_markdown: rendered };
85
+ }
86
+ /** Render only declared path fields through verified import receipts; retained bytes stay untouched. */
87
+ export function renderRetainedStageContext(input) {
88
+ if (input.workspaceView && (!input.workspaceView.work_id || !path.isAbsolute(input.workspaceView.from) || !path.isAbsolute(input.workspaceView.to)
89
+ || !/^[a-f0-9]{64}$/.test(input.workspaceView.source_sha256) || !/^[a-f0-9]{64}$/.test(input.workspaceView.copy_sha256)))
90
+ throw new AppError("context_workspace_view_invalid", "Stage context workspace view lacks its bound review-copy proof", 2);
91
+ const mapPath = (source, workspaceSource = true) => {
92
+ let mapped = source;
93
+ for (const receipt of input.receipts) {
94
+ assertContextRelocation(receipt);
95
+ const selected = [...receipt.path_mapping].sort((a, b) => b.from.length - a.from.length).find(item => mapped === item.from || mapped.startsWith(`${item.from}${path.sep}`));
96
+ if (selected)
97
+ mapped = path.join(selected.to, path.relative(selected.from, mapped));
98
+ }
99
+ const view = input.workspaceView;
100
+ if (view && workspaceSource && (mapped === view.from || mapped.startsWith(`${view.from}${path.sep}`)))
101
+ mapped = path.join(view.to, path.relative(view.from, mapped));
102
+ return mapped;
103
+ };
104
+ const value = input.loaded.value;
105
+ const mappedValue = { ...value,
106
+ ...(value.task_input ? { task_input: value.task_input.map(item => ({ ...item, path: mapPath(item.path) })) } : {}),
107
+ ...(value.sources ? { sources: value.sources.map(item => ({ ...item, path: mapPath(item.path, item.root !== "run") })) } : {}) };
108
+ return [input.receipts.length ? `<context_path_mapping>\nOriginal context SHA-256: ${input.loaded.materializedSha256}\nVerified import receipts: ${JSON.stringify(input.receipts)}\nOnly rendered physical path fields are relocated; original decisions and source bytes are unchanged.\n</context_path_mapping>` : "",
109
+ input.workspaceView ? `<review_workspace_view>\nBound review copy: ${JSON.stringify(input.workspaceView)}\nOnly project/workspace input paths use this read-only view. Original authority and RUN artifacts stay in their retained locations.\n</review_workspace_view>` : "",
110
+ renderExternalStageContext({ loaded: { ...input.loaded, value: mappedValue }, installed: { path: input.loaded.sourcePath } })].filter(Boolean).join("\n\n");
111
+ }
6
112
  /** One precedence rule for coordinator and delegated Work packets. */
7
113
  export const acceptedDecisionPrecedence = "Read the accepted RUN decisions before applying older general project rules. An accepted, explicitly scoped user decision supersedes a conflicting general rule only within that scope; preserve the rule elsewhere. Do not ask the user again about a decision already settled in those artifacts or invent a broader exception.";
114
+ /** Shared Stage/Work projection; historical RUNs do not gain a new grant. */
115
+ export function renderRunOperationalContext(runHome, expectedSha256, artifactPath) {
116
+ const indexFile = path.join(runHome, "run.json");
117
+ const index = fs.existsSync(indexFile) ? JSON.parse(fs.readFileSync(indexFile, "utf8")) : null;
118
+ const metadata = index?.execution_profile?.operational_contract;
119
+ const file = path.join(runHome, artifactPath ?? metadata?.path ?? "operational-contract.json");
120
+ const binding = expectedSha256 ?? metadata?.sha256;
121
+ if (!binding)
122
+ return "";
123
+ if (!fs.existsSync(file) || sha256(fs.readFileSync(file)) !== binding)
124
+ throw new AppError("operational_contract_changed", "Frozen RUN operational input changed or is missing", 2, { file });
125
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
126
+ if (value.policy_source && (!fs.existsSync(path.join(runHome, value.policy_source.path)) || sha256(fs.readFileSync(path.join(runHome, value.policy_source.path))) !== value.policy_source.sha256))
127
+ throw new AppError("operational_contract_changed", "Retained policy source changed", 2);
128
+ return ["<run_operational_contract>", `- immutable artifact: ${file}`, `- sha256: ${binding}`, `- logical scope: ${JSON.stringify(value.binding)}`,
129
+ `- delivery: ${value.settings.merge_delivery.strategy}; cleanup: ${value.settings.merge_cleanup.source}`,
130
+ ...(value.decision ? [`- accepted decision ${value.decision.id}: ${JSON.stringify(value.decision)}`, `- retained policy source: ${value.policy_source ? path.join(runHome, value.policy_source.path) : path.resolve(index?.workspace?.project_root ?? value.binding.project_root, value.decision.policy.path)} (original locators above are provenance only)`, acceptedDecisionPrecedence] : ["No scoped override is accepted. Preserve project policy; frozen defaults do not grant an exception."]),
131
+ "Local completion means proven integration into the declared target commit, not remote publication, deployment, release, or a feature-only handoff. Source retention applies only to this RUN's source. Preserve dirty/unmerged, branch-protection and immutable-tag guards.",
132
+ "A retention review date requests review; it is not an automatic expiry or deletion instruction.", "</run_operational_contract>"].join("\n");
133
+ }
8
134
  export function loadExternalStageContext(input) {
9
135
  const { file: sourcePath, bytes } = prepareFileBytes(input.file, "Stage context");
10
136
  const materializedSha256 = sha256(bytes);
@@ -52,6 +178,15 @@ export function installExternalStageContext(input) {
52
178
  /** Install the runner slice and make it visible in the agent packet before a
53
179
  * caller performs its lifecycle transition. */
54
180
  export function applyExternalStageContext(input) {
181
+ const home = path.dirname(input.stageRoot);
182
+ const row = input.context?.db.get("SELECT index_json FROM runs WHERE run_root = ?", [home]);
183
+ const authoritative = row ? JSON.parse(row.index_json).execution_profile?.operational_contract : undefined;
184
+ const operational = input.context ? authoritative ? renderRunOperationalContext(home, authoritative.sha256, authoritative.path) : "" : renderRunOperationalContext(home);
185
+ if (operational && fs.existsSync(input.promptPath)) {
186
+ const current = fs.readFileSync(input.promptPath, "utf8");
187
+ if (!current.includes("<run_operational_contract>"))
188
+ fs.writeFileSync(input.promptPath, `${operational}\n\n${current}`);
189
+ }
55
190
  if (!input.loaded)
56
191
  return undefined;
57
192
  const installed = installExternalStageContext({ stageRoot: input.stageRoot, loaded: input.loaded });
@@ -8,7 +8,7 @@ import { persistProtocolState, requireProtocol, readProtocolRuntimeState } from
8
8
  import { attachFlowRunStage, prepareFlowRunStart, prepareFlowRunStageAttachment, prepareStageDirectory, completeFlowRunStage, getFlowRunStatus, startFlowRun } from "./runs.js";
9
9
  import { validateSchema } from "./schema-validation.js";
10
10
  import { reconcileSessionCoverage, registerFlowSession } from "./sessions.js";
11
- import { recalculateRunUsage, usageForRun } from "./usage.js";
11
+ import { usageForRun } from "./usage.js";
12
12
  import { boundCanonicalPlan, prepareCanonicalPlan } from "./plan-runtime.js";
13
13
  import { prepareJsonFile, prepareJsonSchema, prepareOutputDirectory, prepareOutputFile, prepareTextFile } from "../cli/input-preparation.js";
14
14
  import { AppError } from "../shared/errors.js";
@@ -20,16 +20,6 @@ import { registerProtocol } from "./protocols.js";
20
20
  import { previewNextEntityId } from "./ids.js";
21
21
  import { hookSessionIdentity, sessionIdForHookEvent } from "./hooks.js";
22
22
  import { assertRunMutationAllowed } from "./run-recovery.js";
23
- function stageUsageProjection(context, view, stage, startedAt) {
24
- if (!startedAt)
25
- return { status: "unavailable", input_tokens: null, cached_input_tokens: null, output_tokens: null, total_tokens: null, session_count: 0, observed_at: context.now() };
26
- const value = recalculateRunUsage(context, { projectId: view.run.project_id, runId: view.run.id, stage: { name: stage, startedAt, completedAt: context.now() } });
27
- const totals = value.totals, coverage = value.coverage;
28
- const count = (name) => typeof coverage?.[name] === "number" ? coverage[name] : 0;
29
- const measured = count("measured"), other = Object.values(coverage ?? {}).reduce((total, item) => total + (typeof item === "number" ? item : 0), 0) - measured;
30
- const tokens = (name) => typeof totals?.[name] === "number" ? totals[name] : null;
31
- return { status: measured === 0 ? "unavailable" : other > 0 || ["input_tokens", "cache_read_input_tokens", "output_tokens", "total_tokens"].some(name => tokens(name) === null) ? "partial" : "measured", input_tokens: tokens("input_tokens"), cached_input_tokens: tokens("cache_read_input_tokens"), output_tokens: tokens("output_tokens"), total_tokens: tokens("total_tokens"), session_count: Array.isArray(value.sessions) ? value.sessions.length : 0, observed_at: typeof value.observed_at === "string" ? value.observed_at : context.now() };
32
- }
33
23
  const stagePromptSections = [
34
24
  "stage_identity",
35
25
  "authoritative_runtime_facts",
@@ -294,7 +284,7 @@ export function prepareStageFinish(context, input) {
294
284
  return { projectRoot, runId, view, existing, runHome, stageRoot, semanticFile, semantic, status, source, planFinish };
295
285
  }
296
286
  export function finishStage(context, input, prepared = prepareStageFinish(context, input)) {
297
- const { projectRoot, runId, view, existing, runHome, stageRoot, semanticFile, semantic, status, source, planFinish } = prepared;
287
+ const { projectRoot, runId, view, runHome, stageRoot, semanticFile, semantic, status, source, planFinish } = prepared;
298
288
  assertRunMutationAllowed(context, view.run.project_id, view.run.id);
299
289
  fs.mkdirSync(stageRoot, { recursive: true });
300
290
  const receipt = beginFinishReceipt(context, stageRoot, semanticFile, view.run.id, input.stage, source.text);
@@ -306,7 +296,6 @@ export function finishStage(context, input, prepared = prepareStageFinish(contex
306
296
  const lint = runTargetedMemoryBankLint(projectRoot);
307
297
  if (planFinish)
308
298
  boundCanonicalPlan(context, { projectId: view.run.project_id, protocolId: view.run.subject.id, planPath: planFinish.plan_path }, planFinish.canonical);
309
- const stageUsage = stageUsageProjection(context, view, input.stage, existing.started_at);
310
299
  const report = buildStageReport(context, view, input.stage, status, semantic, stageRoot, lint, planFinish);
311
300
  const dataPath = path.join(stageRoot, "stage-report.json");
312
301
  const reportPath = path.join(stageRoot, "stage-report.md");
@@ -327,8 +316,7 @@ export function finishStage(context, input, prepared = prepareStageFinish(contex
327
316
  stageReport: htmlPath,
328
317
  data: dataPath,
329
318
  dataSchemaId: String(report.schema_id),
330
- report: reportPath,
331
- usage: stageUsage
319
+ report: reportPath
332
320
  });
333
321
  finishReceipt(receipt, { outcome: "accepted", result: { status, report: dataPath } });
334
322
  appendAudit(context, {