@@ -23,6 +23,16 @@ is protecting and where to re-verify it.
2323 ` test/upstream-drift.test.ts ` that execute the claims against the installed
2424 packages.
2525
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+
2636## Claims
2737
2838### Claim 1 — Loop ordering: ` prepareArguments ` runs before validation, which runs before the ` tool_call ` event
@@ -202,6 +212,124 @@ queues value-free feedback, binds it at post-validation `tool_call`, then uses
202212custom calls already have IDs and can be associated directly, without wrapping
203213another extension's executor.
204214
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+
205333### Package/runtime assumptions
206334
207335The published package targets Node 22+ and compiled ESM. ` typebox ` is a runtime
@@ -245,3 +373,18 @@ work through this list and update the citations/date above:
245373 messages, and handlers still compose in registration order.
24637410 . ** Package/runtime:** run ` pnpm run test:package ` ; re-check Node engines,
247375 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.
0 commit comments