@r3b1s/pi-repair-layer 0.2.1 → 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 -518
  74. package/src/settings.ts +0 -95
  75. package/src/tables.ts +0 -184
  76. package/src/value-strips.ts +0 -218
package/README.md CHANGED
@@ -1,42 +1,32 @@
1
1
  # pi-repair-layer
2
2
 
3
- A tool-input repair layer for the [pi coding agent](https://github.com/earendil-works/pi), doing
4
- what commandcode's repair layer does: catch the small, finite set of malformed
5
- tool calls that open models emit, fix them at the exact paths the validator
6
- flags, and tell the model what was fixed so it can self-correct next turn.
7
-
8
- No LLM calls, no network, no uploaded telemetry.
9
-
10
- ## Glossary
11
-
12
- Plain-English first, precise meaning after — for readers new to LLM tool-calling.
13
-
14
- - **Tool call** — the model asking the agent to run a tool (read a file, run a
15
- shell command). Concretely, a structured request with a tool name and a JSON
16
- arguments object that the agent validates and executes.
17
- - **Schema / validation** the rulebook for a tool's arguments, and the check
18
- against it. Each built-in tool declares a schema (which fields exist, their
19
- types, which are required); pi validates every call against it and rejects
20
- calls that don't fit.
21
- - **Silent coercion** when a wrong value gets quietly bent into a valid-looking
22
- one and runs anyway. pi's validator runs TypeBox `Value.Convert` first, which
23
- turns `null` into the string `"null"` and `'["a"]'` into `['["a"]']` the call
24
- then passes validation and executes with garbage instead of erroring.
25
- - **Grammar leak** the model prints its tool call as text instead of actually
26
- making one. Different model families use different tool-call "grammars" (XML
27
- tags, sentinel tokens); when one leaks, a block like
28
- `<tool_call>read<arg_key>path</arg_key>…` shows up in the assistant's prose and
29
- no tool runs.
30
- - **Anchor bleed** — regex anchor characters (`^`, `$`) stuck onto a value that
31
- isn't a regex. Some models emit `read {path: "^/x$"}` where the `^`/`$` bled in
32
- from the tool-call grammar and were never meant to be part of the path.
33
- - **Phantom tool call** — the model signals "I'm calling a tool" but sends no
34
- actual call. On some providers the stream ends with a tool-use stop reason but
35
- zero tool-call blocks, leaving the loop with nothing to run. (Out of scope
36
- here — see [Limitations](#limitations).)
37
- - **Repair note** — the short explanation this layer hands back to the model
38
- after fixing a call, so it can self-correct. It rides along as a
39
- `<repair_note>…</repair_note>` line prefixed to the tool result.
3
+ pi-repair-layer repairs the small, predictable tool-call mistakes that can
4
+ derail an otherwise productive [pi](https://github.com/earendil-works/pi) turn.
5
+ Install it for pi's built-in tools, or follow the
6
+ [tool-owner integration guide](docs/tool-owner-integration.md) to bring the
7
+ same repair pipeline to tools in your own extension.
8
+
9
+ No LLM calls. No network requests. No uploaded telemetry.
10
+
11
+ ## Why use it?
12
+
13
+ Models often understand the task but miss a detail of the tool contract:
14
+
15
+ | Model sends | Tool expects |
16
+ |---|---|
17
+ | `{file_path: "/x"}` | `{path: "/x"}` |
18
+ | `{include: "src"}` | `{include: ["src"]}` |
19
+ | `{offset: null}` | omit optional `offset` |
20
+ | `'{"command":"ls"}'` | `{command: "ls"}` |
21
+ | flat `old_string` / `new_string` fields | an `edits` array |
22
+
23
+ Without a pre-validation repair seam, pi may reject the call or convert a bad
24
+ value into valid-looking garbage. pi-repair-layer fixes only configured or
25
+ schema-proven cases, validates the result, executes it, and returns a
26
+ `<repair_note>` so the model learns the correct shape for its next call.
27
+
28
+ It covers pi's `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls` tools.
29
+ Already-valid input takes the fast path and is returned untouched.
40
30
 
41
31
  ## Install
42
32
 
@@ -44,285 +34,161 @@ Plain-English first, precise meaning after — for readers new to LLM tool-calli
44
34
  pi install npm:@r3b1s/pi-repair-layer
45
35
  ```
46
36
 
47
- Or from git:
37
+ Or install directly from GitHub:
48
38
 
49
39
  ```bash
50
40
  pi install git:github.com/r3b1s/pi-repair-layer
51
41
  ```
52
42
 
53
- pi's TUI will show a one-time warning that built-in tools were overridden
54
- that is this extension working as intended.
43
+ pi will show a one-time warning that built-in tools were overridden. The
44
+ extension reuses pi's real schemas, executors, renderers, and runtime modules;
45
+ it adds the pre-validation repair step.
46
+
47
+ ## Terms in plain English
48
+
49
+ - **Tool call** — the model asking the agent to run a tool with structured arguments.
50
+ - **Schema / validation** — the rules for those arguments and the check that they fit.
51
+ - **Silent coercion** — a bad value being converted into a valid-looking but unintended value.
52
+ - **Grammar leak** — the model printing a tool call as text instead of making the call.
53
+ - **Anchor bleed** — stray `^` or `$` grammar characters attached to a non-regex value.
54
+ - **Phantom tool call** — a tool-use signal that contains no actual call to run.
55
+ - **Repair note** — a short explanation returned to the model after a repair.
56
+ - **Fast path** — returning valid input without changing or cloning it.
57
+ - **Path selector** — an address such as `/path` or `/edits/*/oldText` for one configured location.
58
+ - **Preprocessor** — an explicit cleanup that runs at a path selector before validation.
59
+ - **Invariant** — a safety rule that must remain true for every input.
60
+ - **Fail closed** — rejecting input when the pipeline cannot prove a safe, valid repair.
55
61
 
56
- At runtime pi's extension loader aliases `@earendil-works/pi-coding-agent` and
57
- `typebox` to the running instance's own modules, so the extension always binds
58
- to your live pi and its exact validator version. The `node_modules/` here is
59
- only used for the tests and `tsc`.
62
+ ## What it repairs
60
63
 
61
- ```bash
62
- pnpm install # once, if you want to run the tests
63
- pnpm test # pure engine unit tests + end-to-end against pi's real tools
64
+ The default adaptive policy handles:
65
+
66
+ - exact field aliases such as `file_path` `path`, `cmd` `command`, and
67
+ `old_string` → `oldText`;
68
+ - JSON-stringified objects and arrays, singleton object envelopes, and bare
69
+ strings where a configured object or array is expected;
70
+ - invalid optional `null` values and empty object placeholders;
71
+ - markdown auto-links in configured filesystem paths;
72
+ - the legacy flat edit shape used by several coding agents; and
73
+ - model-gated anchor bleed and leaked argument tokens at configured locations.
74
+
75
+ Unknown keys are not fuzzily renamed or generically deleted. See
76
+ [How repair works](docs/how-it-works.md) for the ordered pipeline, complete
77
+ repair catalog, grammar handling, and the reasoning behind the hook placement.
78
+
79
+ ## Safe by design
80
+
81
+ The pipeline recovers the outer argument envelope, chains the tool owner's
82
+ compatibility hook, applies explicitly scoped preprocessing, validates strictly,
83
+ repairs only reported schema issues, and validates again before returning a
84
+ repaired result.
85
+
86
+ - Valid input stays untouched unless an explicitly configured valid-value
87
+ transform applies.
88
+ - Every mutation carries a stable rule ID and model-facing note.
89
+ - Repair work is bounded, deterministic, and covered by property and seeded-fuzz tests.
90
+ - Unrepairable input produces a model-readable retry error rather than `{}` or guessed values.
91
+ - Telemetry stays local and excludes arguments, paths, commands, content, and note text.
92
+ - Turning assistant text into an executable tool call always requires explicit `recover` mode.
93
+
94
+ | Profile | Tool arguments | Grammar text | Promotion |
95
+ |---|---|---|---|
96
+ | `conservative` | exact, schema-guided repair | observe only | never |
97
+ | `adaptive` (default) | adds validated/model-gated recovery | strip known tools | never |
98
+ | `recover` | same as adaptive | strip known tools | safety-gated |
99
+
100
+ Unknown or unavailable tool grammar is preserved by default in every profile.
101
+ The [behavior and safety reference](docs/how-it-works.md) explains the gates and
102
+ invariants in detail.
103
+
104
+ ## Use it in your own extension
105
+
106
+ Tools registered by other extensions are not intercepted automatically: their
107
+ owner must opt in at the `prepareArguments` boundary.
108
+
109
+ ```ts
110
+ import { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";
111
+
112
+ pi.registerTool(
113
+ adaptToolDefinition(definition, {
114
+ policy: "adaptive",
115
+ preprocessors: [
116
+ { kind: "alias", selector: "/path", aliases: ["file_path"] },
117
+ ],
118
+ }),
119
+ );
64
120
  ```
65
121
 
66
- The toolchain is managed with [mise](https://mise.jdx.dev/) (`mise.toml` pins
67
- node/pnpm). `mise run ci` runs typecheck + lint + test.
122
+ The adapter chains an existing owner hook and fails closed by default. The
123
+ [full integration guide](docs/tool-owner-integration.md) covers selectors,
124
+ custom transforms, the side-effect-free `/core` API, call-ID correlation,
125
+ `<repair_note>` attachment, privacy, and integration tests.
68
126
 
69
- ## What it repairs
127
+ Public subpaths are compiled ESM with declarations and source maps:
70
128
 
71
- | Rule | Example | Fix |
72
- |---|---|---|
73
- | `renameAliasedField` | `{file_path: "/x"}` for `read` | `{path: "/x"}` |
74
- | `dropNullOrUndefinedField` | `{path: "/x", offset: null}` | `{path: "/x"}` |
75
- | `dropEmptyObjectPlaceholder` | `{tags: {}}` where array expected | field dropped |
76
- | `parseJsonStringifiedArray` | `{include: '["a","b"]'}` | `{include: ["a","b"]}` |
77
- | `parseJsonStringifiedObject` | stringified object for object field | parsed |
78
- | `wrapBareStringAsArray` | `{include: "foo"}` | `{include: ["foo"]}` |
79
- | `wrapRootStringAsObject` | `"echo hi"` as the whole input to `bash` | `{command: "echo hi"}` |
80
- | `parseJsonStringifiedRootObject` | `'{"command":"ls"}'` as the whole input | parsed |
81
- | `unwrapMarkdownAutoLink` | `path: "/x/[notes.md](http://notes.md)"` | `path: "/x/notes.md"` |
82
- | `foldFlatEditFields` | `{path, old_string, new_string}` for `edit` | `{path, edits: [{oldText, newText}]}` |
83
-
84
- Alias tables cover the contracts models actually leak: Claude Code's
85
- `file_path`/`old_string`/`new_string`, aider's `search`/`replace`, generic
86
- `cmd`/`query`/`text`/`contents`, at any nesting depth (e.g. inside `edits[n]`).
87
-
88
- Every repair is surfaced to the model as a `<repair_note>` prefixed to the tool
89
- result, e.g.:
129
+ - `@r3b1s/pi-repair-layer/pi` tool-owner adapter;
130
+ - `@r3b1s/pi-repair-layer/core` — pure pipeline, policy, lifecycle, and formatting APIs; and
131
+ - `@r3b1s/pi-repair-layer/grammar` pure grammar parsing and recovery helpers.
90
132
 
91
- ```
92
- <repair_note>Renamed `file_path` to `path` for tool "read". `file_path` is not a valid field for this tool — use `path` next time.</repair_note>
93
- ```
133
+ Node 22+ is supported; pi 0.80.6 is the verified baseline. Documented exports
134
+ follow semantic versioning, and the existing `repairToolInput` API remains
135
+ supported for the current major.
94
136
 
95
- Transparency over silent magic: the model sees what was picked and can correct
96
- itself on the next turn.
97
-
98
- ## Model-gated value strips
99
-
100
- Before the validate-then-repair engine runs, a small pre-pass
101
- (`src/value-strips.ts`) cleans two model-specific artifacts that are *valid
102
- strings* and so slip past validation entirely:
103
-
104
- - **Anchor bleed** — leading `^` / trailing `$` bled into a value
105
- (`read {path: "^/x$"}` → `{path: "/x"}`).
106
- - **Grammar-token leaks** — GLM `<arg_key>`/`<arg_value>` markers stuck onto
107
- object keys or values (`{"<arg_key>pattern</arg_key>": "<arg_value>foo</arg_value>"}`
108
- → `{pattern: "foo"}`).
109
-
110
- Both are gated on the current model id — anchor bleed on `kimi-k2` / `minimax` /
111
- `glm`, grammar tokens on `glm` — since these are model-specific quirks. Each
112
- strip emits a `<repair_note>` and telemetry exactly like an engine repair, and
113
- the strips are adapted from [pi-tool-repair](#prior-art). One improvement over
114
- upstream: the anchor strip **skips `grep.pattern`**, the one built-in field
115
- that is a real regex, where a `^`/`$` may be intended syntax and is
116
- indistinguishable from a bled anchor — so it is never guessed at.
117
- (`find.pattern` is a glob, not a regex, so it is *not* exempt.) Whether anchor
118
- bleed happens on pi at all is an open empirical question; shipping the strips
119
- instrumented answers it for free.
120
-
121
- ## Grammar-leak recovery (opt-in)
122
-
123
- When a model prints a tool call as text (a [grammar leak](#glossary)) instead of
124
- emitting a real one, `src/grammar-recovery.ts` (adapted from
125
- [pi-tool-repair](#prior-art), 10 grammar families, code-fence-aware) handles it
126
- on pi's `message_end` hook. Modes, set via `/repair-settings`:
127
-
128
- - **`off`** — never touch assistant text.
129
- - **`strip`** (default) — remove the leaked grammar from the text on gated
130
- models, but never promote it to an executable call.
131
- - **`recover`** — additionally promote the parsed call to a real tool call that
132
- executes the same turn and re-enters this layer's `prepareArguments` repair
133
- path. A recovery note is stashed so the executed call surfaces
134
- `<repair_note>recovered a leaked … tool call…</repair_note>`.
135
-
136
- Because promotion turns model *text* into *execution*, it is guarded: opt-in
137
- `recover` mode, a known-tool allowlist, an empty-arguments skip, role
138
- preservation, and a **stopReason gate** — a call is promoted only when the
139
- original message's `stopReason` is `"stop"`. Upstream promotes regardless and
140
- overwrites `stopReason: "length"`, which would defeat pi's protection that fails
141
- all tool calls on truncated output ([research.md Claim 7][r-claim7]). Stripping
142
- leaked text is allowed on any stopReason; only promotion is gated.
143
-
144
- ## Design: validate-then-repair, hooked pre-validation
145
-
146
- pi's agent loop runs, per tool call ([research.md Claim 1][r-claim1]):
137
+ ## Settings and local telemetry
147
138
 
148
- ```
149
- tool.prepareArguments(raw) → validateToolArguments() → tool_call event → execute
150
- ```
139
+ All telemetry stays on your machine and is used only for debugging and tracing tool-call repairs; nothing is sent or uploaded.
151
140
 
152
- Every mechanical claim in this section is verified against pi's source, with
153
- file:line citations and a verification date, in
154
- [`docs/research.md`](docs/research.md); the linked claim numbers point at the
155
- specific entries. Automated tripwires in `test/upstream-drift.test.ts` execute
156
- these claims against the installed pi so a pi upgrade can't invalidate them
157
- silently.
158
-
159
- Two consequences drove the architecture:
160
-
161
- 1. **The `tool_call` extension event fires *after* validation**
162
- ([research.md Claims 1–2][r-claim1]). A validation
163
- failure short-circuits to an error result before any event handler runs, so
164
- an extension listening on `tool_call` can never see — let alone repair —
165
- malformed input. (This is why repair extensions built on that hook can't
166
- work for built-in tools.) The only pre-validation seam is
167
- `prepareArguments`, which extensions reach by overriding a built-in tool via
168
- `pi.registerTool({ same name })`. Each override here spreads the original
169
- definition (`createReadToolDefinition(cwd)` etc. are exported from pi's
170
- root), so renderers, prompt metadata, and execution are the real built-ins;
171
- only `prepareArguments` is chained and `execute` thinly wrapped for notes.
172
-
173
- 2. **pi's own coercion (`Value.Convert`) silently corrupts the classic failure
174
- modes.** Measured on typebox 1.1.38, which pi validates with:
175
-
176
- | Model sends | Convert produces | Then |
177
- |---|---|---|
178
- | `'["a","b"]'` for an array | `['["a","b"]']` | passes validation, executes with garbage |
179
- | `null` for an optional string | `"null"` | passes, e.g. greps for the string `null` |
180
- | `null` for a required `path` | `"null"` | passes, reads a file named `null` |
181
- | `null` for an optional number | `0` | passes, offset silently zero |
182
-
183
- So this engine checks **strictly first** (no Convert) and repairs at the
184
- strict-error sites before Convert can mangle them. Benign coercions
185
- (`"5"` → `5`) still fall through to pi's native behavior untouched. The
186
- repair layer therefore doesn't just recover calls that would have errored —
187
- it fixes calls pi currently executes *wrongly*.
188
-
189
- The engine itself follows the validate-then-repair shape commandcode's author
190
- described publicly: parse as-is; if it succeeds, ship it untouched (fast path
191
- returns the original object by reference); on failure, walk the validator's own
192
- issue list and spend repair budget only at the paths the schema disagreed with;
193
- re-validate; never ship a repair that doesn't verify. Rule order is fixed —
194
- JSON-array parsing before bare-string wrapping, renames before null-drops.
195
-
196
- The one deliberate preprocess (not validate-then-repair) is markdown auto-link
197
- unwrapping on path fields, because `"/x/[notes.md](http://notes.md)"` is a
198
- perfectly valid string that validation can never flag. It unwraps only the
199
- degenerate case where the link text equals the URL minus its protocol; real
200
- markdown links pass through, and content fields are never touched.
201
-
202
- ## Telemetry (local only)
203
-
204
- Repair outcomes append to `~/.pi/agent/tool-repair/telemetry.jsonl`: timestamp,
205
- tool, model id, outcome, rules fired, an issue summary, and an FNV-1a
206
- fingerprint of the (tool, failure-shape) pair — the same shape-fingerprint
207
- trick commandcode uses to count distinct failure signatures. Inputs themselves
208
- are never logged.
209
-
210
- Records come on two channels. The **tool channel** (the default; records with no
211
- `channel` field, so old logs read unchanged) keys on a tool, with `outcome` one
212
- of `repaired`, `unrepairable`, or `recovered` (a promoted grammar-leak call).
213
- The **message channel** (`channel: "message"`) records grammar strip-only events
214
- — a leak removed with nothing promoted, so there is no tool to key on — with the
215
- grammar family that was stripped. `/repair-stats` summarizes both:
141
+ - `/repair-settings` changes the policy, grammar mode, unknown-tool text policy,
142
+ TUI indicator, and visible repair notes.
143
+ - `/repair-stats` summarizes local repair outcomes by model, tool, and rule.
144
+ - `PI_TOOL_REPAIR_LOG=1` prints decisions to stderr.
145
+ - `PI_TOOL_REPAIR_TELEMETRY=off` disables local telemetry.
146
+ - `PI_TOOL_REPAIR_PASSTHROUGH=1` restores pi's native behavior for unrepairable input.
216
147
 
217
- ```
218
- /repair-stats
219
- ```
148
+ Settings persist under `~/.pi/agent/tool-repair/`. See the
149
+ [operations guide](docs/operations.md) for paths, record contents, privacy,
150
+ diagnostics, and verification commands.
220
151
 
221
- It reports repairs/recoveries by tool and by model, grammar strip-only events by
222
- family, and a rules-fired tally. Watch per-model rates to catch a model
223
- regressing on a specific contract.
152
+ ## Documentation
224
153
 
225
- Env switches: `PI_TOOL_REPAIR_LOG=1` logs decisions to stderr;
226
- `PI_TOOL_REPAIR_TELEMETRY=off` disables telemetry (or set it to a path).
154
+ - [Integrating extension-owned tools](docs/tool-owner-integration.md)
155
+ - [How repair works: behavior, safety, and limitations](docs/how-it-works.md)
156
+ - [Operations, settings, telemetry, and verification](docs/operations.md)
157
+ - [Source-backed pi integration research](docs/research.md)
227
158
 
228
- ## Chaos test (deterministic live exercise)
159
+ ## Prior art
229
160
 
230
- `test/run-chaos.sh` + `test/chaos-provider.ts` register a scripted fake
231
- provider that replays canned malformed tool calls through pi's real agent loop
232
- (print mode via the `pi` binary, no network, no tokens), then reports the
233
- `<repair_note>` lines the "model" saw and asserts on them. It exercises the full
234
- path end-to-end — streaming, preflight, validation, repair, execution, and note
235
- surfacing — and passes against pi 0.80.6:
161
+ The value-strip rules and grammar-recovery approach originate in Tom X Nguyen's
162
+ MIT-licensed [`monotykamary/pi-tool-repair`](https://github.com/monotykamary/pi-tool-repair).
163
+ This project adapted its anchor/token cleanup and grammar parsers, then scoped
164
+ the transforms, added reporting, and gated executable promotion.
236
165
 
237
- ```bash
238
- test/run-chaos.sh
239
- ```
166
+ The key integration difference is mechanical: pi runs
167
+ `prepareArguments → validation → tool_call → execute`. A malformed call fails
168
+ before `tool_call`, so this extension hooks the owning tool's
169
+ `prepareArguments`; the sequence and propagation behavior are verified in
170
+ [research Claims 1–4](docs/research.md#claim-1--loop-ordering-preparearguments-runs-before-validation-which-runs-before-the-tool_call-event).
171
+ See [How repair works](docs/how-it-works.md#prior-art-and-hook-placement) for the
172
+ full adaptation notes and comparison.
240
173
 
241
- (An earlier pi release resolved the stream function before an extension's
242
- custom provider finished registering, so this hung; 0.80.6 streams through the
243
- registered provider correctly.) The `test/upstream-drift.test.ts` suite drives
244
- the same real loop fully in-process (via pi-ai's faux provider), so the loop
245
- behaviors this layer depends on are covered by `pnpm test` too, without the
246
- `pi` binary.
247
-
248
- ## Display settings
249
-
250
- Repaired tool calls get a `🔨 ✓ input repaired (rules...)` line appended to
251
- their result row in the TUI; optionally the repair note text renders beneath
252
- it. `/repair-settings` toggles the indicator and the note text independently,
253
- and cycles the grammar-leak recovery mode (`off → strip → recover`; default
254
- `strip`). All three persist to `~/.pi/agent/tool-repair/settings.json` (or
255
- `PI_TOOL_REPAIR_SETTINGS=<path>`), alongside an optional `grammarAllowedTools`
256
- list that restricts which tool names a leaked call may be promoted to (when
257
- empty, the active tool set is used). Indicators are persisted per session via
258
- custom entries, so they survive `/reload` and session resume.
259
-
260
- ## Unrepairable input
261
-
262
- When repairs can't make the input valid, the layer raises a model-readable
263
- retry error (`Invalid input for tool "write". Fix these issues and retry: ...`)
264
- instead of passing the input through — passing through would let pi's
265
- `Value.Convert` coerce it into garbage (`content: null` becomes the literal
266
- string `"null"` on disk) and execute anyway. Set `PI_TOOL_REPAIR_PASSTHROUGH=1`
267
- to restore pi's native behavior.
174
+ ## Limitations
268
175
 
269
- ## Prior art
176
+ - Another extension's tools require owner opt-in through `/pi` or `/core`.
177
+ - Two extensions overriding the same built-in do not compose; load order wins.
178
+ - Similar but unconfigured aliases are not guessed.
179
+ - Phantom tool calls are not synthesized into calls.
180
+ - Grammar promotion is limited to recognized, non-empty calls for active or
181
+ explicitly allowed tools and never runs on truncated output.
270
182
 
271
- The value strips and the grammar-leak recovery approach come from
272
- [`monotykamary/pi-tool-repair`](https://github.com/monotykamary/pi-tool-repair)
273
- (MIT), Tom X Nguyen's pi extension. What this project adapted from it:
274
-
275
- - The anchor-bleed and grammar-token-leak strips (`src/value-strips.ts`),
276
- reworked to run inside `prepareArguments`, to skip the `grep.pattern` regex
277
- field, and to report through this layer's repair-note / telemetry machinery.
278
- - The grammar-leak recovery parsers, candidate selection, code-fence handling,
279
- and range removal (`src/grammar-recovery.ts`, ~10 grammar families), adapted
280
- in place with a one-line provenance header, plus the new `stopReason`
281
- promotion gate.
282
-
283
- The two projects also share a common Command Code lineage in their
284
- validate-then-repair engines (near-identical rule names and alias tables).
285
-
286
- Where they differ is **which hook reaches a malformed built-in call**, and that
287
- is mechanical, not a matter of quality. pi runs
288
- `prepareArguments → validateToolArguments → tool_call event → execute`
289
- ([research.md Claim 1][r-claim1]). A malformed built-in call makes
290
- `validateToolArguments` **throw** before the `tool_call` event ever fires
291
- ([Claim 2][r-claim1]), so a repair that keys on `tool_call` cannot see it; and
292
- even for a call that does pass, the `tool_call` event only propagates a handler's
293
- *in-place* mutation of `event.input`, never a reassignment ([Claim 3][r-claim3]).
294
- pi-tool-repair's built-in repair keys on `tool_call`; this project overrides the
295
- built-in tool so its repair runs in `prepareArguments`, the one seam ahead of
296
- validation. That is why the two hooks reach different cases — see the cited
297
- research entries for the source. Conversely, the capabilities assimilated here
298
- (value strips, grammar recovery) live on hooks that *do* propagate their results
299
- (`prepareArguments`, `message_end` — [Claim 4][r-claim4]), which is why adapting
300
- them is sound.
301
-
302
- [r-claim1]: docs/research.md#claim-1--loop-ordering-preparearguments-runs-before-validation-which-runs-before-the-tool_call-event
303
- [r-claim3]: docs/research.md#claim-3--tool_call-event-in-place-input-mutation-propagates-reassignment-does-not
304
- [r-claim4]: docs/research.md#claim-4--message_end-replacement-mutates-the-loops-message-in-place-listeners-are-awaited-and-the-replacement-executes-same-turn-with-a-role-guard
305
- [r-claim7]: docs/research.md#claim-7--stopreason-length-causes-pi-to-fail-all-tool-calls-in-the-message
183
+ ## Development
306
184
 
307
- ## Limitations
185
+ ```bash
186
+ pnpm install
187
+ mise run ci # typecheck + lint + tests
188
+ pnpm run test:package # packed clean-consumer smoke test
189
+ pnpm run test:fuzz # deterministic bounded fuzz campaign
190
+ test/run-chaos.sh # scripted real pi-loop exercise
191
+ ```
308
192
 
309
- - Only pi's seven built-in tools are wrapped. Custom tools from other
310
- extensions aren't (pi has no API to wrap another extension's execute), but
311
- they can import `repairToolInput` from `src/repair-engine.ts` and use it in
312
- their own `prepareArguments`.
313
- - Wrong-but-optional fields are invisible to validation by design: pi's schemas
314
- allow extra keys, so `grep {pattern, directory: "src"}` validates and
315
- silently searches the cwd. No validate-then-repair layer can catch this
316
- (commandcode's included); it would take strict schemas or key-similarity
317
- heuristics, both with false-positive costs.
318
- - Unrepairable input falls through to pi's stock validation error, which is
319
- already model-readable (per-path issues plus the received arguments).
320
- - If another extension also overrides a built-in tool, load order decides which
321
- override wins — they don't compose.
322
- - Phantom tool calls (a tool-use stop reason with zero tool-call blocks) are not
323
- handled. On pi 0.80.6 that state is a clean terminal stop, not a recoverable
324
- retry ([research.md Claim 6][r-claim6]), so normalizing it would be cosmetic;
325
- it will be revisited only if a concrete stuck state is reproduced on a
326
- provider.
327
-
328
- [r-claim6]: docs/research.md#claim-6--stopreason-error-and-aborted-is-terminal-not-a-retry
193
+ Larger fuzz runs and replay instructions are in the
194
+ [operations guide](docs/operations.md#verification).
@@ -0,0 +1,2 @@
1
+ export { default } from "./src/index.ts";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}
@@ -1,2 +1,3 @@
1
1
  // Entry point for pi's extension auto-discovery (~/.pi/agent/extensions/*/index.ts).
2
- export { default } from "./src/index.ts";
2
+ export { default } from "./src/index.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { DEFAULT_ENVELOPE_LIMITS, type EnvelopeLimits, type EnvelopeRecoveryResult, escapeJsonStringControlCharacters, isPlainObject, recoverEnvelope, truncatedEnvelopeChange, } from "./envelope.ts";
2
+ export { attachRepairNotes, formatRepairNotes, type RepairFeedback, RepairLifecycle, type RepairLifecycleOptions, stableSerialize, } from "./lifecycle.ts";
3
+ export { runRepairPipeline } from "./pipeline.ts";
4
+ export { type GrammarPolicyMode, type RepairPolicy, type RepairPolicyOverrides, type RepairPolicyProfile, resolveRepairPolicy, type UnknownGrammarTextPolicy, } from "./policy.ts";
5
+ export { type AliasPreprocessor, type FieldPreprocessor, type ObjectLocationSelector, type Preprocessor, preprocessInput, type StructuralPreprocessor, } from "./preprocess.ts";
6
+ export { type RepairResult, repairToolInput, type StructuralRepair, type ToolRepairConfig, unwrapMarkdownAutoLinks, } from "./repair-engine.ts";
7
+ export type { RepairChange, RepairObservation, RepairPipelineConfig, RepairPipelineLimits, RepairPipelineResult, RepairStage, } from "./types.ts";
8
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,iCAAiC,EACjC,aAAa,EACb,eAAe,EACf,uBAAuB,GACxB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,cAAc,EACnB,eAAe,EACf,KAAK,sBAAsB,EAC3B,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,mBAAmB,EACnB,KAAK,wBAAwB,GAC9B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,eAAe,EACf,KAAK,sBAAsB,GAC5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,YAAY,EACjB,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,WAAW,GACZ,MAAM,YAAY,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { DEFAULT_ENVELOPE_LIMITS, escapeJsonStringControlCharacters, isPlainObject, recoverEnvelope, truncatedEnvelopeChange, } from "./envelope.js";
2
+ export { attachRepairNotes, formatRepairNotes, RepairLifecycle, stableSerialize, } from "./lifecycle.js";
3
+ export { runRepairPipeline } from "./pipeline.js";
4
+ export { resolveRepairPolicy, } from "./policy.js";
5
+ export { preprocessInput, } from "./preprocess.js";
6
+ export { repairToolInput, unwrapMarkdownAutoLinks, } from "./repair-engine.js";
7
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EAGvB,iCAAiC,EACjC,aAAa,EACb,eAAe,EACf,uBAAuB,GACxB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EAEjB,eAAe,EAEf,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAKL,mBAAmB,GAEpB,MAAM,aAAa,CAAC;AACrB,OAAO,EAKL,eAAe,GAEhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAEL,eAAe,EAGf,uBAAuB,GACxB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { RepairChange, RepairPipelineLimits } from "./types.ts";
2
+ export interface EnvelopeLimits {
3
+ maxInputBytes: number;
4
+ maxNestingDepth: number;
5
+ maxDecodeAttempts: number;
6
+ maxCandidates: number;
7
+ maxWorkMs: number;
8
+ }
9
+ export declare const DEFAULT_ENVELOPE_LIMITS: EnvelopeLimits;
10
+ export interface EnvelopeRecoveryResult {
11
+ value: unknown;
12
+ changes: RepairChange[];
13
+ candidates: unknown[];
14
+ limited: boolean;
15
+ }
16
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
17
+ /** Escape only literal JSON control characters that occur inside a string. */
18
+ export declare function escapeJsonStringControlCharacters(text: string): string;
19
+ export declare function recoverEnvelope(input: unknown, overrides?: RepairPipelineLimits): EnvelopeRecoveryResult;
20
+ export declare function truncatedEnvelopeChange(): RepairChange;
21
+ //# sourceMappingURL=envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../../src/envelope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAErE,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,uBAAuB,EAAE,cAMrC,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,UAAU,EAAE,OAAO,EAAE,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMlC;AAiCD,8EAA8E;AAC9E,wBAAgB,iCAAiC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmCtE;AAgBD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,OAAO,EACd,SAAS,GAAE,oBAAyB,GACnC,sBAAsB,CA4ExB;AAED,wBAAgB,uBAAuB,IAAI,YAAY,CAKtD"}