pi-daddy 0.43.1 → 0.44.0

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 (221) hide show
  1. package/CHANGELOG.md +93 -31
  2. package/README.md +15 -0
  3. package/THIRD_PARTY_LICENSES/musl-COPYRIGHT.txt +193 -0
  4. package/THIRD_PARTY_LICENSES/zig-MIT.txt +21 -0
  5. package/THIRD_PARTY_NOTICES.md +9 -0
  6. package/dist/cli.d.ts +2 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +10 -5
  9. package/dist/cli.js.map +1 -1
  10. package/dist/executors/activity-session.js +116 -65
  11. package/dist/executors/activity-session.js.map +1 -1
  12. package/dist/executors/captured-execution.d.ts +13 -0
  13. package/dist/executors/captured-execution.d.ts.map +1 -0
  14. package/dist/executors/captured-execution.js +53 -0
  15. package/dist/executors/captured-execution.js.map +1 -0
  16. package/dist/executors/child-final.d.ts +31 -0
  17. package/dist/executors/child-final.d.ts.map +1 -0
  18. package/dist/executors/child-final.js +197 -0
  19. package/dist/executors/child-final.js.map +1 -0
  20. package/dist/executors/executor.d.ts +2 -1
  21. package/dist/executors/executor.d.ts.map +1 -1
  22. package/dist/executors/executor.js +12 -0
  23. package/dist/executors/executor.js.map +1 -1
  24. package/dist/executors/owned-worker.d.ts +18 -0
  25. package/dist/executors/owned-worker.d.ts.map +1 -0
  26. package/dist/executors/owned-worker.js +295 -0
  27. package/dist/executors/owned-worker.js.map +1 -0
  28. package/dist/executors/worker-artifact.d.ts +3 -0
  29. package/dist/executors/worker-artifact.d.ts.map +1 -0
  30. package/dist/executors/worker-artifact.js +3 -0
  31. package/dist/executors/worker-artifact.js.map +1 -0
  32. package/dist/governance/approval-prompt.d.ts +4 -3
  33. package/dist/governance/approval-prompt.d.ts.map +1 -1
  34. package/dist/governance/approval-prompt.js +13 -6
  35. package/dist/governance/approval-prompt.js.map +1 -1
  36. package/dist/governance/captured-worker-record.d.ts +9 -0
  37. package/dist/governance/captured-worker-record.d.ts.map +1 -0
  38. package/dist/governance/captured-worker-record.js +123 -0
  39. package/dist/governance/captured-worker-record.js.map +1 -0
  40. package/dist/governance/lease-record.d.ts +4 -0
  41. package/dist/governance/lease-record.d.ts.map +1 -1
  42. package/dist/governance/lease-record.js +19 -2
  43. package/dist/governance/lease-record.js.map +1 -1
  44. package/dist/governance/ledger-events.d.ts +5 -5
  45. package/dist/governance/ledger-events.d.ts.map +1 -1
  46. package/dist/governance/ledger-v3-validation.js +1 -1
  47. package/dist/governance/ledger-v3-validation.js.map +1 -1
  48. package/dist/governance/workspace-acceptance.d.ts +3 -1
  49. package/dist/governance/workspace-acceptance.d.ts.map +1 -1
  50. package/dist/governance/workspace-acceptance.js +21 -5
  51. package/dist/governance/workspace-acceptance.js.map +1 -1
  52. package/dist/governance/workspace-lease.d.ts.map +1 -1
  53. package/dist/governance/workspace-lease.js +44 -1
  54. package/dist/governance/workspace-lease.js.map +1 -1
  55. package/dist/kernel/authority-text.d.ts +3 -0
  56. package/dist/kernel/authority-text.d.ts.map +1 -0
  57. package/dist/kernel/authority-text.js +22 -0
  58. package/dist/kernel/authority-text.js.map +1 -0
  59. package/dist/kernel/bounded-read-failures.d.ts +9 -0
  60. package/dist/kernel/bounded-read-failures.d.ts.map +1 -0
  61. package/dist/kernel/bounded-read-failures.js +33 -0
  62. package/dist/kernel/bounded-read-failures.js.map +1 -0
  63. package/dist/kernel/bounded-read-sync.d.ts +24 -0
  64. package/dist/kernel/bounded-read-sync.d.ts.map +1 -0
  65. package/dist/kernel/bounded-read-sync.js +75 -0
  66. package/dist/kernel/bounded-read-sync.js.map +1 -0
  67. package/dist/kernel/bounded-read.d.ts +29 -2
  68. package/dist/kernel/bounded-read.d.ts.map +1 -1
  69. package/dist/kernel/bounded-read.js +98 -9
  70. package/dist/kernel/bounded-read.js.map +1 -1
  71. package/dist/kernel/capacity.d.ts +36 -0
  72. package/dist/kernel/capacity.d.ts.map +1 -0
  73. package/dist/kernel/capacity.js +94 -0
  74. package/dist/kernel/capacity.js.map +1 -0
  75. package/dist/kernel/captured-worker-contract.d.ts +38 -0
  76. package/dist/kernel/captured-worker-contract.d.ts.map +1 -0
  77. package/dist/kernel/captured-worker-contract.js +2 -0
  78. package/dist/kernel/captured-worker-contract.js.map +1 -0
  79. package/dist/kernel/catalog.d.ts +12 -3
  80. package/dist/kernel/catalog.d.ts.map +1 -1
  81. package/dist/kernel/catalog.js +28 -8
  82. package/dist/kernel/catalog.js.map +1 -1
  83. package/dist/kernel/chain.d.ts +1 -0
  84. package/dist/kernel/chain.d.ts.map +1 -1
  85. package/dist/kernel/chain.js.map +1 -1
  86. package/dist/kernel/definitions.d.ts +11 -1
  87. package/dist/kernel/definitions.d.ts.map +1 -1
  88. package/dist/kernel/definitions.js +6 -3
  89. package/dist/kernel/definitions.js.map +1 -1
  90. package/dist/kernel/fanout.d.ts +5 -25
  91. package/dist/kernel/fanout.d.ts.map +1 -1
  92. package/dist/kernel/fanout.js +15 -30
  93. package/dist/kernel/fanout.js.map +1 -1
  94. package/dist/kernel/grant-env.d.ts +0 -2
  95. package/dist/kernel/grant-env.d.ts.map +1 -1
  96. package/dist/kernel/grant-env.js +0 -1
  97. package/dist/kernel/grant-env.js.map +1 -1
  98. package/dist/kernel/model-preflight.d.ts +8 -0
  99. package/dist/kernel/model-preflight.d.ts.map +1 -1
  100. package/dist/kernel/model-preflight.js +23 -0
  101. package/dist/kernel/model-preflight.js.map +1 -1
  102. package/dist/kernel/selected-definitions.d.ts +15 -0
  103. package/dist/kernel/selected-definitions.d.ts.map +1 -0
  104. package/dist/kernel/selected-definitions.js +146 -0
  105. package/dist/kernel/selected-definitions.js.map +1 -0
  106. package/dist/kernel/skill-packages.d.ts +4 -2
  107. package/dist/kernel/skill-packages.d.ts.map +1 -1
  108. package/dist/kernel/skill-packages.js +34 -41
  109. package/dist/kernel/skill-packages.js.map +1 -1
  110. package/dist/kernel/skill-resources.d.ts.map +1 -1
  111. package/dist/kernel/skill-resources.js +8 -11
  112. package/dist/kernel/skill-resources.js.map +1 -1
  113. package/dist/kernel/workspace.d.ts +6 -3
  114. package/dist/kernel/workspace.d.ts.map +1 -1
  115. package/dist/kernel/workspace.js +10 -6
  116. package/dist/kernel/workspace.js.map +1 -1
  117. package/dist/products/dashboard-cli.js +1 -1
  118. package/dist/products/dashboard-cli.js.map +1 -1
  119. package/dist/products/dashboard-render.d.ts +1 -2
  120. package/dist/products/dashboard-render.d.ts.map +1 -1
  121. package/dist/products/dashboard-render.js +3 -1
  122. package/dist/products/dashboard-render.js.map +1 -1
  123. package/dist/products/dashboard-session-client.d.ts +1 -2
  124. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  125. package/dist/products/dashboard-session-client.js.map +1 -1
  126. package/dist/products/episode-report.d.ts +12 -8
  127. package/dist/products/episode-report.d.ts.map +1 -1
  128. package/dist/products/episode-report.js +61 -12
  129. package/dist/products/episode-report.js.map +1 -1
  130. package/extensions/approvals.ts +1 -1
  131. package/extensions/chain-plan.ts +9 -3
  132. package/extensions/context-staging.ts +30 -35
  133. package/extensions/dashboard-session-server.ts +1 -2
  134. package/extensions/definition-describe.ts +44 -0
  135. package/extensions/definition-runtime.ts +89 -26
  136. package/extensions/delegate-chain.ts +273 -256
  137. package/extensions/delegation-activation.ts +1 -1
  138. package/extensions/delegation.ts +41 -53
  139. package/extensions/execute-child.ts +167 -134
  140. package/extensions/fanout-outcome.ts +1 -9
  141. package/extensions/grants-command.ts +2 -9
  142. package/extensions/grants.ts +106 -38
  143. package/extensions/init-command.ts +52 -12
  144. package/extensions/reload-environment.ts +21 -2
  145. package/extensions/retired-inputs.ts +7 -0
  146. package/extensions/run-delegation.ts +212 -265
  147. package/extensions/session-capacity.ts +53 -0
  148. package/extensions/session-environment.ts +5 -2
  149. package/extensions/session-model-prompt.ts +33 -32
  150. package/extensions/session-report.ts +28 -11
  151. package/extensions/session.ts +200 -157
  152. package/extensions/workspace-runtime.ts +2 -1
  153. package/native/README.md +25 -0
  154. package/native/linux-x64/worker +0 -0
  155. package/native/linux-x64/worker.sha256 +1 -0
  156. package/native/provenance.json +42 -0
  157. package/native/toolchain.json +18 -0
  158. package/native/worker.c +209 -0
  159. package/package.json +15 -7
  160. package/src/cli.ts +16 -6
  161. package/src/executors/activity-session.ts +120 -66
  162. package/src/executors/captured-execution.ts +61 -0
  163. package/src/executors/child-final.ts +190 -0
  164. package/src/executors/executor.ts +14 -1
  165. package/src/executors/owned-worker.ts +322 -0
  166. package/src/executors/worker-artifact.ts +2 -0
  167. package/src/governance/approval-prompt.ts +18 -8
  168. package/src/governance/captured-worker-record.ts +138 -0
  169. package/src/governance/lease-record.ts +21 -2
  170. package/src/governance/ledger-events.ts +5 -5
  171. package/src/governance/ledger-v3-validation.ts +1 -1
  172. package/src/governance/workspace-acceptance.ts +20 -4
  173. package/src/governance/workspace-lease.ts +59 -1
  174. package/src/kernel/authority-text.ts +17 -0
  175. package/src/kernel/bounded-read-failures.ts +42 -0
  176. package/src/kernel/bounded-read-sync.ts +87 -0
  177. package/src/kernel/bounded-read.ts +128 -11
  178. package/src/kernel/capacity.ts +123 -0
  179. package/src/kernel/captured-worker-contract.ts +29 -0
  180. package/src/kernel/catalog.ts +45 -16
  181. package/src/kernel/chain.ts +1 -0
  182. package/src/kernel/definitions.ts +11 -2
  183. package/src/kernel/fanout.ts +17 -32
  184. package/src/kernel/grant-env.ts +0 -3
  185. package/src/kernel/model-preflight.ts +29 -0
  186. package/src/kernel/selected-definitions.ts +156 -0
  187. package/src/kernel/skill-packages.ts +40 -38
  188. package/src/kernel/skill-resources.ts +8 -9
  189. package/src/kernel/workspace.ts +12 -5
  190. package/src/products/dashboard-cli.ts +1 -1
  191. package/src/products/dashboard-render.ts +6 -2
  192. package/src/products/dashboard-session-client.ts +1 -2
  193. package/src/products/episode-report.ts +80 -17
  194. package/dist/advisors/advisor.d.ts +0 -90
  195. package/dist/advisors/advisor.d.ts.map +0 -1
  196. package/dist/advisors/advisor.js +0 -154
  197. package/dist/advisors/advisor.js.map +0 -1
  198. package/dist/advisors/decider.d.ts +0 -88
  199. package/dist/advisors/decider.d.ts.map +0 -1
  200. package/dist/advisors/decider.js +0 -29
  201. package/dist/advisors/decider.js.map +0 -1
  202. package/dist/advisors/jev.d.ts +0 -41
  203. package/dist/advisors/jev.d.ts.map +0 -1
  204. package/dist/advisors/jev.js +0 -139
  205. package/dist/advisors/jev.js.map +0 -1
  206. package/dist/advisors/settings.d.ts +0 -49
  207. package/dist/advisors/settings.d.ts.map +0 -1
  208. package/dist/advisors/settings.js +0 -99
  209. package/dist/advisors/settings.js.map +0 -1
  210. package/dist/governance/episode-cost-gate.d.ts +0 -31
  211. package/dist/governance/episode-cost-gate.d.ts.map +0 -1
  212. package/dist/governance/episode-cost-gate.js +0 -112
  213. package/dist/governance/episode-cost-gate.js.map +0 -1
  214. package/extensions/advisor-session.ts +0 -67
  215. package/extensions/effort-advice.ts +0 -112
  216. package/extensions/pruning-advice.ts +0 -133
  217. package/src/advisors/advisor.ts +0 -246
  218. package/src/advisors/decider.ts +0 -74
  219. package/src/advisors/jev.ts +0 -163
  220. package/src/advisors/settings.ts +0 -115
  221. package/src/governance/episode-cost-gate.ts +0 -128
@@ -14,14 +14,19 @@
14
14
  * copy of `ownGrant` before observation is exactly how a stale upper bound would become an enforced one.
15
15
  */
16
16
  import { randomUUID } from "node:crypto";
17
+ import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
18
+ import type { discoverSkillPackages } from "../src/kernel/skill-packages.ts";
19
+ import { collectBoundedReadFailures } from "../src/kernel/bounded-read-failures.ts";
17
20
  import { isEpisodeId, newEpisodeId } from "../src/kernel/episode-id.ts";
18
21
  import { parseInherited, type InheritableApproval } from "../src/kernel/approval.ts";
19
22
  import type { ApprovalBinding } from "../src/kernel/correlation.ts";
20
23
  import { createApprovalGateProvider } from "../src/governance/approval-prompt.ts";
21
- import { makeCatalog, skillPathsFromCatalog, type Catalog } from "../src/kernel/catalog.ts";
24
+ import { makeCatalog, skillPathsFromCatalog, type CatalogLoaders, type Catalog } from "../src/kernel/catalog.ts";
22
25
  import type { SkillDefinition } from "../src/kernel/definitions.ts";
23
26
  import { DELEGATE_CAPABILITY, type DelegationContext } from "../src/kernel/delegate.ts";
24
- import { budgetFromEnv } from "../src/kernel/fanout.ts";
27
+ import { capacityForLifecycle } from "./session-capacity.ts";
28
+ import type { CapacityAllocator } from "../src/kernel/capacity.ts";
29
+ import type { StructuredRefusal } from "../src/kernel/refusals.ts";
25
30
  import { chooseExecutor, ENV_HERDR, type ExecutorChoice } from "../src/executors/executor.ts";
26
31
  import { WILDCARD } from "../src/kernel/pi-tools.ts";
27
32
  import {
@@ -42,6 +47,7 @@ import {
42
47
  parseList,
43
48
  } from "../src/kernel/propagation.ts";
44
49
  import type { Capability } from "../src/kernel/resolve.ts";
50
+ import { selectedDefinitions, type SelectedCommand } from "../src/kernel/selected-definitions.ts";
45
51
  import { loadDefinitions } from "../src/kernel/definitions.ts";
46
52
  import { buildCatalog } from "../src/kernel/catalog.ts";
47
53
  import { ENV_WORKSPACE_REGISTRY } from "../src/kernel/workspace.ts";
@@ -50,9 +56,9 @@ import { republishable } from "./approvals.ts";
50
56
  import { storedGrantSessionState } from "./stored-grant-session.ts";
51
57
  import { nativeSessionRootFromEnv, type NativeSessionHost } from "../src/executors/native-session-target.ts";
52
58
  import { createHandoffStager, type ParentSession } from "./context-staging.ts";
53
- import { createAdvisorSession, type AdvisorSession } from "./advisor-session.ts";
54
59
  import { join } from "node:path";
55
60
  import { readFileSync, statSync } from "node:fs";
61
+ import { readBoundedTextSync } from "../src/kernel/bounded-read-sync.ts";
56
62
  import { agentDir, projectSettingsPath } from "../src/kernel/project-paths.ts";
57
63
  import { ENV_ALLOW_UNRESOLVED_MODELS } from "../src/kernel/model-preflight.ts";
58
64
  import { beginExtensionLifecycle, rememberChildPublication, type ReloadLifecycle } from "./reload-environment.ts";
@@ -67,7 +73,6 @@ import {
67
73
  } from "../src/kernel/workspace-pin.ts";
68
74
  import { loadWorkspaceRegistry } from "../src/kernel/workspace.ts";
69
75
  import { reconcileAcceptedWorkspaces } from "../src/governance/workspace-acceptance.ts";
70
- import { EpisodeCostGate, episodeCostCeilingFromSettings } from "../src/governance/episode-cost-gate.ts";
71
76
  import {
72
77
  definitionRuntimeSettingsFrom,
73
78
  parseConfiguredModel,
@@ -76,6 +81,13 @@ import {
76
81
  type DefinitionRuntimeSettings,
77
82
  } from "./definition-runtime.ts";
78
83
 
84
+ /** Trusted composition dependencies, not inherited or requester-supplied authority. */
85
+ export interface SessionDiscovery extends CatalogLoaders {
86
+ packages?: typeof discoverSkillPackages;
87
+ acceptanceRead?: (path: string) => Promise<string>;
88
+ canonicalise?: (path: string) => Promise<string>;
89
+ }
90
+
79
91
  /**
80
92
  * Run governed children in herdr panes instead of captured child processes.
81
93
  *
@@ -109,13 +121,7 @@ import {
109
121
  ENV_ACTIVITY_ROOT,
110
122
  ENV_ACTIVITY_TASK,
111
123
  } from "../src/products/activity-timeline.ts";
112
- import {
113
- ENV_HERDR_KEEP_PANE,
114
- ENV_GOVERNANCE,
115
- ENV_ADVISOR,
116
- ENV_EPISODE_ID,
117
- ENV_EPISODE_COST_CEILING,
118
- } from "../src/kernel/env-names.ts";
124
+ import { ENV_HERDR_KEEP_PANE, ENV_GOVERNANCE, ENV_EPISODE_ID } from "../src/kernel/env-names.ts";
119
125
  export { ENV_HERDR_KEEP_PANE, ENV_GOVERNANCE } from "../src/kernel/env-names.ts";
120
126
  import { adoptLegacyEnvironment } from "../src/kernel/env-names.ts";
121
127
 
@@ -165,8 +171,10 @@ export interface GrantsSession extends NativeSessionHost {
165
171
  ownExecutionId?: string;
166
172
  /** Stable identity shared by this root session and every descendant. */
167
173
  episodeId: string;
168
- /** Descendants this subtree may still create — the cardinality bound ADR-0008 never had. */
174
+ /** Configured active descendant capacity; use capacity.available for actual reservations. */
169
175
  fanoutBudget: number;
176
+ capacity: CapacityAllocator;
177
+ capacityRefusal?: StructuredRefusal;
170
178
  /** Whether delegation tools are active. Reconciled against the owner-bound root at session_start. */
171
179
  mayDelegate: boolean;
172
180
  /** True only after session_start binds this instance to ctx.sessionManager. */
@@ -178,10 +186,8 @@ export interface GrantsSession extends NativeSessionHost {
178
186
  /** Committed per-definition and global child runtime defaults. */
179
187
  readonly definitionRuntimeSettings: DefinitionRuntimeSettings;
180
188
  /** PD-7's future session controls write here; deliberately empty in this commit. */
189
+ currentThinking?: () => string;
181
190
  readonly definitionRuntimeOverrides: Map<string, DefinitionRuntimeChoice>;
182
- readonly sessionModelPrompt: "ask" | "never";
183
- sessionModelPrompted: boolean;
184
- sessionModelPromptInFlight?: Promise<void>;
185
191
  /** Path to this extension, so a child granted `tool:delegate` can delegate in turn. */
186
192
  readonly extensionPath?: string;
187
193
  /** Hook-only observer path for leaf children; it exposes no tools. */
@@ -194,8 +200,6 @@ export interface GrantsSession extends NativeSessionHost {
194
200
  * context handoff (ADR-0078): its file path for `fork`, its message turns for `pruned`.
195
201
  */
196
202
  parentSession?: ParentSession;
197
- /** ADR-0077: the session's advisor, off unless the environment enables one. Never consulted for authority. */
198
- advisorSession: AdvisorSession;
199
203
  /** Root identity keyed to ctx.sessionManager once session_start supplies it. */
200
204
  reloadLifecycle: ReloadLifecycle;
201
205
  /** Approval keys approved for this session. In memory only — this dies with the process. */
@@ -213,8 +217,6 @@ export interface GrantsSession extends NativeSessionHost {
213
217
  inheritedApprovals: Map<string, string | undefined>;
214
218
  /** ONE single-flight queue for the whole session — see `obtainApprovals` for why it lives here. */
215
219
  readonly approvalGateFor: ReturnType<typeof createApprovalGateProvider>;
216
- /** One cumulative cost threshold shared by every child in this episode. */
217
- readonly episodeCostGate: EpisodeCostGate;
218
220
  /** Set at `session_start`; `process.cwd()` until then. */
219
221
  cwd: string;
220
222
  /** This session's own grant. Starts as the inherited upper bound, tightened once tools are observed. */
@@ -222,6 +224,7 @@ export interface GrantsSession extends NativeSessionHost {
222
224
  observed: boolean;
223
225
  observedTools: string[] | null;
224
226
  /** ADR-0016: `SKILL.md` definitions, keyed by name. The format this package spawns from now. */
227
+ ensureDefinitions?: () => Promise<void>;
225
228
  definitions: Map<string, SkillDefinition>;
226
229
  /**
227
230
  * Definitions discovery dropped, and why — reported at session start (ADR-0076, rule 8).
@@ -248,6 +251,10 @@ export interface GrantsSession extends NativeSessionHost {
248
251
  workspaceSkips: string[];
249
252
  /** Which registry ids this machine has accepted, and which it has not. Reported, and drives `/grants`. */
250
253
  workspaceAcceptance?: { accepted: string[]; firstUse: boolean; unaccepted: string[] };
254
+ /** Trusted discovery dependencies supplied by composition, never inherited authority. */
255
+ readonly discovery: SessionDiscovery;
256
+ /** Exact unresolved discovery failure/capability; physical recovery never rewrites the failed outcome. */
257
+ discoveryCleanupFailure?: BoundedReadCleanupError;
251
258
  catalog: Catalog;
252
259
  /**
253
260
  * The in-flight catalog build, so `delegate` can wait for it instead of racing it.
@@ -327,7 +334,9 @@ export interface GrantsSession extends NativeSessionHost {
327
334
  * Failing to establish is not an error: a machine with no registry has no workspaces to route to, and the
328
335
  * absent pin refuses anything that tries. That is the same direction as every other failure in this mechanism.
329
336
  */
330
- async function establishRootPin(session: GrantsSession): Promise<void> {
337
+ async function establishRootPin(session: GrantsSession, lifecycle: ReloadLifecycle): Promise<void> {
338
+ assertDiscoveryHealthy(session, lifecycle);
339
+ if (lifecycle.workspacePinFailure) throw lifecycle.workspacePinFailure;
331
340
  if (session.pinSettled) return;
332
341
  // **One assignment, at the end, on every path.** Review found the previous shape — assign at each `return`
333
342
  // — missing two of five exits: the `catch` around an unreadable registry, which is precisely the state a
@@ -339,26 +348,34 @@ async function establishRootPin(session: GrantsSession): Promise<void> {
339
348
  // settled with nothing settled — routing nowhere, which is safe, but with the LIFECYCLE unset, so the next
340
349
  // reload would mint again. Review could construct no throw today; "currently unreachable" is exactly the
341
350
  // property this feature has now been wrong about four times, and the ordering costs nothing.
342
- const settled = await settleWorkspacePin(session);
351
+ // Charge/publish BEFORE invoking trusted loaders: reentrant startup/init/reload joins the same owner.
352
+ lifecycle.workspacePinAcquisition ??= Promise.resolve().then(async () => {
353
+ const settled = await settleWorkspacePin(session, lifecycle);
354
+ assertDiscoveryHealthy(session, lifecycle);
355
+ lifecycle.workspacePin = settled;
356
+ return settled;
357
+ });
358
+ const settled = await lifecycle.workspacePinAcquisition;
359
+ assertDiscoveryHealthy(session, lifecycle);
343
360
  session.pinSettled = true;
344
361
  session.workspacePin = settled;
345
- session.reloadLifecycle.workspacePin = settled;
346
362
  }
347
363
 
348
364
  /**
349
- * What this session's destination pin IS (ADR-0042). Every path returns a map; none writes anything.
365
+ * What this session's destination pin IS (ADR-0042). Ordinary unavailable registries settle an empty map;
366
+ * physical reader cleanup failures instead retain their exact rejected outcome on the owning lifecycle.
350
367
  *
351
- * An empty map means "settled, and you may route nowhere", which is different from never having settled and
352
- * is what every failure resolves to. The distinction that matters is not empty-versus-absent but
368
+ * An empty map means "settled, and you may route nowhere", which is different from never having settled.
369
+ * The distinction that matters is not empty-versus-absent but
353
370
  * settled-versus-not, and settling happens exactly once per owner.
354
371
  */
355
- async function settleWorkspacePin(session: GrantsSession): Promise<WorkspacePins> {
372
+ async function settleWorkspacePin(session: GrantsSession, lifecycle: ReloadLifecycle): Promise<WorkspacePins> {
356
373
  // Settled by an EARLIER SESSION OBJECT for this same owner — an extension reload. Adopting rather than
357
374
  // re-deriving is the point: a root may mint, but only once, and only from the registry as it stood before
358
375
  // any child had a chance to rewrite it.
359
- if (session.reloadLifecycle.workspacePin) return session.reloadLifecycle.workspacePin;
376
+ if (lifecycle.workspacePin) return lifecycle.workspacePin;
360
377
 
361
- const raw = session.reloadLifecycle.root[ENV_WORKSPACE_PIN];
378
+ const raw = lifecycle.root[ENV_WORKSPACE_PIN];
362
379
  const inherited = parseWorkspacePin(raw);
363
380
  // Inherited, so it is authority and is kept exactly as it arrived — including an empty one, which says
364
381
  // "your parent established a pin and gave you none of it".
@@ -379,51 +396,159 @@ async function settleWorkspacePin(session: GrantsSession): Promise<WorkspacePins
379
396
  const registryPath = process.env[ENV_WORKSPACE_REGISTRY];
380
397
  if (!registryPath) return new Map();
381
398
  try {
382
- const registry = await loadWorkspaceRegistry(registryPath);
399
+ let registry: Awaited<ReturnType<typeof loadWorkspaceRegistry>>;
400
+ try {
401
+ registry = await (session.discovery.registry ?? loadWorkspaceRegistry)(registryPath);
402
+ } catch (error) {
403
+ if (error instanceof BoundedReadCleanupError) throw error;
404
+ assertDiscoveryHealthy(session, lifecycle);
405
+ // Only registry unavailability is soft: the catalog reports it, and settling prevents later reminting.
406
+ return new Map();
407
+ }
408
+ assertDiscoveryHealthy(session, lifecycle);
383
409
  // **The id SET, which the destination pin does not cover.** ADR-0042 bound what an id means; a child
384
410
  // holding `tool:write` inherits the registry path and can append an id of its own, and the NEXT root
385
411
  // session mints a pin for it. Measured: a child-created id reached the catalog, the pin and a real route
386
412
  // with no operator action. So an id nobody accepted is not pinned, and therefore not routable.
387
- const acceptance = await reconcileAcceptedWorkspaces(registryPath, Object.keys(registry.workspaces));
388
- session.workspaceAcceptance = acceptance;
389
- for (const id of acceptance.unaccepted)
390
- session.workspaceSkips.push(`${id} — it is in the registry but this machine has never accepted it`);
413
+ const acceptance = await reconcileAcceptedWorkspaces(
414
+ registryPath,
415
+ Object.keys(registry.workspaces),
416
+ process.env,
417
+ session.discovery.acceptanceRead,
418
+ () => assertDiscoveryHealthy(session, lifecycle),
419
+ );
420
+ assertDiscoveryHealthy(session, lifecycle);
421
+ const skips = acceptance.unaccepted.map(
422
+ (id) => `${id} — it is in the registry but this machine has never accepted it`,
423
+ );
391
424
  const accepted = new Set(acceptance.accepted);
392
425
  const narrowed = {
393
426
  workspaces: Object.fromEntries(Object.entries(registry.workspaces).filter(([id]) => accepted.has(id))),
394
427
  };
395
- return await establishWorkspacePin(narrowed, realpath, (id, reason) =>
396
- session.workspaceSkips.push(`${id} — ${reason}`),
428
+ const pins = await establishWorkspacePin(
429
+ narrowed,
430
+ async (path) => {
431
+ assertDiscoveryHealthy(session, lifecycle);
432
+ try {
433
+ return await (session.discovery.canonicalise ?? realpath)(path);
434
+ } finally {
435
+ assertDiscoveryHealthy(session, lifecycle);
436
+ }
437
+ },
438
+ (id, reason) => {
439
+ assertDiscoveryHealthy(session, lifecycle);
440
+ skips.push(`${id} — ${reason}`);
441
+ },
397
442
  );
398
- } catch {
399
- // An unreadable registry is already reported by the catalog and at session start. It does NOT follow that
400
- // nothing is blocked — an earlier draft of this comment claimed that and review measured it false, because
401
- // a grant supplied through `PI_DADDY_GRANT` never passes the catalog and `workspace:` is exempt from the
402
- // unknown check anyway. A session can genuinely hold `workspace:w1` and be refused for want of a pin.
403
- // Returning an empty map SETTLES it, so a later reload cannot mint over a registry that has since changed.
404
- return new Map();
443
+ assertDiscoveryHealthy(session, lifecycle);
444
+ session.workspaceAcceptance = acceptance;
445
+ for (const skip of skips) session.workspaceSkips.push(skip);
446
+ return pins;
447
+ } catch (error) {
448
+ if (error instanceof BoundedReadCleanupError) {
449
+ lifecycle.workspacePinFailure = error;
450
+ throw error;
451
+ }
452
+ throw error;
405
453
  }
406
454
  }
407
455
 
408
- export async function loadProjectDefinitions(session: GrantsSession, cwd: string): Promise<void> {
409
- await establishRootPin(session);
456
+ /** Publish a safely observed rejection for delegation, never a fulfilled prior catalog. The extension reports it. */
457
+ export function recordDiscoveryCleanupFailure(
458
+ session: GrantsSession,
459
+ error: BoundedReadCleanupError,
460
+ lifecycle = session.reloadLifecycle,
461
+ ): BoundedReadCleanupError {
462
+ const failure = collectBoundedReadFailures([lifecycle.discoveryCleanupFailure, error])!;
463
+ lifecycle.discoveryCleanupFailure = failure;
464
+ if (session.reloadLifecycle === lifecycle) session.discoveryCleanupFailure = failure;
465
+ return failure;
466
+ }
467
+
468
+ export function retainDiscoveryCleanupFailure(
469
+ session: GrantsSession,
470
+ error: BoundedReadCleanupError,
471
+ lifecycle = session.reloadLifecycle,
472
+ ): void {
473
+ const failure = recordDiscoveryCleanupFailure(session, error, lifecycle);
474
+ if (session.reloadLifecycle === lifecycle) {
475
+ session.catalogReady = Promise.reject(failure);
476
+ void session.catalogReady.then(undefined, () => undefined);
477
+ }
478
+ }
479
+
480
+ /** Terminal owner state, checked independently of a replaceable catalog promise and after every wait. */
481
+ export function assertDiscoveryHealthy(session: GrantsSession, lifecycle = session.reloadLifecycle): void {
482
+ const failure =
483
+ lifecycle.discoveryCleanupFailure ??
484
+ lifecycle.workspacePinFailure ??
485
+ (session.reloadLifecycle === lifecycle ? session.discoveryCleanupFailure : undefined);
486
+ if (failure) throw failure;
487
+ if (session.reloadLifecycle !== lifecycle) throw new Error("discovery owner replaced before publication");
488
+ }
489
+
490
+ async function delegationCatalog(session: GrantsSession): Promise<Catalog> {
491
+ const lifecycle = session.reloadLifecycle;
492
+ assertDiscoveryHealthy(session, lifecycle);
493
+ const catalog = await session.catalogReady;
494
+ assertDiscoveryHealthy(session, lifecycle);
495
+ return catalog;
496
+ }
497
+
498
+ export async function loadProjectDefinitions(
499
+ session: GrantsSession,
500
+ cwd: string,
501
+ lifecycle = session.reloadLifecycle,
502
+ selected?: readonly SelectedCommand[],
503
+ ): Promise<void> {
504
+ try {
505
+ assertDiscoveryHealthy(session, lifecycle);
506
+ await loadProjectDiscovery(session, cwd, lifecycle, selected);
507
+ } catch (error) {
508
+ if (error instanceof BoundedReadCleanupError) retainDiscoveryCleanupFailure(session, error, lifecycle);
509
+ throw error;
510
+ }
511
+ }
512
+
513
+ async function loadProjectDiscovery(
514
+ session: GrantsSession,
515
+ cwd: string,
516
+ lifecycle: ReloadLifecycle,
517
+ selected?: readonly SelectedCommand[],
518
+ ): Promise<void> {
519
+ await establishRootPin(session, lifecycle);
410
520
  const skips: string[] = [];
411
- session.definitions = await loadDefinitions(cwd, (_path, reason) => skips.push(reason));
521
+ assertDiscoveryHealthy(session, lifecycle);
522
+ const snapshot = selected === undefined ? undefined : await selectedDefinitions(selected);
523
+ const definitions =
524
+ snapshot?.definitions ??
525
+ (await (session.discovery.definitions ?? loadDefinitions)(cwd, (_path, reason) => skips.push(reason)));
526
+ if (snapshot) skips.push(...snapshot.skips);
527
+ assertDiscoveryHealthy(session, lifecycle);
528
+ const ready = buildCatalog(
529
+ {
530
+ ...(snapshot ? { snapshot } : {}),
531
+ cwd,
532
+ observedTools: session.observedTools,
533
+ // ADR-0035: `workspace:<id>` is a capability, so the registered ids belong in the catalog the same way
534
+ // discovered definitions do — for `/grants` to list what this session may route to and for `init` to
535
+ // scaffold them. Read live rather than cached at load, because the registry is an operator file.
536
+ registryPath: process.env[ENV_WORKSPACE_REGISTRY],
537
+ },
538
+ session.discovery,
539
+ );
540
+ session.catalogReady = ready;
541
+ const catalog = await ready;
542
+ assertDiscoveryHealthy(session, lifecycle);
543
+ session.definitions = definitions;
412
544
  session.definitionSkips = skips;
413
- session.catalogReady = buildCatalog({
414
- cwd,
415
- observedTools: session.observedTools,
416
- // ADR-0035: `workspace:<id>` is a capability, so the registered ids belong in the catalog the same way
417
- // discovered definitions do — for `/grants` to list what this session may route to and for `init` to
418
- // scaffold them. Read live rather than cached at load, because the registry is an operator file.
419
- registryPath: process.env[ENV_WORKSPACE_REGISTRY],
420
- });
421
- session.catalog = await session.catalogReady;
545
+ session.catalog = catalog;
422
546
  }
423
547
  export function createGrantsSession(
424
548
  extensionPath: string | undefined,
425
549
  lifecycle?: ReloadLifecycle,
426
550
  observerExtensionPath?: string,
551
+ discovery: SessionDiscovery = {},
427
552
  ): GrantsSession {
428
553
  // ADR-0076 PR 3b: legacy PI_GRANTS_* names are adopted BEFORE the first environment read and before the
429
554
  // reload snapshot, or an operator on the old names would get an ungoverned wildcard root (review finding).
@@ -459,34 +584,18 @@ export function createGrantsSession(
459
584
  // G7 / A-S4 + B-I4: strict, three-way parsing that fails CLOSED. A malformed bound used to yield
460
585
  // `NaN`, and every comparison against `NaN` is false, so depth limiting switched itself off.
461
586
  const bounds = depthConfig(environment[ENV_DEPTH], environment[ENV_MAX_DEPTH]);
462
- const costConfig = episodeCostConfiguration(storeCwd, environment[ENV_EPISODE_COST_CEILING]);
463
587
  const runtimeConfig = projectDefinitionRuntimeConfiguration(storeCwd);
464
588
  const depth = bounds.depth;
465
- const maxDepth = costConfig.malformed || runtimeConfig.malformed ? 0 : bounds.maxDepth;
589
+ const maxDepth = runtimeConfig.malformed ? 0 : bounds.maxDepth;
466
590
  const emptyCatalog = makeCatalog([]);
467
- // ADR-0077. The environment decides whether there is an advisor at all; the project's settings block may only
468
- // narrow it. The block IS read — the first version passed `undefined` and every narrowing the release advertised
469
- // was dead code reachable only from tests, which review measured: `enabled: false` turned nothing off.
470
- //
471
- // Read from `storeCwd` for `loadStoredGrantStateSync`'s reason: this factory runs before any hook, so `ctx.cwd`
472
- // does not exist yet. Reading a workspace-writable file here is safe precisely because it can only narrow.
473
- const advisorSession = createAdvisorSession({
474
- block: projectAdvisorBlock(storeCwd, environment),
475
- episodeId,
476
- ...(storedLedger ? { ledgerPath: storedLedger } : {}),
477
- });
591
+ const capacityState = capacityForLifecycle(activeLifecycle, environment[ENV_FANOUT]);
478
592
  const session: GrantsSession = {
479
- advisorSession,
480
593
  adoptedLegacyEnv,
481
594
  governed,
482
595
  inherited,
483
596
  depth,
484
597
  maxDepth,
485
- malformedBounds: [
486
- ...bounds.malformed,
487
- ...(costConfig.malformed ? [costConfig.malformed] : []),
488
- ...(runtimeConfig.malformed ? [runtimeConfig.malformed] : []),
489
- ],
598
+ malformedBounds: [...bounds.malformed, ...(runtimeConfig.malformed ? [runtimeConfig.malformed] : [])],
490
599
  definitionSkips: [],
491
600
  workspacePin: undefined,
492
601
  pinSettled: false,
@@ -511,9 +620,9 @@ export function createGrantsSession(
511
620
  ownSpawnId: environment[ENV_PARENT_ID]?.trim() || `d${depth}`,
512
621
  ownExecutionId: environment[ENV_EXECUTION_ID]?.trim() || undefined,
513
622
  episodeId,
514
- // The cardinality bound ADR-0008 never had: it attenuates downward like depth, so a subtree can never
515
- // create more descendants than its root was given — with no shared state, no lock and no counter file.
516
- fanoutBudget: budgetFromEnv(environment[ENV_FANOUT]),
623
+ fanoutBudget: capacityState.allocator.total,
624
+ capacity: capacityState.allocator,
625
+ capacityRefusal: capacityState.refusal,
517
626
  /**
518
627
  * Review finding S-5, fixed. The comment on the tools has always claimed conditional registration; the
519
628
  * call was unconditional, `DELEGATE_CAPABILITY` was imported and never used, and "withhold it and the
@@ -528,8 +637,6 @@ export function createGrantsSession(
528
637
  modelResolutionCache: new Map<string, boolean>(),
529
638
  definitionRuntimeSettings: runtimeConfig.settings,
530
639
  definitionRuntimeOverrides: new Map<string, DefinitionRuntimeChoice>(),
531
- sessionModelPrompt: runtimeConfig.prompt,
532
- sessionModelPrompted: false,
533
640
  extensionPath,
534
641
  observerExtensionPath,
535
642
  activityRootId,
@@ -538,12 +645,12 @@ export function createGrantsSession(
538
645
  sessionApprovalBindings: new Map<string, ApprovalBinding>(),
539
646
  inheritedApprovals: parseInherited(environment[ENV_APPROVED]),
540
647
  approvalGateFor: createApprovalGateProvider(),
541
- episodeCostGate: new EpisodeCostGate(costConfig.ceiling),
542
648
  cwd: process.cwd(),
543
649
  ownGrant: deriveOwnGrant(inherited, null),
544
650
  observed: false,
545
651
  observedTools: null,
546
652
  definitions: new Map<string, SkillDefinition>(),
653
+ discovery: Object.freeze({ ...discovery }),
547
654
  catalog: emptyCatalog,
548
655
  catalogReady: Promise.resolve(emptyCatalog),
549
656
  delegationContext: async (approved?: InheritableApproval[]) => ({
@@ -560,21 +667,17 @@ export function createGrantsSession(
560
667
  // one through the same builder `publishChildEnv` uses. Read here rather than in the kernel so there is
561
668
  // one place that knows the environment is where a session's own pin lives.
562
669
  ...(session.workspacePin ? { workspacePin: session.workspacePin } : {}),
563
- // ADR-0078: composition reads, the kernel decides. Called only for a mode that survived the gate.
564
- // `options` is forwarded, and its absence is why the second decision point was dead in production: a
565
- // one-parameter arrow is assignable to a two-parameter type, so the ids reached here and were discarded while
566
- // the advisor had already been asked. Review measured it. `test/pruning-advice.test.ts` now goes through this
567
- // function rather than calling the stager directly.
670
+ // Context is staged only after its capability survives the ceiling, grant and gate.
568
671
  stageHandoff: (granted, options) =>
569
672
  createHandoffStager({
570
673
  cwd: session.cwd,
571
674
  forkRoot: join(agentDir(), "context-forks"),
572
675
  ...(session.parentSession ? { parentSession: session.parentSession } : {}),
573
676
  })(granted, options),
574
- catalog: await session.catalogReady,
677
+ catalog: await delegationCatalog(session),
575
678
  // R-32: where each granted skill lives, so `planSpawn` can pass `--skill` for those and only those.
576
679
  // Derived from the catalog's own `source`, so it cannot drift from what was discovered.
577
- skillPaths: skillPathsFromCatalog(await session.catalogReady),
680
+ skillPaths: skillPathsFromCatalog(await delegationCatalog(session)),
578
681
  // ADR-0016: operator-authored SKILL.md definitions, so `delegate({agent})` can name one.
579
682
  definitions: session.definitions,
580
683
  // The herdr executor drives the child after starting it, so its plan must NOT carry `--print`.
@@ -603,7 +706,14 @@ export function createGrantsSession(
603
706
  }
604
707
  session.publishChildEnv();
605
708
  },
606
- reconcileEnvironment: (environment, lifecycle) => reconcileSessionEnvironment(session, environment, lifecycle),
709
+ reconcileEnvironment: (environment, lifecycle) => {
710
+ if (session.reloadLifecycle !== lifecycle) {
711
+ session.pinSettled = false;
712
+ session.workspacePin = undefined;
713
+ session.discoveryCleanupFailure = lifecycle.discoveryCleanupFailure;
714
+ }
715
+ reconcileSessionEnvironment(session, environment, lifecycle);
716
+ },
607
717
  publishChildEnv: () => {
608
718
  const env = childEnv({
609
719
  ownGrant: session.ownGrant,
@@ -629,66 +739,17 @@ export function createGrantsSession(
629
739
  return session;
630
740
  }
631
741
 
632
- /**
633
- * The `advisor` block of `.pi/pi-daddy/settings.json`, or undefined.
634
- *
635
- * Unreadable, absent or malformed all yield undefined: this file is the reviewable record, not authority, and the
636
- * only thing it can do to an advisor is turn one off. A parse failure therefore costs nothing worth reporting.
637
- */
638
- export function reconcileAdvisorSession(session: GrantsSession, environment: NodeJS.ProcessEnv): void {
639
- session.advisorSession = createAdvisorSession({
640
- block: projectAdvisorBlock(session.storeCwd, environment),
641
- episodeId: session.episodeId,
642
- ...(session.ledgerPath ? { ledgerPath: session.ledgerPath } : {}),
643
- env: environment,
644
- });
645
- }
646
-
647
- function episodeCostConfiguration(cwd: string, inherited: string | undefined): { ceiling: number; malformed?: string } {
648
- if (inherited !== undefined) {
649
- const value = Number(inherited);
650
- return Number.isFinite(value) && value > 0
651
- ? { ceiling: value }
652
- : { ceiling: episodeCostCeilingFromSettings(undefined), malformed: ENV_EPISODE_COST_CEILING };
653
- }
654
- const path = projectSettingsPath(cwd);
655
- try {
656
- const stats = statSync(path);
657
- if (!stats.isFile() || stats.size > 1024 * 1024) {
658
- return { ceiling: episodeCostCeilingFromSettings(undefined), malformed: "settings.json episodeCostCeiling" };
659
- }
660
- const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
661
- if (parsed && typeof parsed === "object" && Object.hasOwn(parsed, "episodeCostCeiling")) {
662
- const value = (parsed as Record<string, unknown>).episodeCostCeiling;
663
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
664
- return { ceiling: episodeCostCeilingFromSettings(undefined), malformed: "settings.json episodeCostCeiling" };
665
- }
666
- }
667
- return { ceiling: episodeCostCeilingFromSettings(parsed) };
668
- } catch (error) {
669
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
670
- return { ceiling: episodeCostCeilingFromSettings(undefined) };
671
- }
672
- return { ceiling: episodeCostCeilingFromSettings(undefined), malformed: "settings.json episodeCostCeiling" };
673
- }
674
- }
675
-
676
742
  function projectDefinitionRuntimeConfiguration(cwd: string): {
677
743
  settings: DefinitionRuntimeSettings;
678
- prompt: "ask" | "never";
679
744
  malformed?: string;
680
745
  } {
681
- const empty = { settings: definitionRuntimeSettingsFrom(undefined), prompt: "ask" as const };
746
+ const empty = { settings: definitionRuntimeSettingsFrom(undefined) };
682
747
  try {
683
748
  const path = projectSettingsPath(cwd);
684
- const stats = statSync(path);
685
- if (!stats.isFile() || stats.size > 1024 * 1024) return { ...empty, malformed: "settings.json model defaults" };
686
- const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
749
+ const parsed: unknown = JSON.parse(readBoundedTextSync(path, { maxBytes: 1024 * 1024 }).text);
687
750
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
688
751
  return { ...empty, malformed: "settings.json model defaults" };
689
752
  const root = parsed as Record<string, unknown>;
690
- if (root.sessionModelPrompt !== undefined && !["ask", "never"].includes(String(root.sessionModelPrompt)))
691
- return { ...empty, malformed: "settings.json sessionModelPrompt" };
692
753
  const candidates = [root.defaults, ...(Array.isArray(root.definitions) ? root.definitions : [])];
693
754
  for (const candidate of candidates) {
694
755
  if (candidate === undefined) continue;
@@ -702,28 +763,10 @@ function projectDefinitionRuntimeConfiguration(cwd: string): {
702
763
  }
703
764
  return {
704
765
  settings: definitionRuntimeSettingsFrom(root),
705
- prompt: root.sessionModelPrompt === "never" ? "never" : "ask",
706
766
  };
707
767
  } catch (error) {
768
+ if (error instanceof BoundedReadCleanupError) throw error;
708
769
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return empty;
709
770
  return { ...empty, malformed: "settings.json model defaults" };
710
771
  }
711
772
  }
712
-
713
- function projectAdvisorBlock(cwd: string, env: NodeJS.ProcessEnv = process.env): unknown {
714
- // Only when an advisor could exist at all. This runs in every session including every child, before any hook, and
715
- // a child can never use the result because `PI_DADDY_ADVISOR` is stripped from it.
716
- if (!env[ENV_ADVISOR]?.trim()) return undefined;
717
- try {
718
- const path = projectSettingsPath(cwd);
719
- // Bounded and type-checked first: this is the third unbounded session-start read AGENTS.md warns about, and the
720
- // only one whose path a governed child holding `tool:write` can replace with a FIFO — which would hang pi
721
- // before any hook exists to report it.
722
- const stats = statSync(path);
723
- if (!stats.isFile() || stats.size > 1024 * 1024) return undefined;
724
- const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
725
- return typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>).advisor : undefined;
726
- } catch {
727
- return undefined;
728
- }
729
- }
@@ -184,6 +184,7 @@ export async function releaseDelegationWorkspace(input: {
184
184
  reason: string;
185
185
  /** Deliberately keep the lease: a herdr writer tab would not close, so the pane may still be live. */
186
186
  retain?: boolean;
187
+ onObservationFailure?: (error: unknown) => void;
187
188
  }): Promise<LeaseReleaseOutcome | "retained" | undefined> {
188
189
  if (!input.prepared) return undefined;
189
190
  // A retained lease writes no `state: "released"`, so the record stays `active` and the NEXT owner reads
@@ -197,7 +198,7 @@ export async function releaseDelegationWorkspace(input: {
197
198
  : await input.prepared.lease.release(input.reason);
198
199
  if (input.ledgerPath) {
199
200
  await appendLedgerEvent(
200
- { path: input.ledgerPath, strict: true },
201
+ { path: input.ledgerPath, strict: !input.onObservationFailure, onFailure: input.onObservationFailure },
201
202
  buildWorkspaceLeaseEvent({
202
203
  ...(input.episodeId ? { episodeId: input.episodeId } : {}),
203
204
  executionId: input.executionId,
@@ -0,0 +1,25 @@
1
+ # Captured worker artifact
2
+
3
+ The packaged Linux x64 helper is built from `worker.c` with the official Zig 0.14.1 compiler
4
+ archive and static musl target pinned in `toolchain.json`. No system compiler or libc is used.
5
+ The distribution is published at <https://ziglang.org/download/>; the archive SHA-256 was checked
6
+ against <https://ziglang.org/download/index.json>. `provenance.json` records all build inputs,
7
+ output bytes/hash and the unmodified license files copied from that archive.
8
+
9
+ From `packages/pi-daddy`, `npm run build:worker` downloads and hash-checks the pinned archive,
10
+ extracts it into a fresh temporary directory, rebuilds, and writes the binary, checksum,
11
+ provenance, license copies and `src/executors/worker-artifact.ts`. Node 22.19+ and `tar` are required.
12
+ `node scripts/build-worker.ts --verify` repeats the build in a new directory and compares every
13
+ output byte without modifying tracked files; CI runs this on each Node matrix leg. An existing
14
+ archive can be supplied with `PI_DADDY_WORKER_TOOLCHAIN_ARCHIVE`; it must pass the same hash check.
15
+ The temporary compiler is removed after each run. The verified archive cache is under the OS
16
+ temporary directory; it is never part of the npm package.
17
+
18
+ The runtime verifies the opened executable against the SHA-256 constant compiled into its
19
+ JavaScript before executing that same held descriptor. The adjacent checksum is for independent
20
+ verification, never runtime authority. This binds the artifact to the reviewed package contents;
21
+ it does not protect a package whose JavaScript was modified by the same user.
22
+
23
+ The build checks ELF64/x86-64 and absence of dynamic/interpreter program headers. Runtime still
24
+ requires the qualified Linux kernel facilities: subreaper, pidfds and `/proc` child enumeration.
25
+ No compiler runs during package installation or execution.
Binary file
@@ -0,0 +1 @@
1
+ 981a45591b1de790501ef531f6b0ccc8299b1e9a2d08a7970ccaeeecb9868798