@r3b1s/pi-repair-layer 0.3.0 → 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 +14 -0
- package/docs/research.md +143 -0
- package/docs/tool-owner-integration.md +230 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,20 @@ same repair pipeline to tools in your own extension.
|
|
|
8
8
|
|
|
9
9
|
No LLM calls. No network requests. No uploaded telemetry.
|
|
10
10
|
|
|
11
|
+
## Contents
|
|
12
|
+
|
|
13
|
+
- [Why use it?](#why-use-it)
|
|
14
|
+
- [Install](#install)
|
|
15
|
+
- [Terms in plain English](#terms-in-plain-english)
|
|
16
|
+
- [What it repairs](#what-it-repairs)
|
|
17
|
+
- [Safe by design](#safe-by-design)
|
|
18
|
+
- [Use it in your own extension](#use-it-in-your-own-extension)
|
|
19
|
+
- [Settings and local telemetry](#settings-and-local-telemetry)
|
|
20
|
+
- [Documentation](#documentation)
|
|
21
|
+
- [Prior art](#prior-art)
|
|
22
|
+
- [Limitations](#limitations)
|
|
23
|
+
- [Development](#development)
|
|
24
|
+
|
|
11
25
|
## Why use it?
|
|
12
26
|
|
|
13
27
|
Models often understand the task but miss a detail of the tool contract:
|
package/docs/research.md
CHANGED
|
@@ -23,6 +23,16 @@ is protecting and where to re-verify it.
|
|
|
23
23
|
`test/upstream-drift.test.ts` that execute the claims against the installed
|
|
24
24
|
packages.
|
|
25
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
|
+
|
|
26
36
|
## Claims
|
|
27
37
|
|
|
28
38
|
### 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
|
|
|
202
212
|
custom calls already have IDs and can be associated directly, without wrapping
|
|
203
213
|
another extension's executor.
|
|
204
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
|
+
|
|
205
333
|
### Package/runtime assumptions
|
|
206
334
|
|
|
207
335
|
The 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:
|
|
|
245
373
|
messages, and handlers still compose in registration order.
|
|
246
374
|
10. **Package/runtime:** run `pnpm run test:package`; re-check Node engines,
|
|
247
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.
|
|
@@ -5,6 +5,19 @@ pipeline to tools that extension owns. The usual integration is one wrapper
|
|
|
5
5
|
around the tool definition. A lower-level core API is available when the tool
|
|
6
6
|
owner needs to control the surrounding lifecycle.
|
|
7
7
|
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- [The ownership boundary](#the-ownership-boundary)
|
|
11
|
+
- [Install the package](#install-the-package)
|
|
12
|
+
- [Recommended: wrap the tool definition](#recommended-wrap-the-tool-definition)
|
|
13
|
+
- [Optional integration](#optional-integration)
|
|
14
|
+
- [Configure only known-safe transforms](#configure-only-known-safe-transforms)
|
|
15
|
+
- [Existing `prepareArguments` hooks](#existing-preparearguments-hooks)
|
|
16
|
+
- [Optional: receive structured outcomes](#optional-receive-structured-outcomes)
|
|
17
|
+
- [Optional: attach `<repair_note>` feedback](#optional-attach-repair_note-feedback)
|
|
18
|
+
- [Lower level: call the pure core](#lower-level-call-the-pure-core)
|
|
19
|
+
- [Test the integration](#test-the-integration)
|
|
20
|
+
|
|
8
21
|
## The ownership boundary
|
|
9
22
|
|
|
10
23
|
pi validates tool arguments after calling that tool's `prepareArguments`.
|
|
@@ -24,6 +37,12 @@ Add pi-repair-layer as a dependency of the extension that owns the tool:
|
|
|
24
37
|
pnpm add @r3b1s/pi-repair-layer
|
|
25
38
|
```
|
|
26
39
|
|
|
40
|
+
The dependency does not have to be hard. A standalone extension can keep
|
|
41
|
+
pi-repair-layer out of its runtime dependencies entirely and degrade to its
|
|
42
|
+
raw tool definition when the package is absent — see
|
|
43
|
+
[Optional integration](#optional-integration) before committing your users to
|
|
44
|
+
an extra install.
|
|
45
|
+
|
|
27
46
|
The public APIs are compiled ESM with TypeScript declarations:
|
|
28
47
|
|
|
29
48
|
- `@r3b1s/pi-repair-layer/pi` provides the pi tool-definition adapter.
|
|
@@ -112,6 +131,208 @@ model-readable retry message. `unrepairable: "passthrough"` is available for a
|
|
|
112
131
|
deliberate compatibility migration, but it can expose the input to pi's native
|
|
113
132
|
conversion and should not be the default.
|
|
114
133
|
|
|
134
|
+
## Optional integration
|
|
135
|
+
|
|
136
|
+
`adaptToolDefinition` is a pure decorator: it takes a `ToolDefinition` and
|
|
137
|
+
returns a `ToolDefinition`. That makes the identity function a complete
|
|
138
|
+
fallback — an extension can attempt to load the adapter at activation time and
|
|
139
|
+
register its unmodified definition when pi-repair-layer is not installed.
|
|
140
|
+
Repair support then becomes an end-user opt-in instead of an author-imposed
|
|
141
|
+
dependency.
|
|
142
|
+
|
|
143
|
+
### The recipe
|
|
144
|
+
|
|
145
|
+
Author repair options as pure data, import only types at compile time, and
|
|
146
|
+
attempt one dynamic import inside the (awaited) extension factory:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
150
|
+
import type {
|
|
151
|
+
adaptToolDefinition,
|
|
152
|
+
PiToolOwnerAdapterOptions,
|
|
153
|
+
} from "@r3b1s/pi-repair-layer/pi";
|
|
154
|
+
import { Type } from "typebox";
|
|
155
|
+
|
|
156
|
+
const parameters = Type.Object({ path: Type.String() });
|
|
157
|
+
|
|
158
|
+
const repairOptions = {
|
|
159
|
+
policy: "adaptive",
|
|
160
|
+
preprocessors: [
|
|
161
|
+
{
|
|
162
|
+
kind: "alias",
|
|
163
|
+
selector: "/path",
|
|
164
|
+
aliases: ["file_path"],
|
|
165
|
+
accepts: "string",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
} satisfies PiToolOwnerAdapterOptions;
|
|
169
|
+
|
|
170
|
+
const definition: ToolDefinition<typeof parameters> = {
|
|
171
|
+
name: "inspect_asset",
|
|
172
|
+
label: "Inspect asset",
|
|
173
|
+
description: "Inspect an asset",
|
|
174
|
+
parameters,
|
|
175
|
+
async execute(_toolCallId, params) {
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text: `Inspecting ${params.path}` }],
|
|
178
|
+
details: undefined,
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
async function loadRepairAdapter(): Promise<
|
|
184
|
+
typeof adaptToolDefinition | undefined
|
|
185
|
+
> {
|
|
186
|
+
try {
|
|
187
|
+
const repair = await import("@r3b1s/pi-repair-layer/pi");
|
|
188
|
+
return repair.adaptToolDefinition;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
const packageAbsent =
|
|
193
|
+
(code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") &&
|
|
194
|
+
message.includes("@r3b1s/pi-repair-layer");
|
|
195
|
+
if (!packageAbsent) throw error;
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default async function myExtension(pi: ExtensionAPI) {
|
|
201
|
+
const adapt = await loadRepairAdapter();
|
|
202
|
+
if (!adapt) {
|
|
203
|
+
console.error(
|
|
204
|
+
"[my-extension] @r3b1s/pi-repair-layer not found; inspect_asset running unwrapped",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
pi.registerTool(adapt ? adapt(definition, repairOptions) : definition);
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The details are load-bearing:
|
|
212
|
+
|
|
213
|
+
- **Discriminate before falling back.** A missing package surfaces as
|
|
214
|
+
`code: "MODULE_NOT_FOUND"` (jiti's require path) or
|
|
215
|
+
`code: "ERR_MODULE_NOT_FOUND"` (native ESM and the compiled pi binary). But
|
|
216
|
+
a *present-but-broken* install — pi-repair-layer resolvable while one of its
|
|
217
|
+
own transitive modules is not — throws the same codes naming the
|
|
218
|
+
*transitive* module. Requiring the message to name
|
|
219
|
+
`@r3b1s/pi-repair-layer` keeps a broken install loud instead of silently
|
|
220
|
+
running unwrapped while the user believes repairs are active. Match the
|
|
221
|
+
package name, not the `/pi` subpath: native ESM reports only
|
|
222
|
+
`Cannot find package '@r3b1s/pi-repair-layer'`.
|
|
223
|
+
- **Rethrow everything else.** Any other error is a real failure, not
|
|
224
|
+
absence.
|
|
225
|
+
- **Emit one stderr line when falling back.** The two branches differ in
|
|
226
|
+
behavior (see the caveats below); a silent divergence cannot be diagnosed
|
|
227
|
+
from a session transcript.
|
|
228
|
+
- **Keep repair options as pure data.** `satisfies PiToolOwnerAdapterOptions`
|
|
229
|
+
with a type-only import gives full typechecking with no runtime import, so
|
|
230
|
+
the compiled extension activates in installs without the package.
|
|
231
|
+
|
|
232
|
+
The runnable, tested copy of this recipe is
|
|
233
|
+
[`test/fixtures/optional-consumer.ts`](../test/fixtures/optional-consumer.ts);
|
|
234
|
+
the package smoke test exercises both branches in clean projects. Keep the
|
|
235
|
+
snippet above and that fixture in sync when editing either.
|
|
236
|
+
|
|
237
|
+
### Consumer `package.json` shape
|
|
238
|
+
|
|
239
|
+
```jsonc
|
|
240
|
+
{
|
|
241
|
+
"devDependencies": {
|
|
242
|
+
// for typechecking and local tests only; erased from the runtime story
|
|
243
|
+
"@r3b1s/pi-repair-layer": "^0.3.0"
|
|
244
|
+
},
|
|
245
|
+
"peerDependencies": {
|
|
246
|
+
"@r3b1s/pi-repair-layer": ">=0.3.0"
|
|
247
|
+
},
|
|
248
|
+
"peerDependenciesMeta": {
|
|
249
|
+
"@r3b1s/pi-repair-layer": { "optional": true }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`import type` and `satisfies` are erased at compile time, so the
|
|
255
|
+
`devDependencies` entry is all that is needed to build. The optional peer
|
|
256
|
+
declaration documents which versions the extension was written against
|
|
257
|
+
without forcing an install.
|
|
258
|
+
|
|
259
|
+
### Adoption paths
|
|
260
|
+
|
|
261
|
+
**End-user opt-in via pi's shared install root (the default story).** pi
|
|
262
|
+
installs every `npm:`-installed extension into one shared project per scope,
|
|
263
|
+
so all such extensions sit side by side in a flat `node_modules`. When an end
|
|
264
|
+
user runs:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pi install npm:@r3b1s/pi-repair-layer
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
to get repairs for pi's built-in tools, the package also becomes resolvable to
|
|
271
|
+
every npm-installed extension in that scope — and consenting extensions using
|
|
272
|
+
this recipe light up automatically. Nothing is required from the extension
|
|
273
|
+
author beyond the recipe.
|
|
274
|
+
|
|
275
|
+
**Author-controlled `optionalDependencies`.** An author who wants repairs
|
|
276
|
+
whenever the environment can provide them can instead declare:
|
|
277
|
+
|
|
278
|
+
```jsonc
|
|
279
|
+
{
|
|
280
|
+
"optionalDependencies": {
|
|
281
|
+
"@r3b1s/pi-repair-layer": "^0.3.0"
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The package installs alongside the extension when possible, and a failed
|
|
287
|
+
optional install does not fail the extension's install. This is also the path
|
|
288
|
+
for consumers the shared-root story cannot reach (see the caveats).
|
|
289
|
+
|
|
290
|
+
### Caveats
|
|
291
|
+
|
|
292
|
+
- **Install source and scope matter.** The shared `node_modules` is per pi
|
|
293
|
+
scope, and only for `npm:` installs. Git-installed extensions get their own
|
|
294
|
+
clone-local dependency install, and project-scope installs do not resolve
|
|
295
|
+
user-scope siblings (or vice versa). Those consumers fall back even though
|
|
296
|
+
the user installed the package; `optionalDependencies` is the alternative
|
|
297
|
+
there. The layout is observed pi behavior (verified 0.80.6–0.80.10), not a
|
|
298
|
+
documented pi guarantee — see
|
|
299
|
+
[research Claim 10](research.md#claim-10--pi-maintains-one-shared-npm-install-project-per-scope-with-flat-sibling-resolution).
|
|
300
|
+
- **The compiled pi binary never takes the adapter branch today.** Under the
|
|
301
|
+
standalone (Bun-compiled) pi executable, dynamic `import()` cannot resolve
|
|
302
|
+
npm-installed siblings at all, so this recipe falls back — safely, with the
|
|
303
|
+
note — even when the package is installed. The optional pattern currently
|
|
304
|
+
activates only under Node-based pi installs (`npm install -g` /
|
|
305
|
+
`bun install -g`). A hard static dependency resolves under both
|
|
306
|
+
distributions. See
|
|
307
|
+
[research Claim 13](research.md#claim-13--bun-binary-probe-the-optional-dynamic-import-falls-back-under-the-compiled-binary-resolves-under-npm-dist-pi).
|
|
308
|
+
- **Fallback mode is baseline pi, not "repairs minus notes."** Without the
|
|
309
|
+
adapter, pi's native validation runs `Value.Convert` first, which silently
|
|
310
|
+
coerces some invalid input into valid-looking values (`null` → `"null"`)
|
|
311
|
+
instead of repairing or rejecting it. Decide explicitly whether that is
|
|
312
|
+
acceptable for your tool, and test both branches.
|
|
313
|
+
- **No double-wrap.** The installable pi-repair-layer extension only overrides
|
|
314
|
+
pi's built-in tools; it never discovers or wraps tools registered by other
|
|
315
|
+
extensions. A tool adapted by this recipe is wrapped exactly once in either
|
|
316
|
+
branch.
|
|
317
|
+
|
|
318
|
+
### Stability contract for optional consumers
|
|
319
|
+
|
|
320
|
+
Copies of this recipe keep working across versions because the surface it
|
|
321
|
+
touches is deliberately small and covered by the compatibility contract in
|
|
322
|
+
[Install the package](#install-the-package):
|
|
323
|
+
|
|
324
|
+
- The subpath names (`/pi`, `/core`, `/grammar`) and the
|
|
325
|
+
`adaptToolDefinition(definition, options?)` signature are stable for the
|
|
326
|
+
current major; documented exports follow semantic versioning.
|
|
327
|
+
- Absence detection semantics are part of the contract: a missing package
|
|
328
|
+
surfaces with `code` `MODULE_NOT_FOUND` or `ERR_MODULE_NOT_FOUND` and a
|
|
329
|
+
message naming the requested module.
|
|
330
|
+
- Unrecognized preprocessor `kind`s are ignored — never fatal, no mutation, no
|
|
331
|
+
claimed change — and results are still schema-validated. Repair options
|
|
332
|
+
written against a newer minor version therefore degrade to the recognized
|
|
333
|
+
subset when an older version is installed, instead of throwing at
|
|
334
|
+
configuration time.
|
|
335
|
+
|
|
115
336
|
## Configure only known-safe transforms
|
|
116
337
|
|
|
117
338
|
Preprocessors use JSON-Pointer-like selectors. `"/path"` selects a field,
|
|
@@ -321,6 +542,15 @@ At minimum, an owning extension should test:
|
|
|
321
542
|
each and stale notes do not leak to later calls.
|
|
322
543
|
7. Telemetry and persisted entries contain no argument values or note text.
|
|
323
544
|
|
|
545
|
+
Optional consumers (the [Optional integration](#optional-integration) recipe)
|
|
546
|
+
should additionally test both branches:
|
|
547
|
+
|
|
548
|
+
8. With the package absent, activation still succeeds: the raw definition is
|
|
549
|
+
registered and the one-line fallback note is emitted.
|
|
550
|
+
9. Strictly valid input behaves identically in both branches; for invalid
|
|
551
|
+
input, the fallback branch exposes pi's native coercion — confirm that is
|
|
552
|
+
acceptable for your tool.
|
|
553
|
+
|
|
324
554
|
For a working compile-time example, see
|
|
325
555
|
[`test/fixtures/public-consumer.ts`](../test/fixtures/public-consumer.ts). The
|
|
326
556
|
package smoke test installs the packed tarball into a clean project and imports
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r3b1s/pi-repair-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Tool-input repair layer for the pi coding agent: validate-then-repair for built-in tool calls, ported from the behavior of commandcode's repair layer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|