@r3b1s/pi-repair-layer 0.2.0 → 0.3.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 +159 -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 +247 -0
  69. package/docs/tool-owner-integration.md +327 -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 -498
  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,327 @@
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
+ ## The ownership boundary
9
+
10
+ pi validates tool arguments after calling that tool's `prepareArguments`.
11
+ Because arguments that fail validation never reach `tool_call`, repair must run
12
+ in `prepareArguments` to recover them safely.
13
+
14
+ Only the extension that owns a tool definition can reliably install that hook.
15
+ Installing pi-repair-layer repairs pi's built-in tools, but it does not discover,
16
+ replace, or wrap tools registered by other extensions. Those extensions must
17
+ explicitly use one of the APIs below.
18
+
19
+ ## Install the package
20
+
21
+ Add pi-repair-layer as a dependency of the extension that owns the tool:
22
+
23
+ ```bash
24
+ pnpm add @r3b1s/pi-repair-layer
25
+ ```
26
+
27
+ The public APIs are compiled ESM with TypeScript declarations:
28
+
29
+ - `@r3b1s/pi-repair-layer/pi` provides the pi tool-definition adapter.
30
+ - `@r3b1s/pi-repair-layer/core` provides the pure pipeline, configuration
31
+ types, lifecycle helpers, and repair-note formatting.
32
+ - `@r3b1s/pi-repair-layer/grammar` provides pure leaked-grammar parsing and
33
+ recovery helpers.
34
+
35
+ Do not import files from `src/`; they are not part of the compatibility
36
+ contract. Node 22 or later is required. The verified pi integration baseline is
37
+ 0.80.6.
38
+
39
+ ## Recommended: wrap the tool definition
40
+
41
+ `adaptToolDefinition` returns a copy of the supplied definition with a repair
42
+ aware `prepareArguments`. It preserves the tool's schema and executor and
43
+ chains any `prepareArguments` already supplied by the owner.
44
+
45
+ Once wrapped, bounded envelope recovery and schema-located repairs such as
46
+ dropping an invalid optional `null` or parsing a JSON-stringified array are
47
+ automatic. Exact aliases, cleanup of values that already satisfy the schema,
48
+ and tool-specific shape changes require explicit preprocessors because only the
49
+ tool owner knows where those transformations are semantically safe.
50
+
51
+ ```ts
52
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
53
+ import { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";
54
+ import { Type } from "typebox";
55
+
56
+ const parameters = Type.Object({
57
+ path: Type.String(),
58
+ labels: Type.Optional(Type.Array(Type.String())),
59
+ });
60
+
61
+ export default function myExtension(pi: ExtensionAPI) {
62
+ pi.registerTool(
63
+ adaptToolDefinition(
64
+ {
65
+ name: "inspect_asset",
66
+ label: "Inspect asset",
67
+ description: "Inspect an asset and optionally apply labels",
68
+ parameters,
69
+ async execute(_toolCallId, params) {
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: `Inspecting ${params.path}`,
75
+ },
76
+ ],
77
+ details: undefined,
78
+ };
79
+ },
80
+ },
81
+ {
82
+ policy: "adaptive",
83
+ preprocessors: [
84
+ {
85
+ kind: "alias",
86
+ selector: "/path",
87
+ aliases: ["file_path", "filePath"],
88
+ accepts: "string",
89
+ },
90
+ {
91
+ kind: "filesystem-path",
92
+ selector: "/path",
93
+ },
94
+ {
95
+ kind: "string-or-array",
96
+ selector: "/labels",
97
+ },
98
+ ],
99
+ },
100
+ ),
101
+ );
102
+ }
103
+ ```
104
+
105
+ With this configuration, `{file_path: "/tmp/a"}` becomes
106
+ `{path: "/tmp/a"}`, a markdown auto-link in `path` is unwrapped, and a single
107
+ string in `labels` becomes a one-item array. Each mutation has a stable rule ID
108
+ and explanatory note, and the final result must pass the supplied schema.
109
+
110
+ The adapter fails closed by throwing `UnrepairableToolInputError` with a
111
+ model-readable retry message. `unrepairable: "passthrough"` is available for a
112
+ deliberate compatibility migration, but it can expose the input to pi's native
113
+ conversion and should not be the default.
114
+
115
+ ## Configure only known-safe transforms
116
+
117
+ Preprocessors use JSON-Pointer-like selectors. `"/path"` selects a field,
118
+ `"/edits/*/oldText"` selects a field in every array item, and `""` selects the
119
+ root. Pointer escapes `~0` and `~1` represent `~` and `/` in field names.
120
+
121
+ Available preprocessors are:
122
+
123
+ | Kind | Intended use |
124
+ |---|---|
125
+ | `alias` | Move an explicitly listed alternative key to its canonical location. |
126
+ | `filesystem-path` | Unwrap a markdown auto-link in one path string. |
127
+ | `filesystem-path-array` | Apply path cleanup to string items in one array. |
128
+ | `string-or-array` | Wrap one configured string as a one-item array. |
129
+ | `scalar` | Convert a configured string/number/boolean scalar to the declared scalar type. |
130
+ | `anchor-bleed` | Strip model-gated `^`/`$` artifacts from one configured string. |
131
+ | `grammar-tokens` | Strip model-gated argument tokens at configured keys/values. |
132
+ | `structural` | Apply an owner-supplied shape transformation with a stable rule ID and note. |
133
+
134
+ Alias values can be guarded with `accepts: "string"`, `"number"`,
135
+ `"boolean"`, `"array"`, or `"object"`. By default, an existing canonical
136
+ value wins. Set `emptyEquivalentToMissing: true` only when an empty canonical
137
+ value is invalid or genuinely means “not supplied.”
138
+
139
+ Model-specific transforms require both a matching `modelId` and configured
140
+ `modelFamilies`:
141
+
142
+ ```ts
143
+ {
144
+ policy: "adaptive",
145
+ modelId: "provider/model-id",
146
+ preprocessors: [{
147
+ kind: "anchor-bleed",
148
+ selector: "/path",
149
+ modelFamilies: [/kimi-k2/i, /glm/i],
150
+ }],
151
+ }
152
+ ```
153
+
154
+ Adapter options are captured when the definition is wrapped. If the tool must
155
+ follow model changes during a session, either re-register it with the new model
156
+ ID or use the pure-core pattern below from an owner-managed `prepareArguments`
157
+ that reads current model state for each call.
158
+
159
+ Do not apply path cleanup to arbitrary content or anchor cleanup to regex
160
+ fields. The pipeline deliberately does not guess aliases, fuzzily rename keys,
161
+ or delete unknown fields.
162
+
163
+ ### Policy profiles
164
+
165
+ - `conservative` permits bounded lossless envelope recovery, exact configured
166
+ preprocessing, and schema-guided repair. It observes model-gated valid-value
167
+ artifacts without mutating them.
168
+ - `adaptive` additionally permits schema-validated truncated-object completion
169
+ and configured model-gated value cleanup. This is the default.
170
+ - `recover` has the same tool-argument behavior as adaptive and additionally
171
+ enables gated assistant-text grammar promotion when the installable extension
172
+ is handling `message_end`.
173
+
174
+ The adapter itself does not register grammar hooks or turn assistant text into
175
+ tool calls.
176
+
177
+ ## Existing `prepareArguments` hooks
178
+
179
+ If the definition already has `prepareArguments`, keep it on the definition.
180
+ The adapter calls it after envelope recovery and before configured
181
+ preprocessors and schema repair:
182
+
183
+ ```ts
184
+ pi.registerTool(
185
+ adaptToolDefinition({
186
+ ...definition,
187
+ prepareArguments(input) {
188
+ return migrateMyExtensionVersion(input);
189
+ },
190
+ }),
191
+ );
192
+ ```
193
+
194
+ The owner shim should perform only compatibility work belonging to that tool.
195
+ If it throws, the pipeline preserves the recovered envelope and continues to a
196
+ fail-closed validity decision.
197
+
198
+ ## Optional: receive structured outcomes
199
+
200
+ Use `onOutcome` for metrics or owner-managed feedback. It runs once with the
201
+ same result returned by the pipeline:
202
+
203
+ ```ts
204
+ adaptToolDefinition(definition, {
205
+ onOutcome(result) {
206
+ if (result.outcome === "repaired") {
207
+ recordValueFreeMetric({
208
+ tool: definition.name,
209
+ policy: result.policy,
210
+ rules: result.changes.map((change) => change.ruleId),
211
+ stages: result.changes.map((change) => change.stage),
212
+ fingerprint: result.fingerprint,
213
+ });
214
+ }
215
+ },
216
+ });
217
+ ```
218
+
219
+ `result.args` contains the actual arguments. Do not write it to telemetry or
220
+ session metadata. Rule IDs, stages, policy, outcome, and non-value failure
221
+ fingerprints are suitable for value-free reporting. Callback errors are
222
+ isolated and do not alter the repair verdict.
223
+
224
+ ## Optional: attach `<repair_note>` feedback
225
+
226
+ The adapter repairs arguments, but it cannot attach feedback by itself because
227
+ `prepareArguments` runs before pi creates or exposes the validated call through
228
+ `tool_call`. Tool owners that want model-facing notes can use the exported
229
+ bounded `RepairLifecycle` to bridge that gap:
230
+
231
+ ```ts
232
+ import {
233
+ attachRepairNotes,
234
+ RepairLifecycle,
235
+ } from "@r3b1s/pi-repair-layer/core";
236
+ import { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";
237
+
238
+ const lifecycle = new RepairLifecycle();
239
+ const toolName = definition.name;
240
+
241
+ pi.registerTool(
242
+ adaptToolDefinition(definition, {
243
+ policy: "adaptive",
244
+ preprocessors,
245
+ onOutcome(result) {
246
+ if (result.outcome !== "repaired") return;
247
+ lifecycle.enqueue(toolName, result.args, {
248
+ rules: result.changes.map((change) => change.ruleId),
249
+ notes: result.changes.map((change) => change.note),
250
+ stages: [...new Set(result.changes.map((change) => change.stage))],
251
+ profile: result.policy,
252
+ outcome: "repaired",
253
+ fingerprint: result.fingerprint,
254
+ });
255
+ },
256
+ }),
257
+ );
258
+
259
+ pi.on("tool_call", async (event) => {
260
+ lifecycle.correlate(event.toolName, event.input, event.toolCallId);
261
+ });
262
+
263
+ pi.on("tool_result", async (event) => {
264
+ const feedback = lifecycle.take(event.toolCallId);
265
+ if (!feedback || feedback.notes.length === 0) return undefined;
266
+ return {
267
+ content: attachRepairNotes(event.content, feedback.notes),
268
+ };
269
+ });
270
+
271
+ pi.on("session_shutdown", async () => {
272
+ lifecycle.clear();
273
+ });
274
+ ```
275
+
276
+ The lifecycle uses stable argument serialization, FIFO matching for identical
277
+ concurrent calls, bounded queues, and a TTL. `attachRepairNotes` preserves other
278
+ content and avoids inserting an identical tagged note twice. A custom TUI
279
+ indicator remains the owning extension's responsibility.
280
+
281
+ ## Lower level: call the pure core
282
+
283
+ Use `runRepairPipeline` when the adapter does not fit the tool framework or when
284
+ repair is needed outside a pi session:
285
+
286
+ ```ts
287
+ import { runRepairPipeline } from "@r3b1s/pi-repair-layer/core";
288
+
289
+ const result = runRepairPipeline({
290
+ input: rawArguments,
291
+ config: {
292
+ toolName: "inspect_asset",
293
+ schema: parameters,
294
+ policy: "adaptive",
295
+ preprocessors,
296
+ },
297
+ });
298
+
299
+ if (result.outcome === "unrepairable") {
300
+ throw new Error(result.retryMessage);
301
+ }
302
+
303
+ const validatedArguments = result.args;
304
+ ```
305
+
306
+ The core registers no pi events and performs no filesystem, UI, telemetry,
307
+ network, or persistence work. It does not mutate caller-owned input. A
308
+ `repaired` outcome always means the final arguments passed the supplied schema;
309
+ an `unrepairable` outcome returns the original input and no claimed changes.
310
+
311
+ ## Test the integration
312
+
313
+ At minimum, an owning extension should test:
314
+
315
+ 1. Strictly valid input remains unchanged.
316
+ 2. Every configured alias and selector repair produces valid arguments.
317
+ 3. Unconfigured content and similar-but-unknown keys remain unchanged.
318
+ 4. Unrepairable input throws before the executor runs.
319
+ 5. An existing owner `prepareArguments` is still called.
320
+ 6. If result feedback is enabled, concurrent identical calls receive one note
321
+ each and stale notes do not leak to later calls.
322
+ 7. Telemetry and persisted entries contain no argument values or note text.
323
+
324
+ For a working compile-time example, see
325
+ [`test/fixtures/public-consumer.ts`](../test/fixtures/public-consumer.ts). The
326
+ package smoke test installs the packed tarball into a clean project and imports
327
+ every documented subpath.
package/package.json CHANGED
@@ -1,19 +1,38 @@
1
1
  {
2
2
  "name": "@r3b1s/pi-repair-layer",
3
- "version": "0.2.0",
3
+ "version": "0.3.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
  }