pi-daddy 0.35.0 → 0.37.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 (41) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +3 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/kernel/bounded-read.d.ts +57 -0
  6. package/dist/kernel/bounded-read.d.ts.map +1 -0
  7. package/dist/kernel/bounded-read.js +100 -0
  8. package/dist/kernel/bounded-read.js.map +1 -0
  9. package/dist/kernel/catalog.d.ts +12 -1
  10. package/dist/kernel/catalog.d.ts.map +1 -1
  11. package/dist/kernel/catalog.js +12 -5
  12. package/dist/kernel/catalog.js.map +1 -1
  13. package/dist/kernel/context-handoff.d.ts +71 -1
  14. package/dist/kernel/context-handoff.d.ts.map +1 -1
  15. package/dist/kernel/context-handoff.js +65 -6
  16. package/dist/kernel/context-handoff.js.map +1 -1
  17. package/dist/kernel/definitions.d.ts +19 -1
  18. package/dist/kernel/definitions.d.ts.map +1 -1
  19. package/dist/kernel/definitions.js +34 -9
  20. package/dist/kernel/definitions.js.map +1 -1
  21. package/dist/kernel/skill-packages.d.ts.map +1 -1
  22. package/dist/kernel/skill-packages.js +20 -9
  23. package/dist/kernel/skill-packages.js.map +1 -1
  24. package/dist/kernel/workspace.d.ts +7 -1
  25. package/dist/kernel/workspace.d.ts.map +1 -1
  26. package/dist/kernel/workspace.js +39 -81
  27. package/dist/kernel/workspace.js.map +1 -1
  28. package/extensions/context-staging.ts +32 -5
  29. package/extensions/grants-command.ts +7 -0
  30. package/extensions/init-command.ts +7 -1
  31. package/extensions/pruning-advice.ts +20 -8
  32. package/extensions/session-report.ts +15 -0
  33. package/extensions/session.ts +14 -1
  34. package/package.json +1 -1
  35. package/src/cli.ts +9 -1
  36. package/src/kernel/bounded-read.ts +125 -0
  37. package/src/kernel/catalog.ts +42 -21
  38. package/src/kernel/context-handoff.ts +96 -6
  39. package/src/kernel/definitions.ts +38 -8
  40. package/src/kernel/skill-packages.ts +26 -9
  41. package/src/kernel/workspace.ts +43 -109
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,7 @@ import { randomUUID } from "node:crypto";
19
19
  import { closeSync, mkdirSync, openSync, readSync, realpathSync, rmSync, statSync } from "node:fs";
20
20
  import { isAbsolute, relative, resolve } from "node:path";
21
21
  import {
22
+ CONTEXT_RANK,
22
23
  fenceContext,
23
24
  selectPrunedTurns,
24
25
  type ContextRequest,
@@ -69,14 +70,21 @@ export function createHandoffStager(input: StagingInput) {
69
70
  return (granted: ContextRequest, options: { keepTurnIds?: readonly string[] } = {}): StagedHandoff => {
70
71
  if (granted.mode === "fork") return stageFork(input);
71
72
  const sections: ContextSection[] = [];
73
+ /** Which entries of `sections` are parent turns, so the record can count the ones that survived the cap. */
74
+ const turnSectionIndices: number[] = [];
72
75
  let keptTurns: number | undefined;
73
76
  let droppedTurns: number | undefined;
74
77
  let rule: string | undefined;
75
78
 
76
79
  if (granted.mode === "summary")
77
- sections.push({ label: "what your parent says you need to know", body: granted.summary ?? "" });
80
+ sections.push({
81
+ label: "what your parent says you need to know",
82
+ body: granted.summary ?? "",
83
+ keepRank: CONTEXT_RANK.summary,
84
+ });
78
85
  if (granted.mode === "files" || granted.mode === "pruned")
79
- for (const path of granted.files ?? []) sections.push(readSection(input.cwd, path));
86
+ for (const path of granted.files ?? [])
87
+ sections.push({ ...readSection(input.cwd, path), keepRank: CONTEXT_RANK.file });
80
88
  if (granted.mode === "pruned") {
81
89
  const all = parentTurns(input.parentSession);
82
90
  const selection = selectPrunedTurns(all, {
@@ -93,20 +101,39 @@ export function createHandoffStager(input: StagingInput) {
93
101
  keptTurns = chosen.length;
94
102
  droppedTurns = all.length - chosen.length;
95
103
  rule = options.keepTurnIds === undefined ? selection.rule : `${selection.rule}+advice`;
96
- for (const turn of chosen) sections.push({ label: `parent turn ${turn.id}`, body: turn.text });
104
+ // Two bands, then position within the band. The probe measured array-order filling losing exactly the
105
+ // turns nearest the task; review then measured the first fix losing the file-matched turns instead,
106
+ // because those sit at the FRONT of `chosen` and a bare positional rank made oldest mean least.
107
+ const matched = new Set(selection.fileMatched);
108
+ chosen.forEach((turn, index) => {
109
+ turnSectionIndices.push(sections.length);
110
+ sections.push({
111
+ label: `parent turn ${turn.id}`,
112
+ body: turn.text,
113
+ keepRank: (matched.has(turn.id) ? CONTEXT_RANK.fileMatchedTurn : CONTEXT_RANK.recentTurn) + index,
114
+ });
115
+ });
97
116
  if (chosen.length === 0)
98
117
  sections.push({ label: "parent turns", body: "(no turn of your parent's session matched the selection)" });
99
118
  }
100
119
 
101
120
  const fenced = fenceContext(sections);
121
+ // **The record counts what CROSSED, which is what this module says it does.** These were counted before
122
+ // the budget ran, so a handoff could record `keptTurns: 21` having sent thirteen. Review measured it, and
123
+ // raising the default turn count moved the cap from binding in 13% of handoffs to 60% — so the wrong
124
+ // number became the usual number.
125
+ const crossed = new Set(fenced.keptIndices);
126
+ const turnsCrossed = turnSectionIndices.filter((index) => crossed.has(index)).length;
102
127
  return {
103
128
  contextPrompt: fenced.text,
104
129
  record: {
105
130
  mode: granted.mode,
106
- sections: sections.length,
131
+ sections: fenced.keptIndices.length,
107
132
  bytes: Buffer.byteLength(fenced.text),
108
133
  truncatedBytes: fenced.truncatedBytes,
109
- ...(keptTurns !== undefined ? { keptTurns, droppedTurns, rule } : {}),
134
+ ...(keptTurns !== undefined
135
+ ? { keptTurns: turnsCrossed, droppedTurns: droppedTurns! + (keptTurns - turnsCrossed), rule }
136
+ : {}),
110
137
  },
111
138
  };
112
139
  };
@@ -381,6 +381,13 @@ export const grantsCommand = {
381
381
  ...(catalog.byKind("workspace").length > 0
382
382
  ? [` routable ${catalog.byKind("workspace").join(", ")} — held ones only are usable (ADR-0035)`]
383
383
  : []),
384
+ // Rule 8's loud half again. The catalog fails soft on an unreadable registry, which is right, and used
385
+ // to discard the reason with it, which was not: one malformed entry removed every workspace from this
386
+ // screen, from the catalog and from `init`, and an operator saw an empty list identical to the one a
387
+ // machine with no registry shows. Placed under `routable` because it explains that line's absence.
388
+ ...(catalog.registryRefusal
389
+ ? [` routable (none — workspace registry unreadable) ${catalog.registryRefusal}`]
390
+ : []),
384
391
  ];
385
392
  // Runs the REAL planner AND the real approval step over each definition, so this listing cannot
386
393
  // disagree with what a spawn would do — the R-28 lesson, kept structural rather than remembered.
@@ -52,7 +52,13 @@ export async function runInit(
52
52
  return;
53
53
  }
54
54
 
55
- const plan = planInit(packages, ctx.cwd, await registeredWorkspaceIds());
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
+ ),
61
+ );
56
62
  const outcome = await applyInit(plan);
57
63
  const lines = [
58
64
  `grants: ${plan.skills.length} definition(s) from ${packages.map((p) => `${p.name}@${p.version}`).join(", ")}`,
@@ -22,7 +22,8 @@ export const PRUNING_PURPOSE = "handoff-pruning";
22
22
 
23
23
  /**
24
24
  * How many candidates may be judged. One question per turn, and a decision a human is waiting on should not carry
25
- * an unbounded number of them; beyond this the rule's own selection stands, which is the safe direction.
25
+ * an unbounded number of them. Beyond this the OLDEST candidates go unjudged and are kept, which is the safe
26
+ * direction: an unjudged turn is never dropped, so the advisor can still only narrow.
26
27
  */
27
28
  export const MAX_JUDGED_TURNS = 12;
28
29
 
@@ -43,11 +44,21 @@ export async function advisePruning(input: {
43
44
  ...(input.granted.turns !== undefined ? { turns: input.granted.turns } : {}),
44
45
  ...(input.granted.files !== undefined ? { files: input.granted.files } : {}),
45
46
  }).kept;
46
- // Nothing to narrow, or more than a bounded number to judge: the rule stands and no call is made.
47
- if (candidates.length < 2 || candidates.length > MAX_JUDGED_TURNS) return undefined;
47
+ if (candidates.length < 2) return undefined;
48
+ // **Judge the MOST RECENT candidates, and keep the rest unjudged.** This was `> MAX_JUDGED_TURNS` → give up
49
+ // entirely, which was fine while the rule offered six turns and became a silent feature death the moment
50
+ // `DEFAULT_CONTEXT_TURNS` rose to 20: every default request would have exceeded the bound, so the pruning
51
+ // decision point shipped in 0.35.0 would never have fired again. Found while raising the default, which is
52
+ // the only reason it was found at all.
53
+ //
54
+ // Judging a SUBSET is still only narrowing — an unjudged turn is kept, never dropped — so the attenuation
55
+ // property is untouched. The recent end is judged because that is where the rule's own recall is
56
+ // concentrated, and because a turn adjacent to the task is the one a wrong answer costs most.
57
+ const unjudged = candidates.slice(0, Math.max(0, candidates.length - MAX_JUDGED_TURNS));
58
+ const judged = candidates.slice(unjudged.length);
48
59
 
49
60
  const questions: Record<string, Question> = {};
50
- for (const [index, turn] of candidates.entries())
61
+ for (const [index, turn] of judged.entries())
51
62
  questions[`turn${index}`] = {
52
63
  kind: "noul",
53
64
  // Both bounded: the task is embedded once per candidate, so an unbounded task became a request twelve times
@@ -67,10 +78,11 @@ export async function advisePruning(input: {
67
78
  if (!advice) return undefined;
68
79
  // Every candidate or none. A response missing eleven of twelve answers would otherwise read as "drop eleven",
69
80
  // which is a narrowing nobody asked for rather than the "unrecognised response means no advice" contract.
70
- if (candidates.some((_, index) => advice.answers[`turn${index}`]?.kind !== "noul")) return undefined;
71
- const kept = candidates
72
- .filter((_, index) => (advice.answers[`turn${index}`] as { value: boolean }).value)
73
- .map((turn) => turn.id);
81
+ if (judged.some((_, index) => advice.answers[`turn${index}`]?.kind !== "noul")) return undefined;
82
+ const kept = [
83
+ ...unjudged.map((turn) => turn.id),
84
+ ...judged.filter((_, index) => (advice.answers[`turn${index}`] as { value: boolean }).value).map((turn) => turn.id),
85
+ ];
74
86
  // An advisor that drops everything is answering a different question from the one that was asked; the rule's
75
87
  // selection stands rather than handing a child a handoff with nothing in it.
76
88
  return kept.length === 0 ? undefined : kept;
@@ -57,6 +57,21 @@ export async function reportSessionStart(session: GrantsSession, ctx: SessionRep
57
57
  "warning",
58
58
  );
59
59
  }
60
+ // **Rule 8's loud half for the two reads that discover what exists.** Both of these used to fail soft and
61
+ // say nothing, and review established that session start — not `/grants` — is where every other such
62
+ // notice lives (a malformed bound above, ledger corruption and an executor refusal below). A reason an
63
+ // operator has to ask for is a reason most operators never see.
64
+ //
65
+ // The definitions one matters more than it looks: the 1 MiB bound is NEW behaviour, so a definition that
66
+ // loaded yesterday can be absent today, and without this line the only symptom is `unknown agent "x"` at
67
+ // the moment somebody tries to delegate to it.
68
+ for (const reason of session.definitionSkips)
69
+ ctx.ui.notify(`grants: a definition was not loaded — ${reason}`, "warning");
70
+ if (session.catalog.registryRefusal)
71
+ ctx.ui.notify(
72
+ `grants: workspace registry unreadable, no workspace is routable this session — ${session.catalog.registryRefusal}`,
73
+ "warning",
74
+ );
60
75
  // ADR-0014: a pre-0.6 in-workspace approvals file is IGNORED, not migrated — importing it would
61
76
  // import exactly the entries whose trustworthiness the move exists to remove. Say so, because an
62
77
  // operator whose approvals silently stopped applying deserves to know why.
@@ -186,6 +186,16 @@ export interface GrantsSession extends NativeSessionHost {
186
186
  observedTools: string[] | null;
187
187
  /** ADR-0016: `SKILL.md` definitions, keyed by name. The format this package spawns from now. */
188
188
  definitions: Map<string, SkillDefinition>;
189
+ /**
190
+ * Definitions discovery dropped, and why — reported at session start (ADR-0076, rule 8).
191
+ *
192
+ * Review found the bound shipped without this: `loadDefinitions` grew a `skipped` callback and NO
193
+ * production caller passed one, so an oversized or unreadable `SKILL.md` still vanished with nothing
194
+ * anywhere explaining it. That is worse than the `catch { continue }` it replaced, because the bound is
195
+ * new behaviour — a 2 MiB definition used to load. A capability that only a test can observe is not a
196
+ * capability.
197
+ */
198
+ definitionSkips: string[];
189
199
  catalog: Catalog;
190
200
  /**
191
201
  * The in-flight catalog build, so `delegate` can wait for it instead of racing it.
@@ -255,7 +265,9 @@ export interface GrantsSession extends NativeSessionHost {
255
265
  * about what loading means, so there is one.
256
266
  */
257
267
  export async function loadProjectDefinitions(session: GrantsSession, cwd: string): Promise<void> {
258
- session.definitions = await loadDefinitions(cwd);
268
+ const skips: string[] = [];
269
+ session.definitions = await loadDefinitions(cwd, (_path, reason) => skips.push(reason));
270
+ session.definitionSkips = skips;
259
271
  session.catalogReady = buildCatalog({
260
272
  cwd,
261
273
  observedTools: session.observedTools,
@@ -318,6 +330,7 @@ export function createGrantsSession(
318
330
  depth,
319
331
  maxDepth,
320
332
  malformedBounds: bounds.malformed,
333
+ definitionSkips: [],
321
334
  // ADR-0012: `bash` is gated by DEFAULT — but only in a governed session. An ungoverned one
322
335
  // (no PI_DADDY_GRANT) still blocks nothing, so "governance is opt-in" holds exactly where it always
323
336
  // did. Inside a session the operator already chose to govern, handing a child `bash` hands it an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-daddy",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Capability governance for pi sub-agents: spawn Agent Skills (SKILL.md) definitions whose allowed-tools becomes a grant that can only narrow going down a delegation tree, enforced by pi's own --tools allowlist, with an append-only ledger.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/cli.ts CHANGED
@@ -150,7 +150,15 @@ async function init(cwd: string, force: boolean): Promise<number> {
150
150
 
151
151
  let plan: InitPlan;
152
152
  try {
153
- plan = planInit(packages, cwd, await registeredWorkspaceIds());
153
+ // A registry that cannot be read scaffolds no workspace capabilities. That was silent, so the operator
154
+ // saw a plan with none and no way to tell it apart from having registered none (rule 8).
155
+ plan = planInit(
156
+ packages,
157
+ cwd,
158
+ await registeredWorkspaceIds(undefined, (reason) =>
159
+ console.error(`pi-daddy init: workspace registry unreadable, scaffolding none — ${reason}`),
160
+ ),
161
+ );
154
162
  } catch (error) {
155
163
  // R-78's backstop reaching the surface. Nothing is written: a grant that could mean something to a
156
164
  // shell is not a grant, and half-scaffolding a project would be worse than scaffolding none of it.
@@ -0,0 +1,125 @@
1
+ /**
2
+ * One bounded reader for the operator-authored files this package reads at SESSION START.
3
+ *
4
+ * **Why this is a module rather than a pattern to copy.** `loadWorkspaceRegistry` worked out the correct
5
+ * shape the expensive way — R-79 hung forever on a FIFO, a `stat`-then-`readFile` rewrite reintroduced the
6
+ * hang through a TOCTOU, and `AbortSignal.timeout` turned out never to interrupt a libuv read. That comment
7
+ * block ends with "One reader is why that cannot happen again", and then a second session-start read
8
+ * (`loadDefinitions`, reading each `SKILL.md`) went on using a bare `readFile`. The guards were not copied
9
+ * because copying them correctly is exactly what the registry's own history says nobody manages.
10
+ *
11
+ * What the shape buys, restated once here so neither caller has to:
12
+ *
13
+ * - `O_NONBLOCK` on the open. A FIFO blocks inside `open(2)` before any read starts, so a signal or a
14
+ * deadline checked between chunks can never rescue it; a non-blocking open returns instead.
15
+ * - every check against the HELD DESCRIPTOR. `stat` by name followed by a read by name is a TOCTOU, and the
16
+ * attacker is any process at the same uid — swapping a regular file for a FIFO between the two hangs the
17
+ * reader. `fstat` on a descriptor has no name left to re-resolve.
18
+ * - a deadline checked BETWEEN chunks, which is all an `AbortSignal` ever managed. Its honest limit: a
19
+ * stalled open or a single wedged read cannot be interrupted from in-process.
20
+ * - a size bound checked twice, before the read from `fstat` and again after, because a file can grow
21
+ * between the two. **The buffer is `maxBytes + 1` regardless of the file's size, and that is deliberate.**
22
+ * Review proposed sizing it from `fstat` instead; that would be faster and would silently break the second
23
+ * check, because a file whose reported size understates its content — anything under procfs, and any file
24
+ * that grows — would fill its small buffer exactly and come back as a successful truncated read. The cost
25
+ * was measured rather than assumed: 500 allocations of 1 MiB + 1 take 8.1ms for an RSS delta of 2.8 MB,
26
+ * because `allocUnsafe` never touches the pages.
27
+ *
28
+ * **The clock is injected** so the deadline is forced by a test rather than by a reviewer. Before this
29
+ * module the registry's deadline could be deleted outright and all 876 tests still passed (measured at
30
+ * `7096f78`), which is rule 7's "a test that cannot fail is worse than no test" with the production change
31
+ * named: delete the `now() > deadline` branch below and `bounded-read.test.ts` fails.
32
+ */
33
+ import { constants } from "node:fs";
34
+ import { open, type FileHandle } from "node:fs/promises";
35
+
36
+ export interface BoundedReadLimits {
37
+ /** Refuse anything at or over this, before allocating it. */
38
+ maxBytes: number;
39
+ /** How long the chunk loop may run before the read is a refusal rather than a wait. */
40
+ timeoutMs: number;
41
+ /** Injected only by tests; production passes nothing and gets the wall clock. */
42
+ now?: () => number;
43
+ }
44
+
45
+ /**
46
+ * Why a bounded read did not produce text.
47
+ *
48
+ * A discriminated reason rather than a thrown error, because the two callers disagree about what a failure
49
+ * MEANS — an unreadable registry is a governance refusal naming the file, while an unreadable `SKILL.md` is
50
+ * one definition that will not be offered — and a shared reader must not decide that for them.
51
+ */
52
+ export type BoundedReadFailure =
53
+ | { why: "unopenable"; detail: string }
54
+ | { why: "not-a-regular-file"; detail: string }
55
+ | { why: "too-large"; detail: string; size: number }
56
+ | { why: "grew-while-reading"; detail: string }
57
+ | { why: "timed-out"; detail: string }
58
+ | { why: "unreadable"; detail: string };
59
+
60
+ export type BoundedReadResult = { ok: true; text: string } | ({ ok: false } & BoundedReadFailure);
61
+ export type BoundedReadBytes = { ok: true; bytes: Buffer } | ({ ok: false } & BoundedReadFailure);
62
+
63
+ /**
64
+ * The same read, handing back raw bytes.
65
+ *
66
+ * `skill-packages.ts` needs the bytes rather than a string, because it asserts that a definition survives a
67
+ * UTF-8 round trip unchanged — a latin-1 byte that decodes to U+FFFD changes the file's digest, and that
68
+ * check is only possible against the original buffer. Decoding here and re-encoding there would defeat it.
69
+ */
70
+ export async function readBoundedFile(path: string, limits: BoundedReadLimits): Promise<BoundedReadResult> {
71
+ const read = await readBoundedBytes(path, limits);
72
+ return read.ok ? { ok: true, text: read.bytes.toString("utf8") } : read;
73
+ }
74
+
75
+ export async function readBoundedBytes(path: string, limits: BoundedReadLimits): Promise<BoundedReadBytes> {
76
+ const now = limits.now ?? Date.now;
77
+ let handle: FileHandle;
78
+ try {
79
+ handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
80
+ } catch (error) {
81
+ return { ok: false, why: "unopenable", detail: String(error) };
82
+ }
83
+ try {
84
+ const info = await handle.stat();
85
+ if (!info.isFile())
86
+ return {
87
+ ok: false,
88
+ why: "not-a-regular-file",
89
+ detail: `${path} is not a regular file — a FIFO, device or socket here would block rather than fail`,
90
+ };
91
+ if (info.size > limits.maxBytes)
92
+ return {
93
+ ok: false,
94
+ why: "too-large",
95
+ size: info.size,
96
+ detail: `${path} is ${info.size} bytes, over the ${limits.maxBytes} limit`,
97
+ };
98
+
99
+ const deadline = now() + limits.timeoutMs;
100
+ const buffer = Buffer.allocUnsafe(limits.maxBytes + 1);
101
+ let filled = 0;
102
+ while (filled < buffer.length) {
103
+ if (now() > deadline)
104
+ return {
105
+ ok: false,
106
+ why: "timed-out",
107
+ detail: `${path} did not finish reading within ${limits.timeoutMs}ms`,
108
+ };
109
+ const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
110
+ if (bytesRead === 0) break;
111
+ filled += bytesRead;
112
+ }
113
+ if (filled > limits.maxBytes)
114
+ return {
115
+ ok: false,
116
+ why: "grew-while-reading",
117
+ detail: `${path} exceeded the ${limits.maxBytes} limit while being read — it grew after its size was checked`,
118
+ };
119
+ return { ok: true, bytes: buffer.subarray(0, filled) };
120
+ } catch (error) {
121
+ return { ok: false, why: "unreadable", detail: String(error) };
122
+ } finally {
123
+ await handle.close().catch(() => {});
124
+ }
125
+ }
@@ -44,6 +44,17 @@ export interface Catalog {
44
44
  all: Capability[];
45
45
  byKind(kind: CapabilityKind): Capability[];
46
46
  has(capability: Capability): boolean;
47
+ /**
48
+ * Why the workspace registry contributed nothing, when one was named and could not be read.
49
+ *
50
+ * The catalog fails SOFT on a bad registry — a malformed one must not stop a session starting — and the
51
+ * refusal used to be discarded by `() => []` at the call below. That is rule 8's silent safe-mode: one
52
+ * malformed entry removed every workspace from `/grants`, from the catalog and from `init`, and produced
53
+ * no message anywhere, so an operator saw an empty list and could not tell it apart from having
54
+ * registered nothing. Failing soft is still right; failing soft and SILENTLY was not. `undefined` means
55
+ * no registry was named, or it read cleanly.
56
+ */
57
+ registryRefusal?: string;
47
58
  }
48
59
 
49
60
  /** Split observed tool names into pi built-ins and extension-provided tools. */
@@ -107,7 +118,7 @@ export function workspaceEntries(registry: WorkspaceRegistryFile, source?: strin
107
118
  }
108
119
 
109
120
  /** Assemble a catalog from parts. Pure, so it is testable without a filesystem. */
110
- export function makeCatalog(entries: CatalogEntry[]): Catalog {
121
+ export function makeCatalog(entries: CatalogEntry[], registryRefusal?: string): Catalog {
111
122
  const deduped = new Map<Capability, CatalogEntry>();
112
123
  for (const entry of entries) if (!deduped.has(entry.capability)) deduped.set(entry.capability, entry);
113
124
  const list = [...deduped.values()].sort((a, b) => a.capability.localeCompare(b.capability));
@@ -118,6 +129,7 @@ export function makeCatalog(entries: CatalogEntry[]): Catalog {
118
129
  all: ids,
119
130
  byKind: (kind) => list.filter((e) => e.kind === kind).map((e) => e.capability),
120
131
  has: (capability) => idSet.has(capability),
132
+ ...(registryRefusal === undefined ? {} : { registryRefusal }),
121
133
  };
122
134
  }
123
135
 
@@ -135,30 +147,39 @@ export async function buildCatalog(input: {
135
147
  // session from starting — `loadWorkspaceRegistry` throws a GovernanceRefusal naming the file, and that
136
148
  // refusal is the operator's signal at the point of USE, where routing actually depends on it. Swallowing
137
149
  // it there would be unsafe; swallowing it here costs a display list.
150
+ //
151
+ // **What the refusal is no longer allowed to do is vanish.** This handler was `() => []`, so the display
152
+ // list was lost AND the reason with it. The reason now rides on the catalog and `/grants` prints it.
138
153
  input.registryPath
139
154
  ? loadWorkspaceRegistry(input.registryPath).then(
140
- (r) => workspaceEntries(r, input.registryPath),
141
- () => [] as CatalogEntry[],
155
+ (r) => ({ entries: workspaceEntries(r, input.registryPath), refusal: undefined as string | undefined }),
156
+ (error: unknown) => ({
157
+ entries: [] as CatalogEntry[],
158
+ refusal: error instanceof Error ? error.message : String(error),
159
+ }),
142
160
  )
143
- : Promise.resolve([] as CatalogEntry[]),
144
- ]);
145
- return makeCatalog([
146
- // pi's built-ins are seeded unconditionally, because they are known statically and the catalog is
147
- // consulted BEFORE any provider request has happened — `/grants` runs at that point. Without this,
148
- // every capability looked "unknown" until the first model call, so the preview refused grants that
149
- // enforcement would have allowed: R-28's failure shape (a diagnostic disagreeing with the enforcer)
150
- // reappearing through a different door.
151
- //
152
- // The trade-off, stated plainly: in a session started with `--tools read`, this still lists `bash`
153
- // as an existing capability, so a delegation naming it passes the *unknown* check and is refused by
154
- // the *grant* check instead ("this session does not hold it"). That is the better error anyway, and
155
- // the grant check — not this catalog — is the authority. Nothing here grants anything.
156
- ...PI_BUILTIN_TOOLS.map((name) => ({ capability: `tool:${name}` as const, kind: "builtin" as const })),
157
- ...(input.observedTools ? classifyToolNames(input.observedTools) : []),
158
- ...skills,
159
- ...definitionEntries(definitions),
160
- ...workspaces,
161
+ : Promise.resolve({ entries: [] as CatalogEntry[], refusal: undefined as string | undefined }),
161
162
  ]);
163
+ return makeCatalog(
164
+ [
165
+ // pi's built-ins are seeded unconditionally, because they are known statically and the catalog is
166
+ // consulted BEFORE any provider request has happened — `/grants` runs at that point. Without this,
167
+ // every capability looked "unknown" until the first model call, so the preview refused grants that
168
+ // enforcement would have allowed: R-28's failure shape (a diagnostic disagreeing with the enforcer)
169
+ // reappearing through a different door.
170
+ //
171
+ // The trade-off, stated plainly: in a session started with `--tools read`, this still lists `bash`
172
+ // as an existing capability, so a delegation naming it passes the *unknown* check and is refused by
173
+ // the *grant* check instead ("this session does not hold it"). That is the better error anyway, and
174
+ // the grant check — not this catalog — is the authority. Nothing here grants anything.
175
+ ...PI_BUILTIN_TOOLS.map((name) => ({ capability: `tool:${name}` as const, kind: "builtin" as const })),
176
+ ...(input.observedTools ? classifyToolNames(input.observedTools) : []),
177
+ ...skills,
178
+ ...definitionEntries(definitions),
179
+ ...workspaces.entries,
180
+ ],
181
+ workspaces.refusal,
182
+ );
162
183
  }
163
184
 
164
185
  /**