@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,557 @@
1
+ # Integrating repairs into extension-owned tools
2
+
3
+ This guide shows how a pi extension can apply pi-repair-layer's public repair
4
+ pipeline to tools that extension owns. The usual integration is one wrapper
5
+ around the tool definition. A lower-level core API is available when the tool
6
+ owner needs to control the surrounding lifecycle.
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
+
21
+ ## The ownership boundary
22
+
23
+ pi validates tool arguments after calling that tool's `prepareArguments`.
24
+ Because arguments that fail validation never reach `tool_call`, repair must run
25
+ in `prepareArguments` to recover them safely.
26
+
27
+ Only the extension that owns a tool definition can reliably install that hook.
28
+ Installing pi-repair-layer repairs pi's built-in tools, but it does not discover,
29
+ replace, or wrap tools registered by other extensions. Those extensions must
30
+ explicitly use one of the APIs below.
31
+
32
+ ## Install the package
33
+
34
+ Add pi-repair-layer as a dependency of the extension that owns the tool:
35
+
36
+ ```bash
37
+ pnpm add @r3b1s/pi-repair-layer
38
+ ```
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
+
46
+ The public APIs are compiled ESM with TypeScript declarations:
47
+
48
+ - `@r3b1s/pi-repair-layer/pi` provides the pi tool-definition adapter.
49
+ - `@r3b1s/pi-repair-layer/core` provides the pure pipeline, configuration
50
+ types, lifecycle helpers, and repair-note formatting.
51
+ - `@r3b1s/pi-repair-layer/grammar` provides pure leaked-grammar parsing and
52
+ recovery helpers.
53
+
54
+ Do not import files from `src/`; they are not part of the compatibility
55
+ contract. Node 22 or later is required. The verified pi integration baseline is
56
+ 0.80.6.
57
+
58
+ ## Recommended: wrap the tool definition
59
+
60
+ `adaptToolDefinition` returns a copy of the supplied definition with a repair
61
+ aware `prepareArguments`. It preserves the tool's schema and executor and
62
+ chains any `prepareArguments` already supplied by the owner.
63
+
64
+ Once wrapped, bounded envelope recovery and schema-located repairs such as
65
+ dropping an invalid optional `null` or parsing a JSON-stringified array are
66
+ automatic. Exact aliases, cleanup of values that already satisfy the schema,
67
+ and tool-specific shape changes require explicit preprocessors because only the
68
+ tool owner knows where those transformations are semantically safe.
69
+
70
+ ```ts
71
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
72
+ import { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";
73
+ import { Type } from "typebox";
74
+
75
+ const parameters = Type.Object({
76
+ path: Type.String(),
77
+ labels: Type.Optional(Type.Array(Type.String())),
78
+ });
79
+
80
+ export default function myExtension(pi: ExtensionAPI) {
81
+ pi.registerTool(
82
+ adaptToolDefinition(
83
+ {
84
+ name: "inspect_asset",
85
+ label: "Inspect asset",
86
+ description: "Inspect an asset and optionally apply labels",
87
+ parameters,
88
+ async execute(_toolCallId, params) {
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: `Inspecting ${params.path}`,
94
+ },
95
+ ],
96
+ details: undefined,
97
+ };
98
+ },
99
+ },
100
+ {
101
+ policy: "adaptive",
102
+ preprocessors: [
103
+ {
104
+ kind: "alias",
105
+ selector: "/path",
106
+ aliases: ["file_path", "filePath"],
107
+ accepts: "string",
108
+ },
109
+ {
110
+ kind: "filesystem-path",
111
+ selector: "/path",
112
+ },
113
+ {
114
+ kind: "string-or-array",
115
+ selector: "/labels",
116
+ },
117
+ ],
118
+ },
119
+ ),
120
+ );
121
+ }
122
+ ```
123
+
124
+ With this configuration, `{file_path: "/tmp/a"}` becomes
125
+ `{path: "/tmp/a"}`, a markdown auto-link in `path` is unwrapped, and a single
126
+ string in `labels` becomes a one-item array. Each mutation has a stable rule ID
127
+ and explanatory note, and the final result must pass the supplied schema.
128
+
129
+ The adapter fails closed by throwing `UnrepairableToolInputError` with a
130
+ model-readable retry message. `unrepairable: "passthrough"` is available for a
131
+ deliberate compatibility migration, but it can expose the input to pi's native
132
+ conversion and should not be the default.
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
+
336
+ ## Configure only known-safe transforms
337
+
338
+ Preprocessors use JSON-Pointer-like selectors. `"/path"` selects a field,
339
+ `"/edits/*/oldText"` selects a field in every array item, and `""` selects the
340
+ root. Pointer escapes `~0` and `~1` represent `~` and `/` in field names.
341
+
342
+ Available preprocessors are:
343
+
344
+ | Kind | Intended use |
345
+ |---|---|
346
+ | `alias` | Move an explicitly listed alternative key to its canonical location. |
347
+ | `filesystem-path` | Unwrap a markdown auto-link in one path string. |
348
+ | `filesystem-path-array` | Apply path cleanup to string items in one array. |
349
+ | `string-or-array` | Wrap one configured string as a one-item array. |
350
+ | `scalar` | Convert a configured string/number/boolean scalar to the declared scalar type. |
351
+ | `anchor-bleed` | Strip model-gated `^`/`$` artifacts from one configured string. |
352
+ | `grammar-tokens` | Strip model-gated argument tokens at configured keys/values. |
353
+ | `structural` | Apply an owner-supplied shape transformation with a stable rule ID and note. |
354
+
355
+ Alias values can be guarded with `accepts: "string"`, `"number"`,
356
+ `"boolean"`, `"array"`, or `"object"`. By default, an existing canonical
357
+ value wins. Set `emptyEquivalentToMissing: true` only when an empty canonical
358
+ value is invalid or genuinely means “not supplied.”
359
+
360
+ Model-specific transforms require both a matching `modelId` and configured
361
+ `modelFamilies`:
362
+
363
+ ```ts
364
+ {
365
+ policy: "adaptive",
366
+ modelId: "provider/model-id",
367
+ preprocessors: [{
368
+ kind: "anchor-bleed",
369
+ selector: "/path",
370
+ modelFamilies: [/kimi-k2/i, /glm/i],
371
+ }],
372
+ }
373
+ ```
374
+
375
+ Adapter options are captured when the definition is wrapped. If the tool must
376
+ follow model changes during a session, either re-register it with the new model
377
+ ID or use the pure-core pattern below from an owner-managed `prepareArguments`
378
+ that reads current model state for each call.
379
+
380
+ Do not apply path cleanup to arbitrary content or anchor cleanup to regex
381
+ fields. The pipeline deliberately does not guess aliases, fuzzily rename keys,
382
+ or delete unknown fields.
383
+
384
+ ### Policy profiles
385
+
386
+ - `conservative` permits bounded lossless envelope recovery, exact configured
387
+ preprocessing, and schema-guided repair. It observes model-gated valid-value
388
+ artifacts without mutating them.
389
+ - `adaptive` additionally permits schema-validated truncated-object completion
390
+ and configured model-gated value cleanup. This is the default.
391
+ - `recover` has the same tool-argument behavior as adaptive and additionally
392
+ enables gated assistant-text grammar promotion when the installable extension
393
+ is handling `message_end`.
394
+
395
+ The adapter itself does not register grammar hooks or turn assistant text into
396
+ tool calls.
397
+
398
+ ## Existing `prepareArguments` hooks
399
+
400
+ If the definition already has `prepareArguments`, keep it on the definition.
401
+ The adapter calls it after envelope recovery and before configured
402
+ preprocessors and schema repair:
403
+
404
+ ```ts
405
+ pi.registerTool(
406
+ adaptToolDefinition({
407
+ ...definition,
408
+ prepareArguments(input) {
409
+ return migrateMyExtensionVersion(input);
410
+ },
411
+ }),
412
+ );
413
+ ```
414
+
415
+ The owner shim should perform only compatibility work belonging to that tool.
416
+ If it throws, the pipeline preserves the recovered envelope and continues to a
417
+ fail-closed validity decision.
418
+
419
+ ## Optional: receive structured outcomes
420
+
421
+ Use `onOutcome` for metrics or owner-managed feedback. It runs once with the
422
+ same result returned by the pipeline:
423
+
424
+ ```ts
425
+ adaptToolDefinition(definition, {
426
+ onOutcome(result) {
427
+ if (result.outcome === "repaired") {
428
+ recordValueFreeMetric({
429
+ tool: definition.name,
430
+ policy: result.policy,
431
+ rules: result.changes.map((change) => change.ruleId),
432
+ stages: result.changes.map((change) => change.stage),
433
+ fingerprint: result.fingerprint,
434
+ });
435
+ }
436
+ },
437
+ });
438
+ ```
439
+
440
+ `result.args` contains the actual arguments. Do not write it to telemetry or
441
+ session metadata. Rule IDs, stages, policy, outcome, and non-value failure
442
+ fingerprints are suitable for value-free reporting. Callback errors are
443
+ isolated and do not alter the repair verdict.
444
+
445
+ ## Optional: attach `<repair_note>` feedback
446
+
447
+ The adapter repairs arguments, but it cannot attach feedback by itself because
448
+ `prepareArguments` runs before pi creates or exposes the validated call through
449
+ `tool_call`. Tool owners that want model-facing notes can use the exported
450
+ bounded `RepairLifecycle` to bridge that gap:
451
+
452
+ ```ts
453
+ import {
454
+ attachRepairNotes,
455
+ RepairLifecycle,
456
+ } from "@r3b1s/pi-repair-layer/core";
457
+ import { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";
458
+
459
+ const lifecycle = new RepairLifecycle();
460
+ const toolName = definition.name;
461
+
462
+ pi.registerTool(
463
+ adaptToolDefinition(definition, {
464
+ policy: "adaptive",
465
+ preprocessors,
466
+ onOutcome(result) {
467
+ if (result.outcome !== "repaired") return;
468
+ lifecycle.enqueue(toolName, result.args, {
469
+ rules: result.changes.map((change) => change.ruleId),
470
+ notes: result.changes.map((change) => change.note),
471
+ stages: [...new Set(result.changes.map((change) => change.stage))],
472
+ profile: result.policy,
473
+ outcome: "repaired",
474
+ fingerprint: result.fingerprint,
475
+ });
476
+ },
477
+ }),
478
+ );
479
+
480
+ pi.on("tool_call", async (event) => {
481
+ lifecycle.correlate(event.toolName, event.input, event.toolCallId);
482
+ });
483
+
484
+ pi.on("tool_result", async (event) => {
485
+ const feedback = lifecycle.take(event.toolCallId);
486
+ if (!feedback || feedback.notes.length === 0) return undefined;
487
+ return {
488
+ content: attachRepairNotes(event.content, feedback.notes),
489
+ };
490
+ });
491
+
492
+ pi.on("session_shutdown", async () => {
493
+ lifecycle.clear();
494
+ });
495
+ ```
496
+
497
+ The lifecycle uses stable argument serialization, FIFO matching for identical
498
+ concurrent calls, bounded queues, and a TTL. `attachRepairNotes` preserves other
499
+ content and avoids inserting an identical tagged note twice. A custom TUI
500
+ indicator remains the owning extension's responsibility.
501
+
502
+ ## Lower level: call the pure core
503
+
504
+ Use `runRepairPipeline` when the adapter does not fit the tool framework or when
505
+ repair is needed outside a pi session:
506
+
507
+ ```ts
508
+ import { runRepairPipeline } from "@r3b1s/pi-repair-layer/core";
509
+
510
+ const result = runRepairPipeline({
511
+ input: rawArguments,
512
+ config: {
513
+ toolName: "inspect_asset",
514
+ schema: parameters,
515
+ policy: "adaptive",
516
+ preprocessors,
517
+ },
518
+ });
519
+
520
+ if (result.outcome === "unrepairable") {
521
+ throw new Error(result.retryMessage);
522
+ }
523
+
524
+ const validatedArguments = result.args;
525
+ ```
526
+
527
+ The core registers no pi events and performs no filesystem, UI, telemetry,
528
+ network, or persistence work. It does not mutate caller-owned input. A
529
+ `repaired` outcome always means the final arguments passed the supplied schema;
530
+ an `unrepairable` outcome returns the original input and no claimed changes.
531
+
532
+ ## Test the integration
533
+
534
+ At minimum, an owning extension should test:
535
+
536
+ 1. Strictly valid input remains unchanged.
537
+ 2. Every configured alias and selector repair produces valid arguments.
538
+ 3. Unconfigured content and similar-but-unknown keys remain unchanged.
539
+ 4. Unrepairable input throws before the executor runs.
540
+ 5. An existing owner `prepareArguments` is still called.
541
+ 6. If result feedback is enabled, concurrent identical calls receive one note
542
+ each and stale notes do not leak to later calls.
543
+ 7. Telemetry and persisted entries contain no argument values or note text.
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
+
554
+ For a working compile-time example, see
555
+ [`test/fixtures/public-consumer.ts`](../test/fixtures/public-consumer.ts). The
556
+ package smoke test installs the packed tarball into a clean project and imports
557
+ every documented subpath.
package/package.json CHANGED
@@ -1,19 +1,38 @@
1
1
  {
2
2
  "name": "@r3b1s/pi-repair-layer",
3
- "version": "0.2.1",
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",
7
7
  "url": "https://github.com/r3b1s/pi-repair-layer.git"
8
8
  },
9
9
  "files": [
10
- "index.ts",
11
- "src",
10
+ "dist",
11
+ "docs",
12
12
  "README.md",
13
13
  "LICENSE"
14
14
  ],
15
15
  "type": "module",
16
- "main": "src/index.ts",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./core": {
24
+ "types": "./dist/src/core.d.ts",
25
+ "import": "./dist/src/core.js"
26
+ },
27
+ "./pi": {
28
+ "types": "./dist/src/pi.d.ts",
29
+ "import": "./dist/src/pi.js"
30
+ },
31
+ "./grammar": {
32
+ "types": "./dist/src/grammar.d.ts",
33
+ "import": "./dist/src/grammar.js"
34
+ }
35
+ },
17
36
  "license": "MIT",
18
37
  "publishConfig": {
19
38
  "access": "public"
@@ -27,7 +46,7 @@
27
46
  ],
28
47
  "pi": {
29
48
  "extensions": [
30
- "./src/index.ts"
49
+ "./dist/index.js"
31
50
  ]
32
51
  },
33
52
  "dependencies": {
@@ -37,6 +56,7 @@
37
56
  "@biomejs/biome": "2.4.8",
38
57
  "@earendil-works/pi-ai": "^0.80.6",
39
58
  "@earendil-works/pi-coding-agent": "0.80.6",
59
+ "@earendil-works/pi-tui": "0.80.6",
40
60
  "@types/node": "^24.0.0",
41
61
  "eslint": "^9.25.1",
42
62
  "typescript": "^5.9.3",
@@ -44,17 +64,22 @@
44
64
  "vitest": "^4.1.10"
45
65
  },
46
66
  "peerDependencies": {
47
- "@earendil-works/pi-coding-agent": "*"
67
+ "@earendil-works/pi-coding-agent": "*",
68
+ "@earendil-works/pi-tui": "*"
48
69
  },
49
70
  "engines": {
50
71
  "node": ">=22"
51
72
  },
52
73
  "scripts": {
53
74
  "check": "tsc --noEmit",
75
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
54
76
  "lint": "biome check . && eslint src/ test/ index.ts",
55
77
  "test": "vitest run",
78
+ "test:fuzz": "vitest run test/envelope-fuzz.test.ts",
79
+ "test:fuzz:large": "PI_REPAIR_FUZZ_CASES=10000 vitest run test/envelope-fuzz.test.ts",
56
80
  "test:watch": "vitest",
57
81
  "format": "biome check --write .",
82
+ "test:package": "node scripts/package-smoke.mjs",
58
83
  "clean": "rm -rf dist node_modules coverage"
59
84
  }
60
85
  }