@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.
- package/README.md +173 -293
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/{index.ts → dist/index.js} +2 -1
- package/dist/index.js.map +1 -0
- package/dist/src/core.d.ts +8 -0
- package/dist/src/core.d.ts.map +1 -0
- package/dist/src/core.js +7 -0
- package/dist/src/core.js.map +1 -0
- package/dist/src/envelope.d.ts +21 -0
- package/dist/src/envelope.d.ts.map +1 -0
- package/dist/src/envelope.js +158 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/grammar-recovery.d.ts +105 -0
- package/dist/src/grammar-recovery.d.ts.map +1 -0
- package/dist/src/grammar-recovery.js +1052 -0
- package/dist/src/grammar-recovery.js.map +1 -0
- package/dist/src/grammar.d.ts +2 -0
- package/dist/src/grammar.d.ts.map +1 -0
- package/dist/src/grammar.js +2 -0
- package/dist/src/grammar.js.map +1 -0
- package/dist/src/index.d.ts +36 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +558 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lifecycle.d.ts +34 -0
- package/dist/src/lifecycle.d.ts.map +1 -0
- package/dist/src/lifecycle.js +133 -0
- package/dist/src/lifecycle.js.map +1 -0
- package/dist/src/pi.d.ts +19 -0
- package/dist/src/pi.d.ts.map +1 -0
- package/dist/src/pi.js +38 -0
- package/dist/src/pi.js.map +1 -0
- package/dist/src/pipeline.d.ts +10 -0
- package/dist/src/pipeline.d.ts.map +1 -0
- package/dist/src/pipeline.js +166 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/policy.d.ts +16 -0
- package/dist/src/policy.d.ts.map +1 -0
- package/dist/src/policy.js +31 -0
- package/dist/src/policy.js.map +1 -0
- package/dist/src/preprocess.d.ts +37 -0
- package/dist/src/preprocess.d.ts.map +1 -0
- package/dist/src/preprocess.js +216 -0
- package/dist/src/preprocess.js.map +1 -0
- package/dist/src/repair-engine.d.ts +91 -0
- package/dist/src/repair-engine.d.ts.map +1 -0
- package/dist/src/repair-engine.js +457 -0
- package/dist/src/repair-engine.js.map +1 -0
- package/dist/src/settings.d.ts +38 -0
- package/dist/src/settings.d.ts.map +1 -0
- package/dist/src/settings.js +87 -0
- package/dist/src/settings.js.map +1 -0
- package/dist/src/tables.d.ts +16 -0
- package/dist/src/tables.d.ts.map +1 -0
- package/dist/src/tables.js +302 -0
- package/dist/src/tables.js.map +1 -0
- package/dist/src/types.d.ts +52 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/value-strips.d.ts +52 -0
- package/dist/src/value-strips.d.ts.map +1 -0
- package/dist/src/value-strips.js +191 -0
- package/dist/src/value-strips.js.map +1 -0
- package/docs/how-it-works.md +227 -0
- package/docs/operations.md +141 -0
- package/docs/research.md +390 -0
- package/docs/tool-owner-integration.md +557 -0
- package/package.json +31 -6
- package/src/grammar-recovery.ts +0 -1269
- package/src/index.ts +0 -621
- package/src/repair-engine.ts +0 -518
- package/src/settings.ts +0 -95
- package/src/tables.ts +0 -184
- package/src/value-strips.ts +0 -218
package/docs/research.md
ADDED
|
@@ -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.
|