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
@@ -25,7 +25,8 @@ import { homedir } from "node:os";
25
25
  import { join, resolve } from "node:path";
26
26
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
27
27
  import { WILDCARD } from "../src/kernel/pi-tools.ts";
28
- import { buildCatalog } from "../src/kernel/catalog.ts";
28
+ import { makeCatalog, classifyToolNames } from "../src/kernel/catalog.ts";
29
+ import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
29
30
  import { ENV_WORKSPACE_REGISTRY } from "../src/kernel/workspace.ts";
30
31
  import { appendRecord, buildRecord } from "../src/governance/ledger.ts";
31
32
  import { openPaneCount, reapOpenPanesAsync } from "../src/executors/pane-reaper.ts";
@@ -35,13 +36,20 @@ import { registerDelegationTools } from "./delegation.ts";
35
36
  import { grantsCommand } from "./grants-command.ts";
36
37
  import { runInit } from "./init-command.ts";
37
38
  import { planWithApprovals } from "./run-delegation.ts";
38
- import { createGrantsSession, loadProjectDefinitions, reconcileAdvisorSession, type GrantsSession } from "./session.ts";
39
+ import {
40
+ createGrantsSession,
41
+ loadProjectDefinitions,
42
+ assertDiscoveryHealthy,
43
+ recordDiscoveryCleanupFailure,
44
+ retainDiscoveryCleanupFailure,
45
+ type GrantsSession,
46
+ } from "./session.ts";
39
47
  import { acceptWorkspaces } from "../src/governance/workspace-acceptance.ts";
40
48
  import { loadWorkspaceRegistry } from "../src/kernel/workspace.ts";
41
49
  import { bindReloadLifecycle } from "./reload-environment.ts";
42
50
  import { reconcileActiveDelegationTools } from "./delegation-activation.ts";
43
51
  import { resolveExecutor } from "./executor-session.ts";
44
- import { reportSessionStart } from "./session-report.ts";
52
+ import { reportSelectedDefinitions, reportSessionStart, type SessionReportContext } from "./session-report.ts";
45
53
  import { SPAWN_TOOLS, tripwireReason } from "./tripwire.ts";
46
54
  import { reportGrantStoreRefusal } from "./grant-store-refusal.ts";
47
55
  import {
@@ -58,7 +66,8 @@ import { legacyProjectLedgerPath, projectLedgerPath } from "../src/kernel/projec
58
66
  import { resolveDefinitionRuntime } from "./definition-runtime.ts";
59
67
  import { changeSessionModels, renderSessionModels } from "./session-model-prompt.ts";
60
68
  import { ensureDashboardSessionServer } from "./dashboard-session-server.ts";
61
- export default function (pi: ExtensionAPI) {
69
+ /** Optional trusted session injection is composition-only, never provider payload input. */
70
+ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
62
71
  // The path pi loads as the extension, so a child granted `tool:delegate` can be started with `-e <this>`.
63
72
  const extensionPath = (() => {
64
73
  try {
@@ -68,13 +77,34 @@ export default function (pi: ExtensionAPI) {
68
77
  }
69
78
  })();
70
79
  const observerExtensionPath = fileURLToPath(new URL("./activity-timeline.ts", import.meta.url));
71
- const session = createGrantsSession(extensionPath, undefined, observerExtensionPath);
80
+ const session = trustedSession ?? createGrantsSession(extensionPath, undefined, observerExtensionPath);
81
+ session.currentThinking = typeof pi.getThinkingLevel === "function" ? () => pi.getThinkingLevel() : undefined;
72
82
  registerActivityTimeline(pi, session);
73
83
  const dashboardPluginRoot = fileURLToPath(new URL("../herdr-plugin/", import.meta.url));
74
84
  const dashboardPaths = defaultDashboardPaths(agentDir());
75
85
  // Definitions are registered only after owner-bound session_start. Until then there is no delegation
76
86
  // dispatch surface; afterwards this callback refreshes their model-facing spawnable-definition text.
77
87
  const delegation = { refreshSpawnable: () => {} };
88
+ // Supported public discovery is ready at the first real request/tool execution, not session_start.
89
+ let selectedReady: Promise<void> | undefined;
90
+ let selectedReportContext: SessionReportContext | undefined;
91
+ if (typeof pi.getCommands === "function")
92
+ session.ensureDefinitions = () =>
93
+ (selectedReady ??= (async () => {
94
+ const lifecycle = session.reloadLifecycle;
95
+ await loadProjectDefinitions(session, session.cwd, lifecycle, pi.getCommands());
96
+ assertDiscoveryHealthy(session, lifecycle);
97
+ delegation.refreshSpawnable();
98
+ if (selectedReportContext)
99
+ try {
100
+ await reportSelectedDefinitions(session, selectedReportContext);
101
+ } catch (error) {
102
+ console.error("grants: selected resource report unavailable", error);
103
+ }
104
+ })());
105
+ pi.on("before_agent_start", async () => {
106
+ await session.ensureDefinitions?.();
107
+ });
78
108
  pi.on("session_start", async (_event, ctx) => {
79
109
  if (session.adoptedLegacyEnv.length > 0)
80
110
  ctx.ui.notify(legacyEnvironmentWarning(session.adoptedLegacyEnv), "warning");
@@ -84,6 +114,8 @@ export default function (pi: ExtensionAPI) {
84
114
  const reload = bindReloadLifecycle(owner, session.reloadLifecycle);
85
115
  session.reconcileEnvironment(reload.environment, reload.lifecycle);
86
116
  session.ownerBound = true;
117
+ selectedReady = undefined;
118
+ selectedReportContext = ctx;
87
119
  // ADR-0078: the parent's own session, for a granted `pruned` or `fork` handoff. Read-only, and only ever
88
120
  // read after the mode has survived the gate.
89
121
  const manager = owner as Partial<import("./context-staging.ts").ParentSession>;
@@ -96,7 +128,6 @@ export default function (pi: ExtensionAPI) {
96
128
  // execution tree across workspaces. Resolve it once at the root's actual pi cwd; descendants inherit
97
129
  // this absolute identity verbatim, and resolve(abs) remains abs at every deeper session start.
98
130
  if (session.ledgerPath) session.ledgerPath = resolve(ctx.cwd, session.ledgerPath);
99
- reconcileAdvisorSession(session, reload.environment);
100
131
  try {
101
132
  // ADR-0076 PR 3d: a pre-format project ledger is imported once into the record envelope, bodies verbatim, each
102
133
  // record marked with its source line; the old file is left untouched. Happens before any spawn can append.
@@ -105,6 +136,7 @@ export default function (pi: ExtensionAPI) {
105
136
  // continues against the new ledger path (an append there still fails closed if that path is unwritable).
106
137
  try {
107
138
  const imported = await importLegacyLedger(legacyProjectLedgerPath(ctx.cwd), projectLedgerPath(ctx.cwd));
139
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
108
140
  if (imported.imported > 0 || imported.stoppedAt !== null)
109
141
  ctx.ui.notify(
110
142
  `grants: imported ${imported.imported} record(s) from ${legacyProjectLedgerPath(ctx.cwd)} into ${projectLedgerPath(ctx.cwd)}` +
@@ -115,6 +147,7 @@ export default function (pi: ExtensionAPI) {
115
147
  imported.stoppedAt !== null ? "warning" : "info",
116
148
  );
117
149
  } catch (error) {
150
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
118
151
  ctx.ui.notify(`grants: legacy ledger import failed: ${String(error)}`, "error");
119
152
  }
120
153
  }
@@ -139,11 +172,13 @@ export default function (pi: ExtensionAPI) {
139
172
  try {
140
173
  await reportGrantStoreRefusal(session, ctx.ui);
141
174
  } catch (error) {
175
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
142
176
  ctx.ui.notify(
143
177
  `grants: stored-grant refusal reporting failed (${String(error)}); the session remains refused.`,
144
178
  "error",
145
179
  );
146
180
  }
181
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
147
182
  // Guarded together, and guarded at all because of R-60 rather than because either one throws today:
148
183
  // both loaders swallow their own filesystem errors, so this catch is currently unreachable. The point
149
184
  // is that "currently" is not a property anyone can rely on — `verifyLedger` was also harmless until
@@ -152,16 +187,28 @@ export default function (pi: ExtensionAPI) {
152
187
  // delegate by `tools:`, and an operator whose `agent:` spawns have all started failing deserves to
153
188
  // know it was the *scan* that broke rather than the grant.
154
189
  try {
155
- await loadProjectDefinitions(session, ctx.cwd);
190
+ await loadProjectDefinitions(session, ctx.cwd, reload.lifecycle, session.ensureDefinitions ? [] : undefined);
156
191
  } catch (error) {
157
- ctx.ui.notify(
158
- `grants: could not read this project's definitions or capability catalog ` +
159
- `(${error instanceof Error ? error.message : String(error)}) — no SKILL.md definition can be ` +
160
- `spawned this session, and delegation by tools: is unaffected. Governance itself is unaffected: ` +
161
- `it is enforced by --tools when a child is spawned.`,
162
- "error",
163
- );
192
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
193
+ if (error instanceof BoundedReadCleanupError) {
194
+ try {
195
+ ctx.ui.notify(
196
+ `grants: discovery cleanup failed — ${error.message}; delegation remains failed until a new owner.`,
197
+ "error",
198
+ );
199
+ } catch (diagnosticError) {
200
+ console.error("grants: startup cleanup diagnostic failed", error, diagnosticError);
201
+ }
202
+ } else
203
+ ctx.ui.notify(
204
+ `grants: could not read this project's definitions or capability catalog ` +
205
+ `(${error instanceof Error ? error.message : String(error)}) — no SKILL.md definition can be ` +
206
+ `spawned this session, and delegation by tools: is unaffected. Governance itself is unaffected: ` +
207
+ `it is enforced by --tools when a child is spawned.`,
208
+ "error",
209
+ );
164
210
  }
211
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
165
212
  // ADR-0031: probe once, HERE, before anything reports — so the disclosure line can name the executor,
166
213
  // and so a demanded-but-unreachable herdr is reported before the operator's first prompt rather than at
167
214
  // their first delegation. Its own try, because a failure here must not cancel the controls after it
@@ -169,6 +216,7 @@ export default function (pi: ExtensionAPI) {
169
216
  try {
170
217
  await resolveExecutor(session);
171
218
  } catch (error) {
219
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
172
220
  // Says what is actually true of the state left behind, which depends on the variable: with
173
221
  // `PI_DADDY_HERDR=1` the session holds a REFUSAL and every delegation fails, so telling the operator
174
222
  // "using the captured subprocess" would be the opposite of what happens. The old wording asserted the
@@ -184,10 +232,12 @@ export default function (pi: ExtensionAPI) {
184
232
  "warning",
185
233
  );
186
234
  }
235
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
187
236
  session.publishChildEnv();
188
237
  // The definitions now exist, so the `delegate` schema can finally name them (R-39). pi serialises a
189
238
  // tool's schema at REQUEST time, not at registration — measured — which is what makes this reach the
190
239
  // model at all.
240
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
191
241
  delegation.refreshSpawnable();
192
242
  // Everything an operator is TOLD at session start now lives in `./session-report.ts`. Lifted because
193
243
  // this file had reached 398 of the 400-line ceiling and ADR-0032 adds a control to it; the split is the
@@ -203,6 +253,7 @@ export default function (pi: ExtensionAPI) {
203
253
  try {
204
254
  await reportSessionStart(session, ctx);
205
255
  } catch (error) {
256
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
206
257
  ctx.ui.notify(
207
258
  // Names the CHECKS as well as the display lines. The first version listed only "the grant, the
208
259
  // executor and the spawnable definitions" — which understated it: a throw partway through the reporter
@@ -218,9 +269,12 @@ export default function (pi: ExtensionAPI) {
218
269
  "warning",
219
270
  );
220
271
  }
272
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
221
273
  // Explicit installation handshake. It only runs in a Herdr-hosted TUI, asks once, and the only branch
222
274
  // that links software is the literal human choice "Install and open".
223
275
  try {
276
+ const sessionEndpoint = await ensureDashboardSessionServer(session);
277
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
224
278
  await offerDashboardHandshake({
225
279
  mode: ctx.mode,
226
280
  env: process.env,
@@ -230,10 +284,12 @@ export default function (pi: ExtensionAPI) {
230
284
  pluginRoot: dashboardPluginRoot,
231
285
  preferencePath: dashboardPaths.preferencePath,
232
286
  paneStatePath: dashboardPaths.paneStatePath,
233
- sessionEndpoint: await ensureDashboardSessionServer(session),
287
+ sessionEndpoint,
234
288
  ui: ctx.ui,
235
289
  });
290
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
236
291
  } catch (error) {
292
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
237
293
  ctx.ui.notify(
238
294
  `grants: dashboard installation handshake could not run (${error instanceof Error ? error.message : String(error)}). ` +
239
295
  `Nothing was installed; /grants dashboard will report manual setup.`,
@@ -241,6 +297,7 @@ export default function (pi: ExtensionAPI) {
241
297
  );
242
298
  }
243
299
  } catch (error) {
300
+ if (session.reloadLifecycle !== reload.lifecycle) return undefined;
244
301
  // Rule 8 — fail closed, and be LOUD about it. Swallowing is still right: a startup fault must not
245
302
  // reach the agent loop. Swallowing SILENTLY is what let R-60 exist, and would let the next one exist
246
303
  // too, because every control added above this line is cancelled by any throw before it with no trace.
@@ -252,8 +309,8 @@ export default function (pi: ExtensionAPI) {
252
309
  `may refuse. The grant itself is unaffected: it is enforced by --tools when a child is spawned.`,
253
310
  "error",
254
311
  );
255
- } catch {
256
- /* a UI that cannot be notified is the one failure there is nowhere to report */
312
+ } catch (diagnosticError) {
313
+ console.error("grants: session-start diagnostic failed", error, diagnosticError);
257
314
  }
258
315
  }
259
316
  return undefined;
@@ -288,7 +345,7 @@ export default function (pi: ExtensionAPI) {
288
345
 
289
346
  // Observe this session's real tool surface once, and tighten the grant to it. Authoritative because
290
347
  // it is exactly what pi sent the model.
291
- pi.on("before_provider_request", (event) => {
348
+ pi.on("before_provider_request", (event, ctx) => {
292
349
  try {
293
350
  if (session.observed) return undefined;
294
351
  const names = observeToolNames(event.payload);
@@ -303,15 +360,17 @@ export default function (pi: ExtensionAPI) {
303
360
  // Refresh the catalog now that the real tool surface is known — this is the only moment extension
304
361
  // tools become visible, so it is the only moment `ext:`/`tool:` grants can be validated.
305
362
  // Keep the handle: a concurrent `delegate` awaits this rather than reading a half-built catalog.
306
- // The `catch` resolves to the CURRENT catalog rather than rejecting, so a failed refresh degrades
307
- // to the previous view instead of failing every delegation in the session.
308
- session.catalogReady = buildCatalog({
309
- cwd: session.cwd,
310
- observedTools: names,
311
- registryPath: process.env[ENV_WORKSPACE_REGISTRY],
312
- })
313
- .then((c) => (session.catalog = c))
314
- .catch(() => session.catalog);
363
+ const lifecycle = session.reloadLifecycle;
364
+ assertDiscoveryHealthy(session, lifecycle);
365
+ session.catalogReady = session.catalogReady.then((catalog) => {
366
+ assertDiscoveryHealthy(session, lifecycle);
367
+ const next = makeCatalog([...catalog.entries, ...classifyToolNames(names)], catalog.registryRefusal);
368
+ session.catalog = next;
369
+ return next;
370
+ });
371
+ void session.catalogReady.catch((error: unknown) => {
372
+ if (error instanceof BoundedReadCleanupError) recordDiscoveryCleanupFailure(session, error, lifecycle);
373
+ });
315
374
  } catch {
316
375
  /* never throw into the agent loop */
317
376
  }
@@ -370,8 +429,10 @@ export default function (pi: ExtensionAPI) {
370
429
  ...grantsCommand,
371
430
  // Built per invocation and spelled out field by field, rather than passing the session whole: what a
372
431
  // read-only diagnostic may see is a decision, and `GrantsCommandContext` is where it is recorded.
373
- handler: (args, ctx) =>
374
- grantsCommand.handler(args, {
432
+ handler: async (args, ctx) => {
433
+ // Named revocation validates the current definition. Emergency revoke-all needs no resource acquisition.
434
+ if (args.trim() !== "revoke --all") await session.ensureDefinitions?.();
435
+ return grantsCommand.handler(args, {
375
436
  ...ctx,
376
437
  grants: {
377
438
  cwd: session.cwd,
@@ -382,11 +443,6 @@ export default function (pi: ExtensionAPI) {
382
443
  depth: session.depth,
383
444
  maxDepth: session.maxDepth,
384
445
  ledgerPath: session.ledgerPath,
385
- advisor: {
386
- decider: session.advisorSession.deciderName,
387
- taskEgress: session.advisorSession.settings.taskEgress,
388
- ...(session.advisorSession.settings.refusal ? { refusal: session.advisorSession.settings.refusal } : {}),
389
- },
390
446
  ...(session.workspacePin ? { workspacePin: session.workspacePin } : {}),
391
447
  ...(session.workspaceAcceptance ? { unacceptedWorkspaces: session.workspaceAcceptance.unaccepted } : {}),
392
448
  // Injected rather than imported, so `/grants` takes its one writing ability visibly from here.
@@ -413,8 +469,11 @@ export default function (pi: ExtensionAPI) {
413
469
  // (R-38). Passing `ctx` here would let `/grants` raise a dialog, and passing `hasUI: false` would
414
470
  // make every gated definition report "no interactive user" instead of what actually blocks it.
415
471
  runInit: () =>
416
- runInit(session, ctx, async () => {
417
- await loadProjectDefinitions(session, ctx.cwd);
472
+ runInit(session, ctx, async (lifecycle) => {
473
+ assertDiscoveryHealthy(session, lifecycle);
474
+ if (session.ensureDefinitions) await session.ensureDefinitions();
475
+ else await loadProjectDefinitions(session, ctx.cwd, lifecycle);
476
+ assertDiscoveryHealthy(session, lifecycle);
418
477
  delegation.refreshSpawnable();
419
478
  }),
420
479
  openDashboard: async () =>
@@ -434,7 +493,15 @@ export default function (pi: ExtensionAPI) {
434
493
  input(title: string, placeholder?: string): Promise<string | undefined>;
435
494
  notify(message: string, type?: "info" | "warning" | "error"): void;
436
495
  }) => {
437
- await changeSessionModels(session, { hasUI: ctx.hasUI, ...ui });
496
+ const lifecycle = session.reloadLifecycle;
497
+ await changeSessionModels(
498
+ session,
499
+ { hasUI: ctx.hasUI, ...ui },
500
+ {
501
+ assertHealthy: () => assertDiscoveryHealthy(session, lifecycle),
502
+ onReadCleanup: (error) => retainDiscoveryCleanupFailure(session, error, lifecycle),
503
+ },
504
+ );
438
505
  return renderSessionModels(session);
439
506
  },
440
507
  runtimeFor: (name: string) =>
@@ -446,6 +513,7 @@ export default function (pi: ExtensionAPI) {
446
513
  piModel: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
447
514
  }),
448
515
  },
449
- }),
516
+ });
517
+ },
450
518
  });
451
519
  }
@@ -13,7 +13,9 @@ import { PI_PROJECT_DIR } from "../src/kernel/project-paths.ts";
13
13
  import { registeredWorkspaceIds } from "../src/kernel/workspace.ts";
14
14
  import { grantStorePath, projectLedgerPath, saveGrant } from "../src/governance/grant-store.ts";
15
15
  import { expandSubsumed, SUBSUMPTION, type Capability } from "../src/kernel/resolve.ts";
16
- import type { GrantsSession } from "./session.ts";
16
+ import { assertDiscoveryHealthy, retainDiscoveryCleanupFailure, type GrantsSession } from "./session.ts";
17
+ import type { ReloadLifecycle } from "./reload-environment.ts";
18
+ import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
17
19
 
18
20
  /**
19
21
  * `/grants init` — scaffold, ask about what is withheld, store grant + ledger consent, apply both now.
@@ -34,15 +36,40 @@ export async function runInit(
34
36
  /**
35
37
  * Reload definitions and the catalog, and re-describe the delegation tools.
36
38
  *
39
+ * Refresh selected definitions for the live grant. New Pi resource references still need explicit /reload.
37
40
  * **Without this the grant goes live and the definitions do not** — `session.definitions` and the catalog
38
41
  * are read at `session_start`, which is before `init` wrote a single file, so a session would hold
39
42
  * `agent:review` while believing no definition of that name exists. `/grants` showed `0 skill,
40
43
  * 0 agent-type` and no verdicts, and the model would have been told `Available: none` — R-39 exactly,
41
44
  * reintroduced by a feature whose whole selling point is "no restart". Found by running it.
42
45
  */
43
- refresh: () => Promise<void>,
46
+ refresh: (lifecycle: ReloadLifecycle) => Promise<void>,
44
47
  ): Promise<void> {
45
- const packages = await discoverSkillPackages(ctx.cwd);
48
+ const lifecycle = session.reloadLifecycle;
49
+ try {
50
+ await runOwnedInit(session, ctx, refresh, lifecycle);
51
+ } catch (error) {
52
+ if (error instanceof BoundedReadCleanupError) {
53
+ retainDiscoveryCleanupFailure(session, error, lifecycle);
54
+ try {
55
+ ctx.ui.notify(`grants: init cleanup failed — ${error.message}; init did not complete.`, "error");
56
+ } catch (diagnosticError) {
57
+ console.error("grants: init cleanup diagnostic failed", error, diagnosticError);
58
+ }
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ async function runOwnedInit(
65
+ session: GrantsSession,
66
+ ctx: any,
67
+ refresh: (lifecycle: ReloadLifecycle) => Promise<void>,
68
+ lifecycle: ReloadLifecycle,
69
+ ): Promise<void> {
70
+ assertDiscoveryHealthy(session, lifecycle);
71
+ const packages = await (session.discovery?.packages ?? discoverSkillPackages)(ctx.cwd);
72
+ assertDiscoveryHealthy(session, lifecycle);
46
73
  if (packages.length === 0) {
47
74
  ctx.ui.notify(
48
75
  "grants: no enabled configured skills or unregistered npm skill packages found. Install and enable one — e.g. " +
@@ -52,14 +79,18 @@ export async function runInit(
52
79
  return;
53
80
  }
54
81
 
55
- const plan = planInit(
56
- packages,
57
- ctx.cwd,
58
- await registeredWorkspaceIds(undefined, (reason) =>
59
- ctx.ui.notify(`grants: workspace registry unreadable, scaffolding no workspaces — ${reason}`, "warning"),
60
- ),
82
+ const workspaces = await registeredWorkspaceIds(
83
+ undefined,
84
+ (reason) => {
85
+ assertDiscoveryHealthy(session, lifecycle);
86
+ ctx.ui.notify(`grants: workspace registry unreadable, scaffolding no workspaces — ${reason}`, "warning");
87
+ },
88
+ session.discovery?.registry,
61
89
  );
90
+ assertDiscoveryHealthy(session, lifecycle);
91
+ const plan = planInit(packages, ctx.cwd, workspaces);
62
92
  const outcome = await applyInit(plan);
93
+ assertDiscoveryHealthy(session, lifecycle);
63
94
  const lines = [
64
95
  `grants: ${plan.skills.length} definition(s) from ${packages.map((p) => `${p.name}@${p.version}`).join(", ")}`,
65
96
  ` using ${plan.skills.filter((s) => s.referenced).length} enabled definition(s) in place; no copies`,
@@ -77,6 +108,7 @@ export async function runInit(
77
108
  const alreadyConferred: string[] = [];
78
109
 
79
110
  for (const [capability, neededBy] of plan.withheldCapabilities) {
111
+ assertDiscoveryHealthy(session, lifecycle);
80
112
  // **Do not ask a question whose answer cannot matter.** `tool:bash` subsumes `write`, `edit` and
81
113
  // `edit-diff` (`SUBSUMPTION`, `src/kernel/resolve.ts`), so once bash is granted those are already conferred.
82
114
  // The first version asked anyway: an operator could answer *no* to `tool:write`, watch `/grants` allow
@@ -114,6 +146,7 @@ export async function runInit(
114
146
  : "it is not gated, so no dialog at spawn time"),
115
147
  ["No", "Yes"],
116
148
  );
149
+ assertDiscoveryHealthy(session, lifecycle);
117
150
  if (answer === "Yes") {
118
151
  grant.add(capability);
119
152
  granted.push(capability);
@@ -125,9 +158,11 @@ export async function runInit(
125
158
  }
126
159
  }
127
160
 
161
+ assertDiscoveryHealthy(session, lifecycle);
128
162
  const finalGrant = [...grant].sort();
129
163
  const ledger = projectLedgerPath(ctx.cwd);
130
164
  const saved = await saveGrant(ctx.cwd, finalGrant, { projectLedger: true });
165
+ assertDiscoveryHealthy(session, lifecycle);
131
166
  if (saved !== "saved") {
132
167
  lines.push(
133
168
  ` NOT STORED — ${saved === "busy" ? "another session holds the grant store" : "the store could not be written"}. ` +
@@ -142,7 +177,8 @@ export async function runInit(
142
177
  session.adoptGrant(finalGrant, ledger);
143
178
  // Order matters: the grant first, then the reload, because `refreshSpawnable` filters the definitions it
144
179
  // advertises through `maySpawnDefinition` against the grant the session now holds.
145
- await refresh();
180
+ await refresh(lifecycle);
181
+ assertDiscoveryHealthy(session, lifecycle);
146
182
 
147
183
  lines.push(
148
184
  ` stored at ${grantStorePath(ctx.cwd)} — outside this project, so no child can rewrite it`,
@@ -164,12 +200,16 @@ export async function runInit(
164
200
  `${plan.settingsPath}. Which worktree a child starts in is not something a package can declare for you.`,
165
201
  );
166
202
  }
167
- if ((await settingsIgnoredByGit(plan.settingsPath)) === true)
203
+ const settingsIgnored = await settingsIgnoredByGit(plan.settingsPath);
204
+ assertDiscoveryHealthy(session, lifecycle);
205
+ if (settingsIgnored === true)
168
206
  lines.push(
169
207
  ` NOTE: git ignores ${plan.settingsPath} (your root .gitignore covers ${PI_PROJECT_DIR}/), so the reviewable record ` +
170
208
  `cannot be committed until you add: ${GITIGNORE_REINCLUDE_LINES.join(" ")}`,
171
209
  );
172
- lines.push(` live now (${finalGrant.length} capabilities) — no restart. /grants shows the verdicts.`);
210
+ lines.push(` grant live now (${finalGrant.length} capabilities). /grants shows current selected definitions.`);
211
+ if (session.ensureDefinitions)
212
+ lines.push(" Newly added skill resources require /reload before selection; the current snapshot stays pinned.");
173
213
  ctx.ui.notify(lines.join("\n"), "info");
174
214
  }
175
215
 
@@ -1,8 +1,12 @@
1
+ import type { OwnerCapacity } from "./session-capacity.ts";
1
2
  import { GRANT_ENV_KEYS } from "../src/kernel/propagation.ts";
3
+ import type { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
2
4
 
3
5
  /** The root baseline and latest child publication for one real Pi session owner. */
4
6
  export interface ReloadLifecycle {
5
7
  root: Record<string, string | undefined>;
8
+ /** Never replaced for the same owner, even when an explicit root environment changes. */
9
+ capacity?: OwnerCapacity;
6
10
  published?: Record<string, string | undefined>;
7
11
  activityRootId?: string;
8
12
  episodeId?: string;
@@ -20,6 +24,12 @@ export interface ReloadLifecycle {
20
24
  * than its child publication", so it is where settled-ness belongs.
21
25
  */
22
26
  workspacePin?: ReadonlyMap<string, string>;
27
+ /** Failed pin acquisition stays failed for this owner even after explicit physical cleanup or reload. */
28
+ workspacePinFailure?: BoundedReadCleanupError;
29
+ /** One charged acquisition per actual owner, published before trusted loader callbacks. */
30
+ workspacePinAcquisition?: Promise<ReadonlyMap<string, string>>;
31
+ /** Catalog failure is terminal for this lifecycle too, not merely one extension object. */
32
+ discoveryCleanupFailure?: BoundedReadCleanupError;
23
33
  }
24
34
  type SessionOwner = object;
25
35
 
@@ -62,7 +72,7 @@ export function bindReloadLifecycle(
62
72
  environment: NodeJS.ProcessEnv;
63
73
  } {
64
74
  const holder = state();
65
- const existing = holder.owners.get(owner);
75
+ let existing = holder.owners.get(owner);
66
76
  if (!existing) {
67
77
  holder.owners.set(owner, provisional);
68
78
  return { lifecycle: provisional, environment: withRoot(provisional.root) };
@@ -82,7 +92,16 @@ export function bindReloadLifecycle(
82
92
  // silently ignored for the pin, so a session reconciled to depth 1 — believing itself a descendant —
83
93
  // kept a root-minted pin naming a workspace the replacement root never granted. Deleting it makes the
84
94
  // next `establishRootPin` re-settle from the new root, which for a descendant means minting nothing.
85
- if (!same(current, existing.root)) delete existing.workspacePin;
95
+ if (!same(current, existing.root)) {
96
+ // Pending old-root operations keep THEIR original owner; they cannot publish/fault this replacement.
97
+ existing = {
98
+ root: current,
99
+ activityRootId: existing.activityRootId,
100
+ episodeId: existing.episodeId,
101
+ capacity: existing.capacity,
102
+ };
103
+ holder.owners.set(owner, existing);
104
+ }
86
105
  existing.root = current;
87
106
  }
88
107
  return { lifecycle: existing, environment: withRoot(existing.root) };
@@ -0,0 +1,7 @@
1
+ /** Closed compatibility list. Preparation precedes Pi schema validation; active arguments still validate. */
2
+ export function withoutRetiredDelegationArguments<T>(args: unknown): T {
3
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return args as T;
4
+ const clean = { ...(args as Record<string, unknown>) };
5
+ delete clean.episodeCostCeiling;
6
+ return clean as T;
7
+ }