pi-daddy 0.13.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 (105) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/LICENSE +21 -0
  3. package/README.md +584 -0
  4. package/dist/approval-prompt.d.ts +116 -0
  5. package/dist/approval-prompt.d.ts.map +1 -0
  6. package/dist/approval-prompt.js +124 -0
  7. package/dist/approval-prompt.js.map +1 -0
  8. package/dist/approval-store.d.ts +149 -0
  9. package/dist/approval-store.d.ts.map +1 -0
  10. package/dist/approval-store.js +311 -0
  11. package/dist/approval-store.js.map +1 -0
  12. package/dist/approval.d.ts +221 -0
  13. package/dist/approval.d.ts.map +1 -0
  14. package/dist/approval.js +244 -0
  15. package/dist/approval.js.map +1 -0
  16. package/dist/capabilities.d.ts +31 -0
  17. package/dist/capabilities.d.ts.map +1 -0
  18. package/dist/capabilities.js +45 -0
  19. package/dist/capabilities.js.map +1 -0
  20. package/dist/catalog.d.ts +81 -0
  21. package/dist/catalog.d.ts.map +1 -0
  22. package/dist/catalog.js +164 -0
  23. package/dist/catalog.js.map +1 -0
  24. package/dist/definitions.d.ts +101 -0
  25. package/dist/definitions.d.ts.map +1 -0
  26. package/dist/definitions.js +182 -0
  27. package/dist/definitions.js.map +1 -0
  28. package/dist/delegate.d.ts +133 -0
  29. package/dist/delegate.d.ts.map +1 -0
  30. package/dist/delegate.js +261 -0
  31. package/dist/delegate.js.map +1 -0
  32. package/dist/fanout.d.ts +62 -0
  33. package/dist/fanout.d.ts.map +1 -0
  34. package/dist/fanout.js +84 -0
  35. package/dist/fanout.js.map +1 -0
  36. package/dist/file-lock.d.ts +54 -0
  37. package/dist/file-lock.d.ts.map +1 -0
  38. package/dist/file-lock.js +142 -0
  39. package/dist/file-lock.js.map +1 -0
  40. package/dist/index.d.ts +7 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +7 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/ledger-report.d.ts +106 -0
  45. package/dist/ledger-report.d.ts.map +1 -0
  46. package/dist/ledger-report.js +157 -0
  47. package/dist/ledger-report.js.map +1 -0
  48. package/dist/ledger.d.ts +139 -0
  49. package/dist/ledger.d.ts.map +1 -0
  50. package/dist/ledger.js +94 -0
  51. package/dist/ledger.js.map +1 -0
  52. package/dist/pane-reaper.d.ts +49 -0
  53. package/dist/pane-reaper.d.ts.map +1 -0
  54. package/dist/pane-reaper.js +120 -0
  55. package/dist/pane-reaper.js.map +1 -0
  56. package/dist/pi-tools.d.ts +20 -0
  57. package/dist/pi-tools.d.ts.map +1 -0
  58. package/dist/pi-tools.js +21 -0
  59. package/dist/pi-tools.js.map +1 -0
  60. package/dist/propagation.d.ts +185 -0
  61. package/dist/propagation.d.ts.map +1 -0
  62. package/dist/propagation.js +255 -0
  63. package/dist/propagation.js.map +1 -0
  64. package/dist/resolve.d.ts +110 -0
  65. package/dist/resolve.d.ts.map +1 -0
  66. package/dist/resolve.js +181 -0
  67. package/dist/resolve.js.map +1 -0
  68. package/dist/run-child.d.ts +50 -0
  69. package/dist/run-child.d.ts.map +1 -0
  70. package/dist/run-child.js +109 -0
  71. package/dist/run-child.js.map +1 -0
  72. package/dist/run-herdr.d.ts +91 -0
  73. package/dist/run-herdr.d.ts.map +1 -0
  74. package/dist/run-herdr.js +306 -0
  75. package/dist/run-herdr.js.map +1 -0
  76. package/dist/spawn.d.ts +60 -0
  77. package/dist/spawn.d.ts.map +1 -0
  78. package/dist/spawn.js +116 -0
  79. package/dist/spawn.js.map +1 -0
  80. package/extensions/approvals.ts +285 -0
  81. package/extensions/delegation.ts +237 -0
  82. package/extensions/grants-command.ts +310 -0
  83. package/extensions/grants.ts +346 -0
  84. package/extensions/run-delegation.ts +270 -0
  85. package/extensions/session.ts +234 -0
  86. package/package.json +122 -0
  87. package/src/approval-prompt.ts +222 -0
  88. package/src/approval-store.ts +383 -0
  89. package/src/approval.ts +364 -0
  90. package/src/capabilities.ts +51 -0
  91. package/src/catalog.ts +186 -0
  92. package/src/definitions.ts +233 -0
  93. package/src/delegate.ts +383 -0
  94. package/src/fanout.ts +96 -0
  95. package/src/file-lock.ts +142 -0
  96. package/src/index.ts +59 -0
  97. package/src/ledger-report.ts +227 -0
  98. package/src/ledger.ts +214 -0
  99. package/src/pane-reaper.ts +132 -0
  100. package/src/pi-tools.ts +25 -0
  101. package/src/propagation.ts +308 -0
  102. package/src/resolve.ts +235 -0
  103. package/src/run-child.ts +149 -0
  104. package/src/run-herdr.ts +357 -0
  105. package/src/spawn.ts +156 -0
package/src/ledger.ts ADDED
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Append-only grant ledger.
3
+ *
4
+ * Exists because pi-fabric's persisted execution trace records `args: {}` — it captures *that* a child
5
+ * ran, not *what it was authorised to do* (docs/probes/pi-fabric-eval probe 5). Without this record
6
+ * you cannot answer "what was this sub-agent permitted to do?" after the fact, which is the whole
7
+ * point of a governance layer.
8
+ *
9
+ * `denied` is the field that earns the file: an agent repeatedly requesting capabilities it does not
10
+ * hold is an escalation attempt, and it is invisible without a record.
11
+ *
12
+ * PRIVACY: capability ids, counts, and identifiers only. Never prompts, tool arguments, or results.
13
+ *
14
+ * ADR-0018 makes the boundary explicit rather than leaving it to be inferred, because a record now carries
15
+ * something about the child's instructions. **`definitionDigest` is an identifier**: a SHA-256 of an
16
+ * operator-authored file already committed to a repository, which names a version without reproducing it.
17
+ * **The task is not recorded, anywhere, ever** — it is assembled by the model from the parent's context and
18
+ * can carry anything the parent could see, so a ledger holding it would be a secrets sink. That half of
19
+ * "what was this child told to do?" is out of the ledger by decision, not by omission.
20
+ */
21
+
22
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
23
+ import { withFileLock } from "./file-lock.ts";
24
+ import { dirname } from "node:path";
25
+ import type { Capability, ResolveResult } from "./resolve.ts";
26
+ import type { DefinitionDigest } from "./definitions.ts";
27
+ import { DELEGATE_SUBJECT } from "./approval.ts";
28
+ import type { ApprovalScope, ApprovalSource } from "./approval.ts";
29
+ import type { PromptOutcomeKind } from "./approval-prompt.ts";
30
+
31
+ export interface GrantRecord {
32
+ ts: string;
33
+ parentId: string;
34
+ childId: string;
35
+ depth: number;
36
+ agentType?: string;
37
+ requested: Capability[];
38
+ parentGrant: Capability[];
39
+ effective: Capability[];
40
+ denied: Capability[];
41
+ clipped: Capability[];
42
+ gatedBlocked: Capability[];
43
+ /** True when the spawn was refused outright rather than narrowed. */
44
+ blocked: boolean;
45
+ /** Present when the spawn was refused, or when resolution failed. */
46
+ reason?: string;
47
+ /**
48
+ * Gated capabilities satisfied for this spawn.
49
+ *
50
+ * The ledger used to have ONE flavour of no. It now has three, and they call for different responses:
51
+ * - `denied` non-empty -> an agent asked for more than it holds. ESCALATION ATTEMPT.
52
+ * - `humanDenied` -> a person was asked and said no. WORKING AS DESIGNED.
53
+ * - `gatedBlocked` with no source -> nobody was there to ask. A background run hit a gate; the fix is
54
+ * an operator pre-approving it, not an incident.
55
+ */
56
+ approved?: Capability[];
57
+ approvalSource?: ApprovalSource;
58
+ /**
59
+ * WHERE each approved capability's yes came from — one entry per capability (R-46).
60
+ *
61
+ * `approvalSource` above is a single scalar and was written for a set: gate `tool:bash` and `tool:write`,
62
+ * let a persisted entry cover `bash` while a human clicks *Allow once* for `write`, and the record read
63
+ * `approved: ["tool:bash","tool:write"], approvalSource: "prompt"` — **asserting a human was asked about
64
+ * `tool:bash`, which they were not.** The ledger's whole job is answering "did a human authorise this?",
65
+ * so over-claiming in that direction is the worst available failure.
66
+ *
67
+ * The scalar is kept and is now written **only when every approved capability shares one source**, so a
68
+ * reader of old and new lines alike can trust it; when sources differ it is omitted and this map carries
69
+ * the truth. Two fields, one of which is a safe summary of the other — not two competing answers.
70
+ */
71
+ approvalSources?: Record<Capability, ApprovalSource>;
72
+ /**
73
+ * How far each prompted capability's yes reaches (F5). Same shape and same reason as `approvalSources`:
74
+ * `approvalScope` below is a **derived summary**, emitted only when every prompted capability shares one
75
+ * scope. This field decides propagation — `inheritApprovals` drops `once` — so a scalar that described
76
+ * one capability while claiming to describe the set was not merely a reporting defect.
77
+ */
78
+ approvalScopes?: Record<Capability, ApprovalScope>;
79
+ /** Present only when the source was a live prompt, and only when one scope covers the whole set. */
80
+ approvalScope?: ApprovalScope;
81
+ /** A human was asked and declined. Distinct from `denied`, which is an escalation attempt. */
82
+ humanDenied?: boolean;
83
+ /**
84
+ * WHY a gate went unsatisfied, when the answer was not a yes.
85
+ *
86
+ * `PromptOutcomeKind` has five members and this record kept exactly one of them (`humanDenied`, from
87
+ * `declined`). So `no-ui`, `dismissed` (a timeout or an abort) and `error` produced **identical** records
88
+ * — `gatedBlocked` non-empty, no `approvalSource`, `blocked: true` — and the only thing separating them
89
+ * was free-text `reason`, written for a human at the call site. Given a failed run, *"was there an
90
+ * operator who timed out, or was there nobody to ask?"* was not answerable from any field, and the fix
91
+ * for each is different: one is a queue or a longer `PI_GRANTS_APPROVAL_TIMEOUT`, the other is an
92
+ * operator pre-approving.
93
+ *
94
+ * The discriminant was already computed and thrown away. ADR-0026 leans on this vocabulary being able to
95
+ * say *"nobody was there to ask"* and be believed, so it is recorded rather than inferred.
96
+ *
97
+ * **Privacy is unchanged**: this is a fixed five-member enum, not text — nothing model-authored, nothing
98
+ * a task could carry.
99
+ */
100
+ gateOutcome?: PromptOutcomeKind;
101
+ /**
102
+ * WHICH operator-authored instructions this child was given (ADR-0018).
103
+ *
104
+ * Identifies, never reproduces: matching digests prove two children ran the same text, and a digest that
105
+ * no longer matches the file proves the definition changed since. **It says nothing about whether those
106
+ * instructions were correct or whether the child obeyed them** — it identifies text, it does not evaluate
107
+ * it. Absent for a `tools:`-style delegation, which has no definition.
108
+ */
109
+ definitionDigest?: DefinitionDigest;
110
+ }
111
+
112
+ export interface LedgerOptions {
113
+ /** Path to the JSONL file. Parent directories are created on demand. */
114
+ path: string;
115
+ /**
116
+ * When true, a ledger write failure throws instead of being swallowed.
117
+ *
118
+ * Default is `true` and that is deliberate: for a security control, an unrecorded grant should fail
119
+ * closed. Set false only where the ledger is advisory.
120
+ */
121
+ strict?: boolean;
122
+ }
123
+
124
+ export function buildRecord(args: {
125
+ parentId: string;
126
+ childId: string;
127
+ depth: number;
128
+ agentType?: string;
129
+ requested: Capability[];
130
+ parentGrant: Capability[];
131
+ result: ResolveResult;
132
+ blocked: boolean;
133
+ reason?: string;
134
+ approved?: Capability[];
135
+ approvalSources?: Record<Capability, ApprovalSource>;
136
+ approvalScopes?: Record<Capability, ApprovalScope>;
137
+ humanDenied?: boolean;
138
+ gateOutcome?: PromptOutcomeKind;
139
+ definitionDigest?: DefinitionDigest;
140
+ now: Date;
141
+ }): GrantRecord {
142
+ // R-46: the scalar is a SUMMARY, emitted only when it cannot mislead. `buildRecord` derives it rather
143
+ // than accepting it, so a call site cannot supply one that disagrees with the map beside it.
144
+ const sources = args.approvalSources ?? {};
145
+ const distinct = [...new Set(Object.values(sources))];
146
+ const scopes = args.approvalScopes ?? {};
147
+ const distinctScopes = [...new Set(Object.values(scopes))];
148
+ return {
149
+ ts: args.now.toISOString(),
150
+ parentId: args.parentId,
151
+ childId: args.childId,
152
+ depth: args.depth,
153
+ agentType: args.agentType,
154
+ requested: args.requested,
155
+ parentGrant: args.parentGrant,
156
+ effective: args.result.effective,
157
+ denied: args.result.denied,
158
+ clipped: args.result.clipped,
159
+ gatedBlocked: args.result.gatedBlocked,
160
+ blocked: args.blocked,
161
+ reason: args.reason,
162
+ ...(args.approved && args.approved.length > 0 ? { approved: args.approved } : {}),
163
+ ...(distinct.length === 1 ? { approvalSource: distinct[0] } : {}),
164
+ ...(Object.keys(sources).length > 0 ? { approvalSources: sources } : {}),
165
+ ...(distinctScopes.length === 1 ? { approvalScope: distinctScopes[0] } : {}),
166
+ ...(Object.keys(scopes).length > 0 ? { approvalScopes: scopes } : {}),
167
+ ...(args.humanDenied ? { humanDenied: true } : {}),
168
+ // Written whenever a gate was reached and not satisfied by a yes. `granted` is omitted deliberately —
169
+ // an approved spawn already says so through `approvalSources`, and a field that appears on every record
170
+ // stops being a signal.
171
+ ...(args.gateOutcome && args.gateOutcome !== "granted" ? { gateOutcome: args.gateOutcome } : {}),
172
+ ...(args.definitionDigest ? { definitionDigest: args.definitionDigest } : {}),
173
+ };
174
+ }
175
+
176
+ // R-49: the lock moved to `src/file-lock.ts` so the approvals store could use the SAME one rather than
177
+ // grow a second copy. Re-exported because `./ledger` is a published subpath and these were part of it.
178
+ export { LOCK_TIMEOUT_MS, STALE_LOCK_MS } from "./file-lock.ts";
179
+
180
+ // The reading half, split out under the file-size guard and re-exported so `./ledger` is unchanged for
181
+ // anyone importing it. See `ledger-report.ts` for why the seam is where it is.
182
+ export { verifyLedger, type LedgerReport } from "./ledger-report.ts";
183
+
184
+ /**
185
+ * Serialise appends across processes.
186
+ *
187
+ * **Why the ledger needs it.** For most of this package's life cardinality was bounded to one by `delegate`
188
+ * being blocking, so there was never a second writer. Fan-out removes that: `ENV_LEDGER` propagates to
189
+ * children, so a subtree can have many processes appending to one file.
190
+ *
191
+ * A ledger write that cannot take the lock **fails the delegation closed** — see `appendRecord`'s `strict`
192
+ * — because a child running with granted capabilities and no audit line is what the ledger exists to
193
+ * prevent. That is the opposite of what the approvals store does with the same lock, and deliberately so.
194
+ */
195
+ const withLedgerLock = <T>(path: string, write: () => Promise<T>): Promise<T> =>
196
+ withFileLock(path, "grant ledger", write);
197
+
198
+ export async function appendRecord(options: LedgerOptions, record: GrantRecord): Promise<void> {
199
+ const line = `${JSON.stringify(record)}\n`;
200
+ try {
201
+ await mkdir(dirname(options.path), { recursive: true });
202
+ // O_APPEND alone is not enough once several processes write to one ledger — see `withLedgerLock`.
203
+ await withLedgerLock(options.path, () => appendFile(options.path, line, { encoding: "utf8", flag: "a" }));
204
+ } catch (error) {
205
+ if (options.strict ?? true) {
206
+ throw new Error(`grant ledger write failed (failing closed): ${String(error)}`);
207
+ }
208
+ }
209
+ }
210
+
211
+ /** True when this record shows an agent asking for more than it holds. */
212
+ export function isEscalationAttempt(record: GrantRecord): boolean {
213
+ return record.denied.length > 0;
214
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Close herdr panes this process opened but never got to close.
3
+ *
4
+ * `runHerdrPane` closes its pane in a `finally`, which covers a thrown error and a timeout — **not the
5
+ * process being killed**. A pi session interrupted mid-fan-out left one pane per in-flight child, and
6
+ * `docs/probes/g16-herdr` records that an orphaned pane is not trivially closable afterwards.
7
+ *
8
+ * **Registered on `exit` only, deliberately — not on SIGINT or SIGTERM.** That is the part worth reading,
9
+ * because the obvious fix is the dangerous one. Adding a signal listener *suppresses Node's default
10
+ * termination*, so a library that adds one takes over an application-level decision it has no standing to
11
+ * make: pi uses SIGINT to interrupt a turn, and a listener here that re-raised would turn "cancel this
12
+ * delegation" into "exit pi". A governance package quietly changing the host's interrupt semantics is a
13
+ * worse defect than the leak it fixes, and it would land on **every** session rather than the opt-in ones.
14
+ *
15
+ * So the coverage is exact and stated rather than implied:
16
+ *
17
+ * - **Covered:** normal exit, `process.exit()`, an uncaught exception that unwinds to the default handler.
18
+ * - **NOT covered:** SIGKILL, and SIGTERM/SIGINT where nothing else in the process has installed a
19
+ * listener. Node terminates without running `exit` handlers in those cases, by design. A pane can still
20
+ * be orphaned there, and `herdr tab close <id>` is the manual remedy.
21
+ *
22
+ * Everything here is **synchronous**, because an `exit` handler is: a promise scheduled there never runs.
23
+ */
24
+
25
+ import { execFileSync } from "node:child_process";
26
+ import { rmSync } from "node:fs";
27
+
28
+ export interface OpenPane {
29
+ /** herdr tab id — what `tab close` takes. */
30
+ tab: string;
31
+ /** Agent name, so the agent is stopped before its tab goes away. */
32
+ name: string;
33
+ /** Staged system-prompt directory, removed with the pane it belonged to. */
34
+ promptDir?: string;
35
+ }
36
+
37
+ /**
38
+ * Panes opened by THIS process and not yet closed. Keyed by tab id, so a double close is impossible.
39
+ *
40
+ * **That keying rests on tab ids being unique, which was measured rather than assumed** (herdr 0.7.5): ids
41
+ * are `w<workspace>:t<counter>`, allocated by the single server, and **not recycled** — creating a tab,
42
+ * closing it, and creating again yields the next counter value, never the freed one. If that ever changed,
43
+ * concurrent panes would collapse into one entry and an early finisher's `untrackPane` would drop a live
44
+ * sibling. Recorded because the hazard is invisible in the code and the precondition lives in another
45
+ * project.
46
+ */
47
+ const open = new Map<string, OpenPane>();
48
+ let hookInstalled = false;
49
+
50
+ /** A run has opened a pane. Idempotent per tab, and installs the exit hook on first use only. */
51
+ export function trackPane(pane: OpenPane): void {
52
+ open.set(pane.tab, pane);
53
+ if (hookInstalled) return;
54
+ hookInstalled = true;
55
+ // `once`, and only ever one, so a fan-out of eight children does not install eight handlers and trip
56
+ // Node's MaxListenersExceededWarning — which would be this package printing a warning about itself.
57
+ process.once("exit", () => void reapOpenPanes());
58
+ }
59
+
60
+ /** A run closed its own pane the normal way. */
61
+ export function untrackPane(tab: string): void {
62
+ open.delete(tab);
63
+ }
64
+
65
+ /** How many panes are currently outstanding — for tests and for `/grants`. */
66
+ export function openPaneCount(): number {
67
+ return open.size;
68
+ }
69
+
70
+ /** Per-command wall clock. `SIGKILL` because `timeout` alone is not a bound — see `TOTAL_BUDGET_MS`. */
71
+ const PER_CALL_MS = 2000;
72
+
73
+ /**
74
+ * Total wall clock the whole sweep may add to process exit.
75
+ *
76
+ * **Measured, and the reason this exists.** With eight panes and a hung herdr, a per-call timeout of 5s
77
+ * across two calls per pane is **80 seconds of silent hang at shutdown** — `stdio: "ignore"`, so the process
78
+ * looks wedged with no output. Worse, `timeout` is not a hard bound at all: `spawnSync` sends `SIGTERM` and
79
+ * then waits for the child to actually die, so a process ignoring `SIGTERM` runs to its own completion
80
+ * (measured: a 3s timeout took 59.8s against `trap '' TERM; sleep 60`). Hence `killSignal: "SIGKILL"` **and**
81
+ * a budget across the whole sweep rather than per call.
82
+ *
83
+ * A pane left open because the budget ran out is the failure this whole module downgrades to, and it is the
84
+ * right one: `herdr tab close <id>` is a five-second manual fix, whereas a shell that will not exit is not.
85
+ */
86
+ const TOTAL_BUDGET_MS = 6000;
87
+
88
+ /** Run one herdr command synchronously, swallowing everything: at exit there is nowhere to report. */
89
+ const defaultSyncExec = (args: string[]): void => {
90
+ execFileSync("herdr", args, { stdio: "ignore", timeout: PER_CALL_MS, killSignal: "SIGKILL" });
91
+ };
92
+
93
+ /**
94
+ * Close every outstanding pane and return the tab ids closed.
95
+ *
96
+ * Exported and parameterised so it can be tested without herdr installed: the exit hook is unreachable from
97
+ * a test (registering a real `exit` handler would run during the test runner's own shutdown), so the hook is
98
+ * one line and *this* is where the behaviour lives.
99
+ *
100
+ * Failures are swallowed per pane rather than per call — one pane herdr will not close must not strand the
101
+ * other seven, and nothing at exit has anywhere to report to anyway.
102
+ */
103
+ export function reapOpenPanes(syncExec: (args: string[]) => void = defaultSyncExec, now = Date.now): string[] {
104
+ const closed: string[] = [];
105
+ const deadline = now() + TOTAL_BUDGET_MS;
106
+ for (const pane of [...open.values()]) {
107
+ // Checked BEFORE each pane rather than after, so the budget bounds what we start, not what we finish.
108
+ // Panes left behind stay in the map; there is no later sweep, and saying so is the honest position —
109
+ // `openPaneCount()` is non-zero afterwards precisely so a caller could report it if it ever wanted to.
110
+ if (now() >= deadline) break;
111
+ try {
112
+ syncExec(["agent", "stop", pane.name]);
113
+ } catch {
114
+ /* the agent may already be gone; the tab is what matters */
115
+ }
116
+ try {
117
+ syncExec(["tab", "close", pane.tab]);
118
+ closed.push(pane.tab);
119
+ } catch {
120
+ /* an orphan we could not close: `herdr tab close` is the manual remedy, as documented above */
121
+ }
122
+ if (pane.promptDir) {
123
+ try {
124
+ rmSync(pane.promptDir, { recursive: true, force: true });
125
+ } catch {
126
+ /* /tmp litter, not correctness */
127
+ }
128
+ }
129
+ open.delete(pane.tab);
130
+ }
131
+ return closed;
132
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Facts about pi itself: its built-in tool names, and the capability that means "all of them".
3
+ *
4
+ * These lived in `agent-types.ts` until ADR-0016 deleted that module along with the rest of the
5
+ * `@tintinweb/pi-subagents` port. They are not about agent types at all — `PI_BUILTIN_TOOLS` is how the
6
+ * catalog tells a built-in from an extension tool, and `WILDCARD` is a capability id — so they outlive
7
+ * the port and belong somewhere that does not name a package we no longer depend on.
8
+ *
9
+ * **`PI_BUILTIN_TOOLS` is a pinned observation, and it has already drifted once** (R-31): pi 0.84.1
10
+ * exposes a `parallel` tool that the 0.83.0 list did not contain, which made it classify as an
11
+ * *extension* capability. The list is used for classification only — never for enforcement, which is
12
+ * `--tools`' job — so drift misfiles a capability rather than granting one. Keep it current anyway; a
13
+ * misfiled capability is a confusing catalog.
14
+ */
15
+
16
+ import type { Capability } from "./resolve.ts";
17
+
18
+ /** Capability held only by a delegator authorised to hand out everything. */
19
+ export const WILDCARD: Capability = "tool:*";
20
+
21
+
22
+ /** pi's built-in tools, observed against 0.84.1. `parallel` was added since 0.83.0 — see R-31. */
23
+ export const PI_BUILTIN_TOOLS = [
24
+ "bash", "edit", "edit-diff", "find", "grep", "ls", "parallel", "read", "write",
25
+ ] as const;