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
@@ -1,3 +1,6 @@
1
+ import { reconcileDelegationCapacity, reserveDelegationCapacity } from "./session-capacity.ts";
2
+ import type { CapacityReservation } from "../src/kernel/capacity.ts";
3
+ import { assertDefinitionIdentity } from "./definition-describe.ts";
1
4
  /**
2
5
  * Plan, gate, audit and run ONE governed child — the whole of a delegation except its tool surface.
3
6
  *
@@ -12,8 +15,6 @@
12
15
  */
13
16
 
14
17
  import { nativeDelegationContext } from "./delegation-native.ts";
15
- import { adviseEffort } from "./effort-advice.ts";
16
- import { advisePruning } from "./pruning-advice.ts";
17
18
  import { DELEGATE_SUBJECT, shouldSeekApproval } from "../src/kernel/approval.ts";
18
19
  import { planDelegation } from "../src/kernel/delegate.ts";
19
20
  import {
@@ -27,13 +28,12 @@ import {
27
28
  import type { InheritableApproval } from "../src/kernel/approval.ts";
28
29
  import type { GrantsSession } from "./session.ts";
29
30
  import type { CorrelationMetadata } from "../src/kernel/correlation.ts";
30
- import { preflightModel, type ModelCatalogue } from "../src/kernel/model-preflight.ts";
31
+ import { preflightModel, runtimePairUnavailable, type ModelCatalogue } from "../src/kernel/model-preflight.ts";
31
32
  import { GovernanceRefusal, refusal as structuredRefusal } from "../src/kernel/refusals.ts";
32
33
  import { executePlannedChild, type DelegationOutcome } from "./execute-child.ts";
33
34
  import { recordDelegationDecision, type ApprovalLedgerFacts } from "./delegation-ledger.ts";
34
35
  import type { ExecutionOccurrenceIds } from "./execution-occurrence.ts";
35
- import { ENV_EPISODE_COST_CEILING } from "../src/kernel/env-names.ts";
36
- import { resolvedModelOf, resolveDefinitionRuntime } from "./definition-runtime.ts";
36
+ import { resolveDefinitionRuntime } from "./definition-runtime.ts";
37
37
  import {
38
38
  governedWorkspaceAccess,
39
39
  prepareDelegationWorkspace,
@@ -44,13 +44,12 @@ import {
44
44
 
45
45
  /** What one child was asked to do. The shape both tools accept, per child. */
46
46
  interface ChildSpec {
47
+ definitionId?: string;
47
48
  task: string;
48
49
  agent?: string;
49
50
  tools?: string[];
50
51
  model?: string;
51
52
  thinking?: string;
52
- /** Positive USD override for the whole episode, applied only when this delegation will run. */
53
- episodeCostCeiling?: number;
54
53
  /** ADR-0078: what of the parent's session crosses. Validated in the kernel, never here. */
55
54
  context?: unknown;
56
55
  correlation?: CorrelationMetadata;
@@ -209,6 +208,10 @@ export async function runOneDelegation(
209
208
  * mistake unspellable.
210
209
  */
211
210
  options: {
211
+ /** Trusted reservation made before a chain's upfront gate. */
212
+ capacityReservation?: CapacityReservation;
213
+ /** Exact pair selected during the chain preflight; never reselect after its approval. */
214
+ resolvedRuntime?: import("./definition-runtime.ts").ResolvedDefinitionRuntime;
212
215
  /** Actual public execute argument, never read from model-authored correlation or output. */
213
216
  toolCallId?: string;
214
217
  /** Progress for the parent's status block (ADR-0032). Display only. */
@@ -246,282 +249,226 @@ export async function runOneDelegation(
246
249
  approvalFacts?: ApprovalLedgerFacts;
247
250
  } = {},
248
251
  ): Promise<DelegationOutcome> {
249
- const { toolCallId, onProgress, preApproved, taskFrom, taskFromExecutionId, approvalFacts } = options;
250
- // pi resolves a BARE model id to an unauthenticated provider and the child dies at startup — the id
251
- // alone is not enough, it must be qualified with its provider (`Model<Api>` carries both).
252
- const defaultModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
253
- const configured = resolveDefinitionRuntime({
254
- definition: spec.agent,
255
- explicit: { model: spec.model, thinking: spec.thinking },
256
- session: session.definitionRuntimeOverrides,
257
- settings: session.definitionRuntimeSettings,
258
- piModel: defaultModel,
259
- });
260
- let resolvedRuntime = configured;
261
- const request = {
262
- task: spec.task,
263
- agent: spec.agent,
264
- tools: spec.tools,
265
- model: configured.model,
266
- // Filled below, once the refusals that doom a delegation are known: asking first shipped the task text for a
267
- // child that never starts, which is the ordering this module already fixed for the approval dialog.
268
- thinking:
269
- configured.thinkingSource === "explicit" || configured.thinkingSource === "session"
270
- ? configured.thinking
271
- : undefined,
272
- context: spec.context,
273
- correlation: spec.workspace
274
- ? { ...(spec.correlation ?? {}), workspace_id: spec.workspace.workspace_id }
275
- : spec.correlation,
276
- // The binding's workspace comes from the ROUTING SPEC, which is resolved against the operator
277
- // registry and leased before any human is asked — never from `correlation`, which is a model-supplied
278
- // claim that nothing validates when no spec accompanies it (R-110).
279
- boundWorkspaceId: spec.workspace?.workspace_id,
280
- boundContextId: spec.correlation?.context_id,
281
- };
282
-
283
- // ADR-0031: herdr was DEMANDED (`PI_DADDY_HERDR=1`) and is not answering. Refused rather than relocated —
284
- // the operator chose that over falling back, so the ledger can never name a child that ran somewhere nobody
285
- // chose.
286
- //
287
- // **Decided BEFORE the gate, and the ordering is a fix.** This sat after `planWithApprovals`, which opens the
288
- // approval dialog — so with herdr down a human was asked to approve `tool:bash`, answered *Always*, and was
289
- // then refused anyway. Measured: the answer still reached `process.env.PI_DADDY_APPROVED`, still wrote a
290
- // **30-day project-wide** entry to the persisted store, and still produced a ledger line asserting a human
291
- // approved `bash` for a child that never existed. A refused operation must not leave authority behind, and
292
- // asking for permission that cannot be used is R-25's fatigue shape with nothing bought.
293
- //
294
- // `ctx: null` rather than skipping the plan entirely: the ledger still gets a full, honest record of what was
295
- // requested and refused, and stored approvals still count toward it — nothing is *hidden*, only nobody is
296
- // *asked*. It is the same argument `/grants` uses for its preview.
297
- let executorRefusal = session.executor.refusal;
298
- const modelRefusal = preflightModel(
299
- request.model,
300
- ctx.modelRegistry,
301
- session.modelResolutionCache,
302
- session.allowUnresolvedModels,
303
- );
304
- const { extra, refusal: nativeRefusal } = await nativeDelegationContext(
305
- session,
306
- ids,
307
- budget,
308
- Boolean(executorRefusal || modelRefusal),
309
- );
310
- executorRefusal ||= nativeRefusal;
311
-
312
- // ADR-0077's first decision point, after the refusal checks for the reason above. Fills a blank from the levels
313
- // the CHILD's model reports; never overrules a caller, and yields today's behaviour whenever there is no answer.
314
- if (!executorRefusal && !modelRefusal) {
315
- const advisedThinking = await adviseEffort({
316
- session,
317
- requested: request.thinking,
318
- model: configured.model,
319
- registry: ctx.modelRegistry,
252
+ let capacity: CapacityReservation | undefined;
253
+ let capacityRefusal: import("../src/kernel/refusals.ts").StructuredRefusal | undefined;
254
+ try {
255
+ if (!options.capacityReservation) await reconcileDelegationCapacity(session);
256
+ capacity = options.capacityReservation ?? reserveDelegationCapacity(session, ids.executionId, budget);
257
+ if (capacity.executionId !== ids.executionId || (budget !== undefined && capacity.childAllowance !== budget))
258
+ throw new GovernanceRefusal(
259
+ structuredRefusal("FANOUT_EXCEEDED", "capacity reservation does not match its execution"),
260
+ );
261
+ } catch (error) {
262
+ if (!(error instanceof GovernanceRefusal)) throw error;
263
+ capacityRefusal = structuredRefusal(error.code, error.message, error.details);
264
+ }
265
+ let executorEntered = false;
266
+ try {
267
+ await session.ensureDefinitions?.();
268
+ assertDefinitionIdentity(session, spec);
269
+ const { toolCallId, onProgress, preApproved, taskFrom, taskFromExecutionId, approvalFacts } = options;
270
+ // pi resolves a BARE model id to an unauthenticated provider and the child dies at startup — the id
271
+ // alone is not enough, it must be qualified with its provider (`Model<Api>` carries both).
272
+ const defaultModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
273
+ let runtimeRefusal: import("../src/kernel/refusals.ts").StructuredRefusal | undefined;
274
+ let configured: import("./definition-runtime.ts").ResolvedDefinitionRuntime = {
275
+ model: spec.model ?? defaultModel,
276
+ modelSource: spec.model ? "explicit" : "pi",
277
+ thinkingSource: "pi",
278
+ };
279
+ try {
280
+ configured =
281
+ options.resolvedRuntime ??
282
+ resolveDefinitionRuntime({
283
+ definition: spec.agent,
284
+ explicit: { model: spec.model, thinking: spec.thinking },
285
+ session: session.definitionRuntimeOverrides,
286
+ settings: session.definitionRuntimeSettings,
287
+ piModel: defaultModel,
288
+ piThinking: session.currentThinking?.(),
289
+ authoredPreferences: spec.agent ? session.definitions.get(spec.agent)?.runtimePreferences : undefined,
290
+ unavailable: (choice) => runtimePairUnavailable(choice, ctx.modelRegistry, session.allowUnresolvedModels),
291
+ });
292
+ } catch (error) {
293
+ runtimeRefusal = structuredRefusal("MODEL_UNRESOLVED", `runtime selection refused: ${String(error)}`);
294
+ }
295
+ const resolvedRuntime = configured;
296
+ const request = {
320
297
  task: spec.task,
321
- executionId: ids.executionId,
322
298
  agent: spec.agent,
323
- fallback: configured,
324
- signal,
325
- });
326
- resolvedRuntime = resolveDefinitionRuntime({
327
- definition: spec.agent,
328
- explicit: { model: spec.model, thinking: spec.thinking },
329
- session: session.definitionRuntimeOverrides,
330
- settings: session.definitionRuntimeSettings,
331
- piModel: defaultModel,
332
- advisorThinking: advisedThinking,
333
- });
334
- request.thinking = resolvedRuntime.thinking;
335
- }
299
+ tools: spec.tools,
300
+ model: configured.model,
301
+ thinking: configured.thinking,
302
+ context: spec.context,
303
+ correlation: spec.workspace
304
+ ? { ...(spec.correlation ?? {}), workspace_id: spec.workspace.workspace_id }
305
+ : spec.correlation,
306
+ // The binding's workspace comes from the ROUTING SPEC, which is resolved against the operator
307
+ // registry and leased before any human is asked — never from `correlation`, which is a model-supplied
308
+ // claim that nothing validates when no spec accompanies it (R-110).
309
+ boundWorkspaceId: spec.workspace?.workspace_id,
310
+ boundContextId: spec.correlation?.context_id,
311
+ };
312
+
313
+ // ADR-0031: herdr was DEMANDED (`PI_DADDY_HERDR=1`) and is not answering. Refused rather than relocated —
314
+ // the operator chose that over falling back, so the ledger can never name a child that ran somewhere nobody
315
+ // chose.
316
+ //
317
+ // **Decided BEFORE the gate, and the ordering is a fix.** This sat after `planWithApprovals`, which opens the
318
+ // approval dialog — so with herdr down a human was asked to approve `tool:bash`, answered *Always*, and was
319
+ // then refused anyway. Measured: the answer still reached `process.env.PI_DADDY_APPROVED`, still wrote a
320
+ // **30-day project-wide** entry to the persisted store, and still produced a ledger line asserting a human
321
+ // approved `bash` for a child that never existed. A refused operation must not leave authority behind, and
322
+ // asking for permission that cannot be used is R-25's fatigue shape with nothing bought.
323
+ //
324
+ // `ctx: null` rather than skipping the plan entirely: the ledger still gets a full, honest record of what was
325
+ // requested and refused, and stored approvals still count toward it — nothing is *hidden*, only nobody is
326
+ // *asked*. It is the same argument `/grants` uses for its preview.
327
+ let executorRefusal = session.executor.refusal;
328
+ const modelRefusal =
329
+ runtimeRefusal ??
330
+ preflightModel(request.model, ctx.modelRegistry, session.modelResolutionCache, session.allowUnresolvedModels);
331
+ const { extra, refusal: nativeRefusal } = await nativeDelegationContext(
332
+ session,
333
+ ids,
334
+ capacity?.childAllowance ?? 0,
335
+ Boolean(executorRefusal || modelRefusal || capacityRefusal),
336
+ );
337
+ executorRefusal ||= nativeRefusal;
338
+
339
+ const planContext = extra;
340
+ let preparedWorkspace: PreparedWorkspace | undefined;
341
+ let approvalOutcome: ApprovalOutcome | undefined;
342
+ let plan: ReturnType<typeof planDelegation>;
336
343
 
337
- const planContext = await handoffPlanContext({
338
- session,
339
- base: extra,
340
- task: spec.task,
341
- executionId: ids.executionId,
342
- toolCallId,
343
- blocked: Boolean(executorRefusal || modelRefusal),
344
- preview: () => planWithApprovals(session, request, extra, null, signal, preApproved).then((r) => r.plan),
345
- attribution: {
346
- resolvedModel: resolvedModelOf(resolvedRuntime.model),
347
- modelSource: resolvedRuntime.modelSource,
348
- thinkingLevel: resolvedRuntime.thinking ?? null,
349
- thinkingSource: resolvedRuntime.thinkingSource,
350
- },
351
- ...(signal ? { signal } : {}),
352
- });
353
- let preparedWorkspace: PreparedWorkspace | undefined;
354
- let approvalOutcome: ApprovalOutcome | undefined;
355
- let plan: ReturnType<typeof planDelegation>;
344
+ if (spec.workspace && !executorRefusal && !modelRefusal && !capacityRefusal) {
345
+ // Check non-liftable refusals before taking a lease, and take the lease before asking a human. This
346
+ // preserves both anti-race rules: a doomed spawn cannot bank approval, and a conflicting writer starts
347
+ // no child process.
348
+ const preview = await planWithApprovals(session, request, planContext, null, signal, preApproved);
349
+ plan = preview.plan;
350
+ if (plan.ok || shouldSeekApproval(plan.result)) {
351
+ try {
352
+ preparedWorkspace = await prepareDelegationWorkspace({
353
+ ...(session.workspacePin ? { workspacePin: session.workspacePin } : {}),
354
+ spec: { ...spec.workspace, access: governedWorkspaceAccess(spec.workspace.access, plan.requested) },
355
+ correlation: spec.correlation,
356
+ childId: ids.childId,
357
+ episodeId: session.episodeId,
358
+ executionId: ids.executionId,
359
+ parentExecutionId: ids.parentExecutionId,
360
+ signal,
361
+ ledgerPath: session.ledgerPath,
362
+ });
363
+ request.correlation = preparedWorkspace.correlation;
364
+ const gated = await planWithApprovals(session, request, planContext, ctx, signal, preApproved);
365
+ plan = gated.plan;
366
+ approvalOutcome = gated.approval;
367
+ } catch (error) {
368
+ const value =
369
+ error instanceof GovernanceRefusal
370
+ ? { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) }
371
+ : structuredRefusal("WORKSPACE_LEASE_STALE", `workspace setup failed (${String(error)})`);
372
+ plan = { ...plan, ok: false, reason: value.message, refusal: value };
373
+ }
374
+ }
375
+ } else {
376
+ const gated = await planWithApprovals(
377
+ session,
378
+ request,
379
+ planContext,
380
+ executorRefusal || modelRefusal || capacityRefusal ? null : ctx,
381
+ signal,
382
+ preApproved,
383
+ );
384
+ plan = gated.plan;
385
+ approvalOutcome = gated.approval;
386
+ }
387
+
388
+ if (capacityRefusal) {
389
+ plan = { ...plan, ok: false, reason: capacityRefusal.message, refusal: capacityRefusal };
390
+ } else if (executorRefusal) {
391
+ const message = `grants: ${executorRefusal}`;
392
+ plan = { ...plan, ok: false, reason: message, refusal: structuredRefusal("EXECUTOR_UNAVAILABLE", message) };
393
+ } else if (modelRefusal) {
394
+ // This is a routing preflight, not a grant decision: preserve the resolved capability facts and replace
395
+ // only the outcome. It is still recorded by the common refusal path, and no lease, dialog or child starts.
396
+ plan = { ...plan, ok: false, reason: modelRefusal.message, refusal: modelRefusal };
397
+ }
356
398
 
357
- if (spec.workspace && !executorRefusal && !modelRefusal) {
358
- // Check non-liftable refusals before taking a lease, and take the lease before asking a human. This
359
- // preserves both anti-race rules: a doomed spawn cannot bank approval, and a conflicting writer starts
360
- // no child process.
361
- const preview = await planWithApprovals(session, request, planContext, null, signal, preApproved);
362
- plan = preview.plan;
363
- if (plan.ok || shouldSeekApproval(plan.result)) {
399
+ // The capability decision provisions the child, so this append remains load-bearing and fails closed.
400
+ try {
401
+ await recordDelegationDecision({
402
+ session,
403
+ plan,
404
+ ids,
405
+ agent: spec.agent,
406
+ taskFrom,
407
+ taskFromExecutionId,
408
+ approval: approvalOutcome,
409
+ approvalFacts,
410
+ });
411
+ } catch (error) {
412
+ plan = {
413
+ ...plan,
414
+ ok: false,
415
+ reason: `grants: ledger write failed, denying — ${String(error)}`,
416
+ refusal: structuredRefusal("LEDGER_WRITE_FAILED", `grants: ledger write failed, denying — ${String(error)}`),
417
+ };
418
+ }
419
+
420
+ if (!plan.ok) {
421
+ // EVERY refusal reached after the gate ran. Gating on `ledgerDenied` left three post-gate refusals
422
+ // stranding a 30-day approval — most reachably a human declining the SECOND of two gated capabilities,
423
+ // which needs no fault at all. The predicate is now the rule itself, so it cannot drift from it again.
424
+ if (ctx) await unbankApprovals(session, ctx, approvalOutcome?.banked);
425
+ // Guarded: this contains a `strict: true` append, and on the path where the ledger is already known
426
+ // unwritable an unguarded call replaced the governance refusal, and its code, with a ledger error.
364
427
  try {
365
- preparedWorkspace = await prepareDelegationWorkspace({
366
- ...(session.workspacePin ? { workspacePin: session.workspacePin } : {}),
367
- spec: { ...spec.workspace, access: governedWorkspaceAccess(spec.workspace.access, plan.requested) },
368
- correlation: spec.correlation,
428
+ await releaseDelegationWorkspace({
429
+ prepared: preparedWorkspace,
369
430
  childId: ids.childId,
370
431
  episodeId: session.episodeId,
371
432
  executionId: ids.executionId,
372
433
  parentExecutionId: ids.parentExecutionId,
373
- signal,
374
434
  ledgerPath: session.ledgerPath,
435
+ reason: "refused",
375
436
  });
376
- request.correlation = preparedWorkspace.correlation;
377
- const gated = await planWithApprovals(session, request, planContext, ctx, signal, preApproved);
378
- plan = gated.plan;
379
- approvalOutcome = gated.approval;
380
437
  } catch (error) {
381
- const value =
382
- error instanceof GovernanceRefusal
383
- ? { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) }
384
- : structuredRefusal("WORKSPACE_LEASE_STALE", `workspace setup failed (${String(error)})`);
385
- plan = { ...plan, ok: false, reason: value.message, refusal: value };
438
+ plan = { ...plan, reason: `${plan.reason ?? "refused"}; workspace release record failed: ${String(error)}` };
386
439
  }
440
+ return {
441
+ ok: false,
442
+ text: "",
443
+ reason: plan.reason,
444
+ granted: [],
445
+ depth: plan.childDepth,
446
+ exitCode: null,
447
+ ...(plan.refusal ? { refusal: plan.refusal } : {}),
448
+ };
387
449
  }
388
- } else {
389
- const gated = await planWithApprovals(
390
- session,
391
- request,
392
- planContext,
393
- executorRefusal || modelRefusal ? null : ctx,
394
- signal,
395
- preApproved,
396
- );
397
- plan = gated.plan;
398
- approvalOutcome = gated.approval;
399
- }
400
-
401
- if (executorRefusal) {
402
- const message = `grants: ${executorRefusal}`;
403
- plan = { ...plan, ok: false, reason: message, refusal: structuredRefusal("EXECUTOR_UNAVAILABLE", message) };
404
- } else if (modelRefusal) {
405
- // This is a routing preflight, not a grant decision: preserve the resolved capability facts and replace
406
- // only the outcome. It is still recorded by the common refusal path, and no lease, dialog or child starts.
407
- plan = { ...plan, ok: false, reason: modelRefusal.message, refusal: modelRefusal };
408
- }
409
450
 
410
- // The capability decision provisions the child, so this append remains load-bearing and fails closed.
411
- try {
412
- await recordDelegationDecision({
451
+ executorEntered = true;
452
+ return await executePlannedChild({
453
+ capacityReservation: capacity,
413
454
  session,
414
455
  plan,
415
- ids,
416
456
  agent: spec.agent,
417
- taskFrom,
418
- taskFromExecutionId,
419
- approval: approvalOutcome,
420
- approvalFacts,
457
+ childId: ids.childId,
458
+ executionId: ids.executionId,
459
+ parentExecutionId: ids.parentExecutionId,
460
+ toolCallId: options.toolCallId,
461
+ cwd: ctx.cwd,
462
+ preparedWorkspace,
463
+ resolvedRuntime,
464
+ signal,
465
+ onProgress,
421
466
  });
422
- } catch (error) {
423
- plan = {
424
- ...plan,
425
- ok: false,
426
- reason: `grants: ledger write failed, denying — ${String(error)}`,
427
- refusal: structuredRefusal("LEDGER_WRITE_FAILED", `grants: ledger write failed, denying — ${String(error)}`),
428
- };
429
- }
430
-
431
- if (!plan.ok) {
432
- // EVERY refusal reached after the gate ran. Gating on `ledgerDenied` left three post-gate refusals
433
- // stranding a 30-day approval — most reachably a human declining the SECOND of two gated capabilities,
434
- // which needs no fault at all. The predicate is now the rule itself, so it cannot drift from it again.
435
- if (ctx) await unbankApprovals(session, ctx, approvalOutcome?.banked);
436
- // Guarded: this contains a `strict: true` append, and on the path where the ledger is already known
437
- // unwritable an unguarded call replaced the governance refusal, and its code, with a ledger error.
438
- try {
439
- await releaseDelegationWorkspace({
440
- prepared: preparedWorkspace,
441
- childId: ids.childId,
442
- episodeId: session.episodeId,
443
- executionId: ids.executionId,
444
- parentExecutionId: ids.parentExecutionId,
445
- ledgerPath: session.ledgerPath,
446
- reason: "refused",
447
- });
448
- } catch (error) {
449
- plan = { ...plan, reason: `${plan.reason ?? "refused"}; workspace release record failed: ${String(error)}` };
450
- }
451
- return {
452
- ok: false,
453
- text: "",
454
- reason: plan.reason,
455
- granted: [],
456
- depth: plan.childDepth,
457
- exitCode: null,
458
- ...(plan.refusal ? { refusal: plan.refusal } : {}),
459
- };
467
+ } finally {
468
+ capacity?.finalize(
469
+ executorEntered
470
+ ? { state: "unknown", reason: "execution returned without qualified cleanup" }
471
+ : { state: "not-started", reason: "delegation ended before entering the executor" },
472
+ );
460
473
  }
461
-
462
- if (spec.episodeCostCeiling !== undefined) session.episodeCostGate.setCeiling(spec.episodeCostCeiling);
463
- plan = {
464
- ...plan,
465
- env: { ...plan.env, [ENV_EPISODE_COST_CEILING]: String(session.episodeCostGate.ceiling) },
466
- };
467
- const costUI = ctx.ui as typeof ctx.ui & {
468
- input(title: string, placeholder?: string, opts?: { signal?: AbortSignal }): Promise<string | undefined>;
469
- };
470
- return executePlannedChild({
471
- session,
472
- plan,
473
- agent: spec.agent,
474
- childId: ids.childId,
475
- executionId: ids.executionId,
476
- parentExecutionId: ids.parentExecutionId,
477
- toolCallId: options.toolCallId,
478
- cwd: ctx.cwd,
479
- preparedWorkspace,
480
- resolvedRuntime,
481
- signal,
482
- onProgress,
483
- costGateUI: {
484
- hasUI: ctx.hasUI,
485
- input: (title, placeholder, gateSignal) => costUI.input(title, placeholder, { signal: gateSignal }),
486
- notify: (message) => ctx.ui.notify(message, "warning"),
487
- },
488
- });
489
- }
490
-
491
- /**
492
- * The planner context for one delegation, including a `pruned` handoff narrowed by an advisor (ADR-0077).
493
- *
494
- * **Exported and taking its own `preview`, so the ordering is forced by a test rather than by a reviewer.** Three
495
- * properties live here and each was, at some point in this change's history, true only because somebody had
496
- * checked it by hand: an advisor is not asked for a delegation that is already refused; it is not asked until a
497
- * plan says the `pruned` handoff actually survived the ceiling, the grant and the gate; and the ids it returns
498
- * reach the planner. Reviewers measured all three by mutating the source and finding the suite still green. A
499
- * function with a seam is the only version of this that a test can hold.
500
- */
501
- export async function handoffPlanContext(input: {
502
- session: Parameters<typeof advisePruning>[0]["session"];
503
- base: Record<string, unknown>;
504
- task: string;
505
- executionId?: string;
506
- toolCallId?: string;
507
- /** A refusal is already certain, so nothing may be asked. */
508
- blocked: boolean;
509
- /** Plans with no human in the loop; its result decides whether an advisor is consulted at all. */
510
- preview: () => Promise<{ handoff?: { mode: string } }>;
511
- attribution?: Parameters<typeof advisePruning>[0]["attribution"];
512
- signal?: AbortSignal;
513
- }): Promise<Record<string, unknown>> {
514
- if (input.blocked) return { ...input.base };
515
- const plan = await input.preview();
516
- if (plan.handoff?.mode !== "pruned") return { ...input.base };
517
- const ids = await advisePruning({
518
- session: input.session,
519
- granted: plan.handoff as Parameters<typeof advisePruning>[0]["granted"],
520
- task: input.task,
521
- ...(input.executionId ? { executionId: input.executionId } : {}),
522
- ...(input.toolCallId ? { toolCallId: input.toolCallId } : {}),
523
- ...(input.attribution ? { attribution: input.attribution } : {}),
524
- ...(input.signal ? { signal: input.signal } : {}),
525
- });
526
- return ids ? { ...input.base, handoffTurnIds: ids } : { ...input.base };
527
474
  }
@@ -0,0 +1,53 @@
1
+ import { readCapturedWorkerReceipt } from "../src/governance/captured-worker-record.ts";
2
+ import { createCapacityAllocator, type CapacityAllocator, type CapacityReservation } from "../src/kernel/capacity.ts";
3
+ import { budgetFromEnv } from "../src/kernel/fanout.ts";
4
+ import { GovernanceRefusal, refusal, type StructuredRefusal } from "../src/kernel/refusals.ts";
5
+ import type { ReloadLifecycle } from "./reload-environment.ts";
6
+ import type { GrantsSession } from "./session.ts";
7
+
8
+ /** Live reservations belong to the real owner, not to replaceable extension/configuration objects. */
9
+ export interface OwnerCapacity {
10
+ allocator: CapacityAllocator;
11
+ refusal?: StructuredRefusal;
12
+ }
13
+ export function capacityForLifecycle(lifecycle: ReloadLifecycle, raw: string | undefined): OwnerCapacity {
14
+ let configured: number;
15
+ try {
16
+ configured = budgetFromEnv(raw);
17
+ } catch (error) {
18
+ const rejected = refusal("FANOUT_EXCEEDED", `PI_DADDY_FANOUT refuses delegation: ${String(error)}`);
19
+ lifecycle.capacity ??= { allocator: createCapacityAllocator(0), refusal: rejected };
20
+ return { allocator: lifecycle.capacity.allocator, refusal: rejected };
21
+ }
22
+ lifecycle.capacity ??= { allocator: createCapacityAllocator(configured) };
23
+ if (lifecycle.capacity.refusal) return lifecycle.capacity;
24
+ if (configured !== lifecycle.capacity.allocator.total)
25
+ return {
26
+ allocator: lifecycle.capacity.allocator,
27
+ refusal: refusal(
28
+ "FANOUT_EXCEEDED",
29
+ "PI_DADDY_FANOUT changed for an existing owner; start a new session to change capacity without replacing live reservations",
30
+ ),
31
+ };
32
+ return lifecycle.capacity;
33
+ }
34
+ export function reserveDelegationCapacity(
35
+ session: GrantsSession,
36
+ executionId: string,
37
+ childAllowance?: number,
38
+ ): CapacityReservation {
39
+ if (session.capacityRefusal) throw new GovernanceRefusal(session.capacityRefusal);
40
+ const result = session.capacity.reserve(executionId, childAllowance ?? Math.max(0, session.capacity.available - 1));
41
+ if (!result.ok) throw new GovernanceRefusal(refusal("FANOUT_EXCEEDED", result.reason));
42
+ return result.reservation;
43
+ }
44
+
45
+ /** Recheck only original bound receipt paths; later proof frees capacity, never rewrites a failed outcome. */
46
+ export async function reconcileDelegationCapacity(session: Pick<GrantsSession, "capacity">): Promise<void> {
47
+ for (const reservation of session.capacity.retainedReservations) {
48
+ const identity = reservation.identity;
49
+ if (!identity) continue;
50
+ const receipt = await readCapturedWorkerReceipt(identity);
51
+ if (receipt) reservation.finalize({ state: "settled", identity, receipt });
52
+ }
53
+ }
@@ -1,6 +1,6 @@
1
1
  import { parseInherited } from "../src/kernel/approval.ts";
2
2
  import { DELEGATE_CAPABILITY } from "../src/kernel/delegate.ts";
3
- import { budgetFromEnv } from "../src/kernel/fanout.ts";
3
+ import { capacityForLifecycle } from "./session-capacity.ts";
4
4
  import { isEpisodeId } from "../src/kernel/episode-id.ts";
5
5
  import { WILDCARD } from "../src/kernel/pi-tools.ts";
6
6
  import {
@@ -54,7 +54,10 @@ export function reconcileSessionEnvironment(
54
54
  ? lifecycle.episodeId
55
55
  : session.episodeId;
56
56
  lifecycle.episodeId = session.episodeId;
57
- session.fanoutBudget = budgetFromEnv(environment[ENV_FANOUT]);
57
+ const capacityState = capacityForLifecycle(lifecycle, environment[ENV_FANOUT]);
58
+ session.capacity = capacityState.allocator;
59
+ session.capacityRefusal = capacityState.refusal;
60
+ session.fanoutBudget = session.capacity.total;
58
61
  session.mayDelegate =
59
62
  !session.governed || session.inherited.includes(DELEGATE_CAPABILITY) || session.inherited.includes(WILDCARD);
60
63
  session.allowUnresolvedModels = environment[ENV_ALLOW_UNRESOLVED_MODELS] === "1";