@r3b1s/pi-repair-layer 0.2.1 → 0.4.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 (76) hide show
  1. package/README.md +173 -293
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/{index.ts → dist/index.js} +2 -1
  5. package/dist/index.js.map +1 -0
  6. package/dist/src/core.d.ts +8 -0
  7. package/dist/src/core.d.ts.map +1 -0
  8. package/dist/src/core.js +7 -0
  9. package/dist/src/core.js.map +1 -0
  10. package/dist/src/envelope.d.ts +21 -0
  11. package/dist/src/envelope.d.ts.map +1 -0
  12. package/dist/src/envelope.js +158 -0
  13. package/dist/src/envelope.js.map +1 -0
  14. package/dist/src/grammar-recovery.d.ts +105 -0
  15. package/dist/src/grammar-recovery.d.ts.map +1 -0
  16. package/dist/src/grammar-recovery.js +1052 -0
  17. package/dist/src/grammar-recovery.js.map +1 -0
  18. package/dist/src/grammar.d.ts +2 -0
  19. package/dist/src/grammar.d.ts.map +1 -0
  20. package/dist/src/grammar.js +2 -0
  21. package/dist/src/grammar.js.map +1 -0
  22. package/dist/src/index.d.ts +36 -0
  23. package/dist/src/index.d.ts.map +1 -0
  24. package/dist/src/index.js +558 -0
  25. package/dist/src/index.js.map +1 -0
  26. package/dist/src/lifecycle.d.ts +34 -0
  27. package/dist/src/lifecycle.d.ts.map +1 -0
  28. package/dist/src/lifecycle.js +133 -0
  29. package/dist/src/lifecycle.js.map +1 -0
  30. package/dist/src/pi.d.ts +19 -0
  31. package/dist/src/pi.d.ts.map +1 -0
  32. package/dist/src/pi.js +38 -0
  33. package/dist/src/pi.js.map +1 -0
  34. package/dist/src/pipeline.d.ts +10 -0
  35. package/dist/src/pipeline.d.ts.map +1 -0
  36. package/dist/src/pipeline.js +166 -0
  37. package/dist/src/pipeline.js.map +1 -0
  38. package/dist/src/policy.d.ts +16 -0
  39. package/dist/src/policy.d.ts.map +1 -0
  40. package/dist/src/policy.js +31 -0
  41. package/dist/src/policy.js.map +1 -0
  42. package/dist/src/preprocess.d.ts +37 -0
  43. package/dist/src/preprocess.d.ts.map +1 -0
  44. package/dist/src/preprocess.js +216 -0
  45. package/dist/src/preprocess.js.map +1 -0
  46. package/dist/src/repair-engine.d.ts +91 -0
  47. package/dist/src/repair-engine.d.ts.map +1 -0
  48. package/dist/src/repair-engine.js +457 -0
  49. package/dist/src/repair-engine.js.map +1 -0
  50. package/dist/src/settings.d.ts +38 -0
  51. package/dist/src/settings.d.ts.map +1 -0
  52. package/dist/src/settings.js +87 -0
  53. package/dist/src/settings.js.map +1 -0
  54. package/dist/src/tables.d.ts +16 -0
  55. package/dist/src/tables.d.ts.map +1 -0
  56. package/dist/src/tables.js +302 -0
  57. package/dist/src/tables.js.map +1 -0
  58. package/dist/src/types.d.ts +52 -0
  59. package/dist/src/types.d.ts.map +1 -0
  60. package/dist/src/types.js +2 -0
  61. package/dist/src/types.js.map +1 -0
  62. package/dist/src/value-strips.d.ts +52 -0
  63. package/dist/src/value-strips.d.ts.map +1 -0
  64. package/dist/src/value-strips.js +191 -0
  65. package/dist/src/value-strips.js.map +1 -0
  66. package/docs/how-it-works.md +227 -0
  67. package/docs/operations.md +141 -0
  68. package/docs/research.md +390 -0
  69. package/docs/tool-owner-integration.md +557 -0
  70. package/package.json +31 -6
  71. package/src/grammar-recovery.ts +0 -1269
  72. package/src/index.ts +0 -621
  73. package/src/repair-engine.ts +0 -518
  74. package/src/settings.ts +0 -95
  75. package/src/tables.ts +0 -184
  76. package/src/value-strips.ts +0 -218
@@ -0,0 +1,390 @@
1
+ # Research: pi agent-loop facts this extension relies on
2
+
3
+ This is the tracked, claim-by-claim ledger of the mechanical facts about pi that
4
+ `pi-repair-layer` is built on. Every design decision (which hook, why
5
+ `prepareArguments`, the grammar-recovery `stopReason` gate, the dropped
6
+ schema-anchor defense) traces back to one of the claims below. If a pi upgrade
7
+ changes one of these, the corresponding tripwire in `test/upstream-drift.test.ts`
8
+ is meant to fail loudly — this document tells the next reader what the tripwire
9
+ is protecting and where to re-verify it.
10
+
11
+ ## Verification provenance
12
+
13
+ - **Verification date:** 2026-07-17
14
+ - **Source read:** pi monorepo TypeScript source, clone at `~/Local/docs/pi`,
15
+ git `0e6909f0` (`git describe`: `v0.80.6-24-g0e6909f0`). Package versions in
16
+ the clone: `pi-coding-agent`, `pi-agent-core` (`packages/agent`), and `pi-ai`
17
+ all `0.80.6`.
18
+ - **Installed (this repo's `node_modules`):** `@earendil-works/pi-ai`,
19
+ `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` `0.80.6`.
20
+ Clone citations below remain the source-of-truth citations; installed `dist/`
21
+ line numbers shift but the tripwires execute that published build directly.
22
+ - **Method:** direct source reading, plus the behavioral tripwires in
23
+ `test/upstream-drift.test.ts` that execute the claims against the installed
24
+ packages.
25
+
26
+ Claims 10–14 (optional integration) were added later with their own provenance:
27
+
28
+ - **Verification date:** 2026-07-18
29
+ - **Source read:** the same pi clone (`v0.80.6-24-g0e6909f0`); line citations
30
+ below refer to it.
31
+ - **Empirical runs:** a live pi 0.80.10 install (npm-dist `cli.js` under
32
+ Node 24) and the official `pi-linux-x64` release binary v0.80.10 (Bun
33
+ compiled executable), each driven with a throwaway probe extension
34
+ npm-installed into an isolated `$HOME/.pi/agent/npm` scope.
35
+
36
+ ## Claims
37
+
38
+ ### Claim 1 — Loop ordering: `prepareArguments` runs before validation, which runs before the `tool_call` event
39
+
40
+ Per tool call, pi runs, in order: `tool.prepareArguments(raw)` →
41
+ `validateToolArguments(...)` → (if configured) the `beforeToolCall` hook, which
42
+ is what surfaces the `tool_call` extension event → `tool.execute(validatedArgs)`.
43
+
44
+ - `packages/agent/src/agent-loop.ts:588-600` — `prepareToolCallArguments` calls
45
+ `tool.prepareArguments(toolCall.arguments)`.
46
+ - `packages/agent/src/agent-loop.ts:618-621` — inside `prepareToolCall`:
47
+ `prepareToolCallArguments(...)` then `validateToolArguments(tool, preparedToolCall)`
48
+ then `config.beforeToolCall(...)`.
49
+ - `packages/agent/src/agent-loop.ts:653-657` — only after the above does the
50
+ call resolve to `{ kind: "prepared", ... args: validatedArgs }` for execution.
51
+
52
+ **Why it matters:** `prepareArguments` is the *only* seam that sees the raw input
53
+ before validation can throw. This is the founding reason the extension overrides
54
+ built-in tools rather than handling the `tool_call` event.
55
+
56
+ ### Claim 2 — `validateToolArguments` throws on failure (Convert-then-Check)
57
+
58
+ `validateToolArguments` clones the args, runs TypeBox `Value.Convert` (coercion),
59
+ then `validator.Check`; on failure it throws a formatted `Error`.
60
+
61
+ - `packages/ai/src/utils/validation.ts:278-309` — `structuredClone` →
62
+ `Value.Convert(tool.parameters, args)` → `validator.Check(args)` → on failure,
63
+ builds an error message and `throw new Error(errorMessage)` (line 309).
64
+
65
+ **Why it matters:** two things. (a) A validation failure throws before the
66
+ `tool_call` event fires, so that event can never repair a failing built-in call
67
+ (basis of Claim 1's consequence). (b) `Value.Convert` runs *first* and silently
68
+ coerces (`null` → `"null"`, `'["a"]'` → `['["a"]']`), which is why the repair
69
+ engine checks strictly **before** Convert — see `src/repair-engine.ts`.
70
+
71
+ ### Claim 3 — `tool_call` event: in-place `input` mutation propagates, reassignment does not
72
+
73
+ The `tool_call` event's `input` is the very `validatedArgs` object the loop will
74
+ execute with, so mutating it in place reaches `execute`. But the event dispatcher
75
+ returns only the handler's `{ block, reason }` result — a handler that
76
+ *reassigns* `event.input = newObject` has that new object dropped.
77
+
78
+ - `packages/agent/src/agent-loop.ts:621-626` — `beforeToolCall` is passed
79
+ `args: validatedArgs`.
80
+ - `packages/coding-agent/src/core/agent-session.ts:424-436` — `beforeToolCall`
81
+ forwards to `runner.emitToolCall({ ..., input: args })`; `input` **is** the
82
+ `validatedArgs` reference.
83
+ - `packages/coding-agent/src/core/extensions/runner.ts:881-902` — `emitToolCall`
84
+ returns the handler result and the caller reads only `.block`
85
+ (`agent-loop.ts:638`). The executed object is the separate `validatedArgs`
86
+ reference (`agent-loop.ts:657`), so a reassignment of `event.input` is never
87
+ read back.
88
+
89
+ **Why it matters:** this is why an upstream extension that does
90
+ `event.input = repairedClone` on the `tool_call` event cannot repair a built-in
91
+ call even setting aside Claim 2 — its computed repair is discarded.
92
+
93
+ ### Claim 4 — `message_end` replacement mutates the loop's message in place, listeners are awaited, and the replacement executes same-turn (with a role guard)
94
+
95
+ A `message_end` handler that returns `{ message }` has that message written back
96
+ onto the loop's own assistant-message object **in place**, before the loop
97
+ filters the message for tool calls — so a replacement whose content includes a
98
+ `toolCall` block is executed in the same turn. The replacement must keep the
99
+ original role.
100
+
101
+ - `packages/agent/src/agent-loop.ts:359,372` — `streamAssistantResponse` emits
102
+ `message_end` with `finalMessage`, then `return finalMessage` (line 373).
103
+ - `packages/agent/src/agent.ts:571-572` — the emit path awaits every listener
104
+ (`for (const listener of this.listeners) await listener(event, signal)`), so
105
+ the replacement is applied before `streamAssistantResponse` returns.
106
+ - `packages/coding-agent/src/core/agent-session.ts:709-726` — on `message_end`,
107
+ `emitMessageEnd` returns a replacement which is written via
108
+ `_replaceMessageInPlace(event.message, normalized)`.
109
+ - `packages/coding-agent/src/core/agent-session.ts:657-671` —
110
+ `_replaceMessageInPlace` deletes the target's keys and `Object.assign`s the
111
+ replacement onto the *same object reference* the loop holds.
112
+ - `packages/coding-agent/src/core/extensions/runner.ts:804-808` — role guard:
113
+ a handler result whose `message.role !== currentMessage.role` is rejected with
114
+ an error and skipped.
115
+ - `packages/agent/src/agent-loop.ts:203` — back in the loop, tool calls are read
116
+ from `message.content` *after* `streamAssistantResponse` returns; because the
117
+ replacement mutated that same object, injected `toolCall` blocks are seen and
118
+ executed this turn.
119
+
120
+ **Why it matters:** grammar-leak recovery hooks `message_end`, returns a
121
+ same-role replacement with the leaked text stripped, and (in `recover` mode)
122
+ appends `toolCall` blocks that execute same-turn and re-enter our
123
+ `prepareArguments` repair path. All of that depends on this claim.
124
+
125
+ ### Claim 5 — `before_provider_request` propagates the returned payload
126
+
127
+ A `before_provider_request` handler's return value replaces the outgoing request
128
+ payload for later handlers and the provider.
129
+
130
+ - `packages/coding-agent/src/core/extensions/runner.ts:965-997` —
131
+ `emitBeforeProviderRequest` threads `currentPayload = handlerResult` whenever a
132
+ handler returns a non-`undefined` value, and returns the final payload.
133
+
134
+ **Why it matters:** this is the hook the *dropped* schema-anchor defense would
135
+ have used. It is documented here so that if scope ever broadens to non-built-in
136
+ tools with regex `pattern` schemas (see Claim 7), the mechanism is known-good.
137
+
138
+ ### Claim 6 — `stopReason: "error"` (and `"aborted"`) is terminal, not a retry
139
+
140
+ When the assistant message's `stopReason` is `"error"` or `"aborted"`, the loop
141
+ emits `turn_end` + `agent_end` and returns — it does not auto-retry.
142
+
143
+ - `packages/agent/src/agent-loop.ts:196-200`.
144
+
145
+ **Why it matters:** it demotes the upstream "phantom toolUse → error triggers
146
+ auto-retry" claim (that normalization is out of scope here — a clean stop, not a
147
+ retry, on this pi). Also bounds what the grammar-recovery `stopReason` gate may
148
+ safely do.
149
+
150
+ ### Claim 7 — `stopReason: "length"` causes pi to fail all tool calls in the message
151
+
152
+ If tool calls are present but `stopReason` is `"length"` (output truncated by the
153
+ token limit), pi fails every tool call rather than executing possibly-truncated
154
+ arguments.
155
+
156
+ - `packages/agent/src/agent-loop.ts:207-214` — when `toolCalls.length > 0`,
157
+ `message.stopReason === "length"` routes to
158
+ `failToolCallsFromTruncatedMessage(toolCalls, emit)` instead of
159
+ `executeToolCalls(...)`.
160
+ - `packages/agent/src/agent-loop.ts:383` — `failToolCallsFromTruncatedMessage`
161
+ definition.
162
+
163
+ **Why it matters:** this is exactly the protection the grammar-recovery
164
+ `stopReason` gate must not bypass. Upstream promotes leaked calls regardless of
165
+ `stopReason`, overwriting `"length"` with `"toolUse"` and defeating this
166
+ truncation guard. Our gate only *promotes* on `stopReason: "stop"` (stripping is
167
+ allowed on any `stopReason`), so a truncated message keeps failing its calls.
168
+
169
+ ### Claim 8 — Built-in tool schemas carry no regex `pattern` keyword
170
+
171
+ Every field in the seven wrapped built-in tool schemas is a plain
172
+ `Type.String({ description })` / `Type.Number` / etc. No field declares a
173
+ JSON-schema `pattern` (regex) keyword. The literal string `pattern` appears only
174
+ as a *field name* (grep's and find's search-pattern parameter) and in
175
+ descriptions.
176
+
177
+ - `packages/coding-agent/src/core/tools/grep.ts:25` — `pattern: Type.String({
178
+ description: "Search pattern (regex or literal string)" })` — a plain string
179
+ field, no schema-level `pattern` keyword.
180
+ - `packages/coding-agent/src/core/tools/find.ts:21-23` — `pattern: Type.String({
181
+ description: ... })` — likewise a glob string, no regex keyword.
182
+ - The remaining tool files (`read.ts`, `write.ts`, `edit.ts`, `bash.ts`,
183
+ `ls.ts`) declare only plain-typed fields.
184
+
185
+ **Why it matters:** (a) it justifies dropping the schema-anchor poisoning defense
186
+ (there is no regex schema to sanitize among built-ins). (b) It justifies the
187
+ anchor-strip exemption for `grep.pattern`: because `grep`'s pattern *is* a real
188
+ regex field, a leading `^` / trailing `$` there may be intentional syntax and is
189
+ indistinguishable from bled anchors, so we never strip it.
190
+
191
+ ### Claim 9 — `tool_result` is global, replaceable, and handler-composed
192
+
193
+ After any built-in or custom tool finishes, the coding-agent session emits a
194
+ global `tool_result` event containing `toolName`, `toolCallId`, validated
195
+ `input`, `content`, `details`, and `isError`. A handler may replace `content`,
196
+ `details`, or `isError`. The runner threads each replacement through the event
197
+ seen by later handlers and returns the final fields to the agent loop.
198
+
199
+ - Installed `dist/core/agent-session.js:211-238` — `afterToolCall` emits the
200
+ complete result event and returns the hook's replacement, preserving the
201
+ original error flag when no replacement is supplied.
202
+ - Installed `dist/core/extensions/runner.js:600-641` — `emitToolResult` copies
203
+ the event, visits handlers in registration order, writes each returned field
204
+ into `currentEvent`, and returns the composed final result.
205
+ - `test/upstream-drift.test.ts` — real-loop tripwires cover a successful custom
206
+ tool, a throwing custom tool, replacement content/details/error state, and
207
+ two-handler composition.
208
+
209
+ **Why it matters:** repair input is prepared before a call ID exists. The layer
210
+ queues value-free feedback, binds it at post-validation `tool_call`, then uses
211
+ `tool_result` to attach one note to success or error output. Grammar-promoted
212
+ custom calls already have IDs and can be associated directly, without wrapping
213
+ another extension's executor.
214
+
215
+ ### Claim 10 — pi maintains one shared npm install project per scope, with flat sibling resolution
216
+
217
+ All `npm:`-installed extensions in a scope are dependencies of a single private
218
+ npm project named `pi-extensions`, so their packages sit side by side in one
219
+ flat `node_modules`. An end user who runs
220
+ `pi install npm:@r3b1s/pi-repair-layer` therefore makes the package resolvable
221
+ from every other npm-installed extension in that scope.
222
+
223
+ - `packages/coding-agent/src/core/package-manager.ts:1933-1944` —
224
+ `ensureNpmProject` writes `{ name: "pi-extensions", private: true }` into the
225
+ install root's `package.json`.
226
+ - `packages/coding-agent/src/core/package-manager.ts:1956-1964` —
227
+ `getNpmInstallRoot` returns `join(this.agentDir, "npm")` for the user scope
228
+ (project scope roots under the project's config dir instead).
229
+ - Live install (pi 0.80.10): `~/.pi/agent/npm/package.json` has
230
+ `name: "pi-extensions"` with every npm-installed extension as a dependency,
231
+ and `node_modules` holds them as flat siblings (bun-backed, `bun.lock`
232
+ present).
233
+
234
+ **Why it matters:** this is the mechanism behind the shared-`node_modules`
235
+ adoption path in the optional-integration recipe
236
+ (`docs/tool-owner-integration.md`). It is observed managed-install layout, not
237
+ a documented pi API guarantee — the recipe never *depends* on it (resolution
238
+ either succeeds or falls back safely), but the adoption story does.
239
+
240
+ ### Claim 11 — Missing-module error shapes: `MODULE_NOT_FOUND` (jiti/require) vs `ERR_MODULE_NOT_FOUND` (ESM import)
241
+
242
+ A dynamic import of an uninstalled package surfaces differently depending on
243
+ the loader, but always with one of two `code` values and a message that names
244
+ the requested module:
245
+
246
+ - jiti 2.7.0 require path (Node): plain `Error`, `code: "MODULE_NOT_FOUND"`,
247
+ message `Cannot find module '<full specifier>'`.
248
+ - Native Node ESM `import()` (Node 24): `Error`,
249
+ `code: "ERR_MODULE_NOT_FOUND"`, message
250
+ `Cannot find package '<package name>'` — **it names the bare package, not
251
+ the full subpath specifier**, so absence checks must match the package name
252
+ (`@r3b1s/pi-repair-layer`), never the `/pi` subpath string.
253
+ - Compiled Bun binary (pi-linux-x64 v0.80.10): Bun `ResolveMessage` (an
254
+ `Error` subclass), `code: "ERR_MODULE_NOT_FOUND"` for `import()` and
255
+ `"MODULE_NOT_FOUND"` for a scoped `require`, message
256
+ `Cannot find module '<full specifier>' from '<importer path>'`.
257
+
258
+ **Why it matters:** the optional-integration recipe treats an import failure
259
+ as "package absent" only when the code is one of these two values **and** the
260
+ message names `@r3b1s/pi-repair-layer`; anything else rethrows so a broken
261
+ install (a *transitive* module missing) is not silently misread as absent.
262
+ All three observed shapes pass that discrimination.
263
+
264
+ ### Claim 12 — Git installs and other scopes do not resolve the shared npm siblings
265
+
266
+ Git-installed extensions are cloned into `<agentDir>/git/<host>/<path>` and get
267
+ their **own** dependency install inside the clone; they are not siblings of the
268
+ shared npm root. User scope (`~/.pi/agent`) and project scope (`<cwd>/.pi`)
269
+ likewise use separate install roots.
270
+
271
+ - `packages/coding-agent/src/core/package-manager.ts:1820-1846` — `installGit`
272
+ clones into `getGitInstallPath(...)` and runs a dependency install inside the
273
+ clone when it has a `package.json`.
274
+ - `packages/coding-agent/src/core/package-manager.ts:2036-2046` —
275
+ `getGitInstallRoot` returns `join(this.agentDir, "git")` (user scope) or
276
+ `join(this.cwd, CONFIG_DIR_NAME, "git")` (project scope).
277
+
278
+ **Why it matters:** a git-installed or cross-scope consumer cannot resolve an
279
+ npm-installed `@r3b1s/pi-repair-layer` sibling, so the optional recipe falls
280
+ back there even though the user "installed" the package. Documented as a hard
281
+ caveat in the integration guide; `optionalDependencies` is the alternative for
282
+ those consumers.
283
+
284
+ ### Claim 13 — Bun-binary probe: the optional dynamic import falls back under the compiled binary, resolves under npm-dist pi
285
+
286
+ pi loads extensions through jiti (`moduleCache: false`; in the compiled binary
287
+ additionally `virtualModules` + `tryNative: false`; under Node, `alias`):
288
+
289
+ - `packages/coding-agent/src/core/extensions/loader.ts:398-404` — the
290
+ `createJiti` call and both option branches.
291
+
292
+ End-to-end probe (2026-07-18): a throwaway extension implementing the
293
+ documented recipe, npm-installed into an isolated scope, run once with
294
+ `@r3b1s/pi-repair-layer` npm-installed as a sibling and once without, under
295
+ both pi distributions of v0.80.10:
296
+
297
+ | runtime | package | observed error (`name`/`code`) | branch taken |
298
+ |---|---|---|---|
299
+ | npm-dist (Node) | absent | `Error`/`ERR_MODULE_NOT_FOUND` | fallback, note emitted |
300
+ | npm-dist (Node) | present | — | **adapted** |
301
+ | compiled binary | absent | `ResolveMessage`/`ERR_MODULE_NOT_FOUND` | fallback, note emitted |
302
+ | compiled binary | present | `ResolveMessage`/`ERR_MODULE_NOT_FOUND` | fallback, note emitted |
303
+
304
+ Under the compiled Bun binary, *static* imports of the sibling package do
305
+ resolve (jiti's own resolver handles them — verified with a static-import
306
+ probe extension in the same scope), but native dynamic `import()` and the
307
+ scoped `require` both bypass jiti and hit Bun's embedded resolver, which does
308
+ no filesystem `node_modules` resolution — even an absolute-path `import()` of
309
+ the package's entry file loads but then dies on its transitive bare specifier
310
+ (`typebox/value`). There is no consumer-accessible dynamic path through jiti's
311
+ resolver as of this pi version.
312
+
313
+ **Why it matters:** the optional-integration pattern *activates* only under
314
+ Node-based pi installs (npm/bun global install). Under the official compiled
315
+ binary it degrades safely — the import failure has exactly the absent-package
316
+ shape, so consumers fall back to their raw definition with the one-line note,
317
+ even when the package is installed. A hard static dependency, by contrast,
318
+ works under both distributions. Both facts are documented in the integration
319
+ guide's caveats.
320
+
321
+ ### Claim 14 — Unrecognized preprocessor kinds fall through `preprocessInput` untouched (local guarantee)
322
+
323
+ This claim is about this repo, not pi: `preprocessInput`
324
+ (`src/preprocess.ts`) matches each configured entry against the known `kind`
325
+ branches and simply skips entries it does not recognize — no mutation, no
326
+ error, no claimed change — and the pipeline still schema-validates the final
327
+ result. A consumer configured against a newer options shape therefore degrades
328
+ to the recognized subset when running against an older installed version.
329
+ Promoted to a spec-level compatibility guarantee by the
330
+ `optional-integration-fallback` change and pinned by a unit test in
331
+ `test/pipeline.test.ts`.
332
+
333
+ ### Package/runtime assumptions
334
+
335
+ The published package targets Node 22+ and compiled ESM. `typebox` is a runtime
336
+ dependency; pi coding-agent and pi-tui are peers so the host supplies its live
337
+ integration/UI runtime. `pnpm run test:package` builds and packs the artifact,
338
+ installs it into a clean temporary project, imports `.`, `/core`, `/pi`, and
339
+ `/grammar`, runs the APIs, and typechecks the consumer fixture. This is the
340
+ tripwire for missing JavaScript, declarations, source maps, or stale exports.
341
+
342
+ ## Re-verification checklist (run on any pi minor bump)
343
+
344
+ The version canary in `test/upstream-drift.test.ts` fails when installed
345
+ `pi-coding-agent` changes minor version (`VERIFIED_PI_VERSION`). When it does,
346
+ work through this list and update the citations/date above:
347
+
348
+ 1. **Loop ordering (Claim 1):** confirm `prepareArguments` still runs before
349
+ `validateToolArguments`, which still runs before the `tool_call`/`beforeToolCall`
350
+ hook. Re-read `packages/agent/src/agent-loop.ts` `prepareToolCall`.
351
+ 2. **Validation throws (Claim 2):** confirm `validateToolArguments` still does
352
+ `Value.Convert` then `Check` then `throw`. Re-read
353
+ `packages/ai/src/utils/validation.ts`.
354
+ 3. **`tool_call` propagation (Claim 3):** confirm the event's `input` is still the
355
+ executed `validatedArgs` reference and that the dispatcher still reads only
356
+ `.block`. If pi adds an args-return channel to `tool_call`, revisit the
357
+ founding decision.
358
+ 4. **`message_end` in-place replacement (Claim 4):** confirm replacement still
359
+ mutates the loop's message object in place, listeners are still awaited, the
360
+ role guard still holds, and toolCalls are still filtered after the stream
361
+ returns (so recovery still executes same-turn).
362
+ 5. **`before_provider_request` (Claim 5):** confirm it still propagates the
363
+ returned payload.
364
+ 6. **`stopReason` terminality (Claims 6, 7):** confirm `"error"`/`"aborted"` are
365
+ still terminal and `"length"` still fails all tool calls.
366
+ 7. **Built-in schema shape (Claim 8):** re-run the schema-shape snapshot test and
367
+ confirm no wrapped built-in gained a regex `pattern` keyword. If any did,
368
+ revisit the `grep.pattern` exemption and the dropped schema-anchor defense.
369
+ 8. Update `VERIFIED_PI_VERSION`, the verification date, and the commit hash above,
370
+ then commit the refreshed snapshot fixture deliberately.
371
+ 9. **`tool_result` lifecycle (Claim 9):** confirm built-in and custom success/error
372
+ results still reach the global hook, replacements still reach conversation
373
+ messages, and handlers still compose in registration order.
374
+ 10. **Package/runtime:** run `pnpm run test:package`; re-check Node engines,
375
+ peer/runtime dependencies, every `exports` target, and the pi extension path.
376
+ 11. **Shared npm root (Claim 10):** confirm `ensureNpmProject` still writes one
377
+ `pi-extensions` project per scope and installs remain flat siblings —
378
+ re-read `package-manager.ts` and inspect a live `~/.pi/agent/npm`.
379
+ 12. **Error shapes (Claim 11):** re-run a missing-module import under jiti, under
380
+ native Node `import()`, and under the compiled binary; confirm the codes are
381
+ still `MODULE_NOT_FOUND` / `ERR_MODULE_NOT_FOUND` and the message still names
382
+ the requested package.
383
+ 13. **Git/scope boundaries (Claim 12):** confirm git installs still get their own
384
+ clone-local dependency install and scopes still use separate install roots.
385
+ 14. **Bun-binary probe (Claim 13):** re-run the four-cell probe (npm-dist and
386
+ compiled binary, package present and absent) with a throwaway recipe
387
+ extension npm-installed into an isolated `$HOME`; update the outcome table —
388
+ especially whether dynamic `import()` in the compiled binary has gained
389
+ filesystem `node_modules` resolution, which would let optional consumers
390
+ activate there.