@r3b1s/pi-repair-layer 0.1.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/LICENSE +21 -0
- package/README.md +323 -0
- package/index.ts +2 -0
- package/package.json +60 -0
- package/src/grammar-recovery.ts +1269 -0
- package/src/index.ts +621 -0
- package/src/repair-engine.ts +498 -0
- package/src/settings.ts +95 -0
- package/src/tables.ts +184 -0
- package/src/value-strips.ts +218 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 r3b1s
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# pi-repair-layer
|
|
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.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ln -s /home/dev/Local/personal/pi-commandcode-conundrum ~/.pi/agent/extensions/pi-repair-layer
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
pi auto-discovers `~/.pi/agent/extensions/*/index.ts`. Remove the symlink to
|
|
48
|
+
uninstall. pi's TUI will show a one-time warning that built-in tools were
|
|
49
|
+
overridden — that is this extension working as intended.
|
|
50
|
+
|
|
51
|
+
At runtime pi's extension loader aliases `@earendil-works/pi-coding-agent` and
|
|
52
|
+
`typebox` to the running instance's own modules, so the extension always binds
|
|
53
|
+
to your live pi and its exact validator version. The `node_modules/` here is
|
|
54
|
+
only used for the tests and `tsc`.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm install # once, if you want to run the tests
|
|
58
|
+
pnpm test # pure engine unit tests + end-to-end against pi's real tools
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The toolchain is managed with [mise](https://mise.jdx.dev/) (`mise.toml` pins
|
|
62
|
+
node/pnpm). `mise run ci` runs typecheck + lint + test.
|
|
63
|
+
|
|
64
|
+
## What it repairs
|
|
65
|
+
|
|
66
|
+
| Rule | Example | Fix |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `renameAliasedField` | `{file_path: "/x"}` for `read` | `{path: "/x"}` |
|
|
69
|
+
| `dropNullOrUndefinedField` | `{path: "/x", offset: null}` | `{path: "/x"}` |
|
|
70
|
+
| `dropEmptyObjectPlaceholder` | `{tags: {}}` where array expected | field dropped |
|
|
71
|
+
| `parseJsonStringifiedArray` | `{include: '["a","b"]'}` | `{include: ["a","b"]}` |
|
|
72
|
+
| `parseJsonStringifiedObject` | stringified object for object field | parsed |
|
|
73
|
+
| `wrapBareStringAsArray` | `{include: "foo"}` | `{include: ["foo"]}` |
|
|
74
|
+
| `wrapRootStringAsObject` | `"echo hi"` as the whole input to `bash` | `{command: "echo hi"}` |
|
|
75
|
+
| `parseJsonStringifiedRootObject` | `'{"command":"ls"}'` as the whole input | parsed |
|
|
76
|
+
| `unwrapMarkdownAutoLink` | `path: "/x/[notes.md](http://notes.md)"` | `path: "/x/notes.md"` |
|
|
77
|
+
| `foldFlatEditFields` | `{path, old_string, new_string}` for `edit` | `{path, edits: [{oldText, newText}]}` |
|
|
78
|
+
|
|
79
|
+
Alias tables cover the contracts models actually leak: Claude Code's
|
|
80
|
+
`file_path`/`old_string`/`new_string`, aider's `search`/`replace`, generic
|
|
81
|
+
`cmd`/`query`/`text`/`contents`, at any nesting depth (e.g. inside `edits[n]`).
|
|
82
|
+
|
|
83
|
+
Every repair is surfaced to the model as a `<repair_note>` prefixed to the tool
|
|
84
|
+
result, e.g.:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
<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>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Transparency over silent magic: the model sees what was picked and can correct
|
|
91
|
+
itself on the next turn.
|
|
92
|
+
|
|
93
|
+
## Model-gated value strips
|
|
94
|
+
|
|
95
|
+
Before the validate-then-repair engine runs, a small pre-pass
|
|
96
|
+
(`src/value-strips.ts`) cleans two model-specific artifacts that are *valid
|
|
97
|
+
strings* and so slip past validation entirely:
|
|
98
|
+
|
|
99
|
+
- **Anchor bleed** — leading `^` / trailing `$` bled into a value
|
|
100
|
+
(`read {path: "^/x$"}` → `{path: "/x"}`).
|
|
101
|
+
- **Grammar-token leaks** — GLM `<arg_key>`/`<arg_value>` markers stuck onto
|
|
102
|
+
object keys or values (`{"<arg_key>pattern</arg_key>": "<arg_value>foo</arg_value>"}`
|
|
103
|
+
→ `{pattern: "foo"}`).
|
|
104
|
+
|
|
105
|
+
Both are gated on the current model id — anchor bleed on `kimi-k2` / `minimax` /
|
|
106
|
+
`glm`, grammar tokens on `glm` — since these are model-specific quirks. Each
|
|
107
|
+
strip emits a `<repair_note>` and telemetry exactly like an engine repair, and
|
|
108
|
+
the strips are adapted from [pi-tool-repair](#prior-art). One improvement over
|
|
109
|
+
upstream: the anchor strip **skips `grep.pattern`**, the one built-in field
|
|
110
|
+
that is a real regex, where a `^`/`$` may be intended syntax and is
|
|
111
|
+
indistinguishable from a bled anchor — so it is never guessed at.
|
|
112
|
+
(`find.pattern` is a glob, not a regex, so it is *not* exempt.) Whether anchor
|
|
113
|
+
bleed happens on pi at all is an open empirical question; shipping the strips
|
|
114
|
+
instrumented answers it for free.
|
|
115
|
+
|
|
116
|
+
## Grammar-leak recovery (opt-in)
|
|
117
|
+
|
|
118
|
+
When a model prints a tool call as text (a [grammar leak](#glossary)) instead of
|
|
119
|
+
emitting a real one, `src/grammar-recovery.ts` (adapted from
|
|
120
|
+
[pi-tool-repair](#prior-art), 10 grammar families, code-fence-aware) handles it
|
|
121
|
+
on pi's `message_end` hook. Modes, set via `/repair-settings`:
|
|
122
|
+
|
|
123
|
+
- **`off`** — never touch assistant text.
|
|
124
|
+
- **`strip`** (default) — remove the leaked grammar from the text on gated
|
|
125
|
+
models, but never promote it to an executable call.
|
|
126
|
+
- **`recover`** — additionally promote the parsed call to a real tool call that
|
|
127
|
+
executes the same turn and re-enters this layer's `prepareArguments` repair
|
|
128
|
+
path. A recovery note is stashed so the executed call surfaces
|
|
129
|
+
`<repair_note>recovered a leaked … tool call…</repair_note>`.
|
|
130
|
+
|
|
131
|
+
Because promotion turns model *text* into *execution*, it is guarded: opt-in
|
|
132
|
+
`recover` mode, a known-tool allowlist, an empty-arguments skip, role
|
|
133
|
+
preservation, and a **stopReason gate** — a call is promoted only when the
|
|
134
|
+
original message's `stopReason` is `"stop"`. Upstream promotes regardless and
|
|
135
|
+
overwrites `stopReason: "length"`, which would defeat pi's protection that fails
|
|
136
|
+
all tool calls on truncated output ([research.md Claim 7][r-claim7]). Stripping
|
|
137
|
+
leaked text is allowed on any stopReason; only promotion is gated.
|
|
138
|
+
|
|
139
|
+
## Design: validate-then-repair, hooked pre-validation
|
|
140
|
+
|
|
141
|
+
pi's agent loop runs, per tool call ([research.md Claim 1][r-claim1]):
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
tool.prepareArguments(raw) → validateToolArguments() → tool_call event → execute
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Every mechanical claim in this section is verified against pi's source, with
|
|
148
|
+
file:line citations and a verification date, in
|
|
149
|
+
[`docs/research.md`](docs/research.md); the linked claim numbers point at the
|
|
150
|
+
specific entries. Automated tripwires in `test/upstream-drift.test.ts` execute
|
|
151
|
+
these claims against the installed pi so a pi upgrade can't invalidate them
|
|
152
|
+
silently.
|
|
153
|
+
|
|
154
|
+
Two consequences drove the architecture:
|
|
155
|
+
|
|
156
|
+
1. **The `tool_call` extension event fires *after* validation**
|
|
157
|
+
([research.md Claims 1–2][r-claim1]). A validation
|
|
158
|
+
failure short-circuits to an error result before any event handler runs, so
|
|
159
|
+
an extension listening on `tool_call` can never see — let alone repair —
|
|
160
|
+
malformed input. (This is why repair extensions built on that hook can't
|
|
161
|
+
work for built-in tools.) The only pre-validation seam is
|
|
162
|
+
`prepareArguments`, which extensions reach by overriding a built-in tool via
|
|
163
|
+
`pi.registerTool({ same name })`. Each override here spreads the original
|
|
164
|
+
definition (`createReadToolDefinition(cwd)` etc. are exported from pi's
|
|
165
|
+
root), so renderers, prompt metadata, and execution are the real built-ins;
|
|
166
|
+
only `prepareArguments` is chained and `execute` thinly wrapped for notes.
|
|
167
|
+
|
|
168
|
+
2. **pi's own coercion (`Value.Convert`) silently corrupts the classic failure
|
|
169
|
+
modes.** Measured on typebox 1.1.38, which pi validates with:
|
|
170
|
+
|
|
171
|
+
| Model sends | Convert produces | Then |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `'["a","b"]'` for an array | `['["a","b"]']` | passes validation, executes with garbage |
|
|
174
|
+
| `null` for an optional string | `"null"` | passes, e.g. greps for the string `null` |
|
|
175
|
+
| `null` for a required `path` | `"null"` | passes, reads a file named `null` |
|
|
176
|
+
| `null` for an optional number | `0` | passes, offset silently zero |
|
|
177
|
+
|
|
178
|
+
So this engine checks **strictly first** (no Convert) and repairs at the
|
|
179
|
+
strict-error sites before Convert can mangle them. Benign coercions
|
|
180
|
+
(`"5"` → `5`) still fall through to pi's native behavior untouched. The
|
|
181
|
+
repair layer therefore doesn't just recover calls that would have errored —
|
|
182
|
+
it fixes calls pi currently executes *wrongly*.
|
|
183
|
+
|
|
184
|
+
The engine itself follows the validate-then-repair shape commandcode's author
|
|
185
|
+
described publicly: parse as-is; if it succeeds, ship it untouched (fast path
|
|
186
|
+
returns the original object by reference); on failure, walk the validator's own
|
|
187
|
+
issue list and spend repair budget only at the paths the schema disagreed with;
|
|
188
|
+
re-validate; never ship a repair that doesn't verify. Rule order is fixed —
|
|
189
|
+
JSON-array parsing before bare-string wrapping, renames before null-drops.
|
|
190
|
+
|
|
191
|
+
The one deliberate preprocess (not validate-then-repair) is markdown auto-link
|
|
192
|
+
unwrapping on path fields, because `"/x/[notes.md](http://notes.md)"` is a
|
|
193
|
+
perfectly valid string that validation can never flag. It unwraps only the
|
|
194
|
+
degenerate case where the link text equals the URL minus its protocol; real
|
|
195
|
+
markdown links pass through, and content fields are never touched.
|
|
196
|
+
|
|
197
|
+
## Telemetry (local only)
|
|
198
|
+
|
|
199
|
+
Repair outcomes append to `~/.pi/agent/tool-repair/telemetry.jsonl`: timestamp,
|
|
200
|
+
tool, model id, outcome, rules fired, an issue summary, and an FNV-1a
|
|
201
|
+
fingerprint of the (tool, failure-shape) pair — the same shape-fingerprint
|
|
202
|
+
trick commandcode uses to count distinct failure signatures. Inputs themselves
|
|
203
|
+
are never logged.
|
|
204
|
+
|
|
205
|
+
Records come on two channels. The **tool channel** (the default; records with no
|
|
206
|
+
`channel` field, so old logs read unchanged) keys on a tool, with `outcome` one
|
|
207
|
+
of `repaired`, `unrepairable`, or `recovered` (a promoted grammar-leak call).
|
|
208
|
+
The **message channel** (`channel: "message"`) records grammar strip-only events
|
|
209
|
+
— a leak removed with nothing promoted, so there is no tool to key on — with the
|
|
210
|
+
grammar family that was stripped. `/repair-stats` summarizes both:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
/repair-stats
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
It reports repairs/recoveries by tool and by model, grammar strip-only events by
|
|
217
|
+
family, and a rules-fired tally. Watch per-model rates to catch a model
|
|
218
|
+
regressing on a specific contract.
|
|
219
|
+
|
|
220
|
+
Env switches: `PI_TOOL_REPAIR_LOG=1` logs decisions to stderr;
|
|
221
|
+
`PI_TOOL_REPAIR_TELEMETRY=off` disables telemetry (or set it to a path).
|
|
222
|
+
|
|
223
|
+
## Chaos test (deterministic live exercise)
|
|
224
|
+
|
|
225
|
+
`test/run-chaos.sh` + `test/chaos-provider.ts` register a scripted fake
|
|
226
|
+
provider that replays canned malformed tool calls through pi's real agent loop
|
|
227
|
+
(print mode via the `pi` binary, no network, no tokens), then reports the
|
|
228
|
+
`<repair_note>` lines the "model" saw and asserts on them. It exercises the full
|
|
229
|
+
path end-to-end — streaming, preflight, validation, repair, execution, and note
|
|
230
|
+
surfacing — and passes against pi 0.80.6:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
test/run-chaos.sh
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
(An earlier pi release resolved the stream function before an extension's
|
|
237
|
+
custom provider finished registering, so this hung; 0.80.6 streams through the
|
|
238
|
+
registered provider correctly.) The `test/upstream-drift.test.ts` suite drives
|
|
239
|
+
the same real loop fully in-process (via pi-ai's faux provider), so the loop
|
|
240
|
+
behaviors this layer depends on are covered by `pnpm test` too, without the
|
|
241
|
+
`pi` binary.
|
|
242
|
+
|
|
243
|
+
## Display settings
|
|
244
|
+
|
|
245
|
+
Repaired tool calls get a `🔨 ✓ input repaired (rules...)` line appended to
|
|
246
|
+
their result row in the TUI; optionally the repair note text renders beneath
|
|
247
|
+
it. `/repair-settings` toggles the indicator and the note text independently,
|
|
248
|
+
and cycles the grammar-leak recovery mode (`off → strip → recover`; default
|
|
249
|
+
`strip`). All three persist to `~/.pi/agent/tool-repair/settings.json` (or
|
|
250
|
+
`PI_TOOL_REPAIR_SETTINGS=<path>`), alongside an optional `grammarAllowedTools`
|
|
251
|
+
list that restricts which tool names a leaked call may be promoted to (when
|
|
252
|
+
empty, the active tool set is used). Indicators are persisted per session via
|
|
253
|
+
custom entries, so they survive `/reload` and session resume.
|
|
254
|
+
|
|
255
|
+
## Unrepairable input
|
|
256
|
+
|
|
257
|
+
When repairs can't make the input valid, the layer raises a model-readable
|
|
258
|
+
retry error (`Invalid input for tool "write". Fix these issues and retry: ...`)
|
|
259
|
+
instead of passing the input through — passing through would let pi's
|
|
260
|
+
`Value.Convert` coerce it into garbage (`content: null` becomes the literal
|
|
261
|
+
string `"null"` on disk) and execute anyway. Set `PI_TOOL_REPAIR_PASSTHROUGH=1`
|
|
262
|
+
to restore pi's native behavior.
|
|
263
|
+
|
|
264
|
+
## Prior art
|
|
265
|
+
|
|
266
|
+
The value strips and the grammar-leak recovery approach come from
|
|
267
|
+
[`monotykamary/pi-tool-repair`](https://github.com/monotykamary/pi-tool-repair)
|
|
268
|
+
(MIT), Tom X Nguyen's pi extension. What this project adapted from it:
|
|
269
|
+
|
|
270
|
+
- The anchor-bleed and grammar-token-leak strips (`src/value-strips.ts`),
|
|
271
|
+
reworked to run inside `prepareArguments`, to skip the `grep.pattern` regex
|
|
272
|
+
field, and to report through this layer's repair-note / telemetry machinery.
|
|
273
|
+
- The grammar-leak recovery parsers, candidate selection, code-fence handling,
|
|
274
|
+
and range removal (`src/grammar-recovery.ts`, ~10 grammar families), adapted
|
|
275
|
+
in place with a one-line provenance header, plus the new `stopReason`
|
|
276
|
+
promotion gate.
|
|
277
|
+
|
|
278
|
+
The two projects also share a common Command Code lineage in their
|
|
279
|
+
validate-then-repair engines (near-identical rule names and alias tables).
|
|
280
|
+
|
|
281
|
+
Where they differ is **which hook reaches a malformed built-in call**, and that
|
|
282
|
+
is mechanical, not a matter of quality. pi runs
|
|
283
|
+
`prepareArguments → validateToolArguments → tool_call event → execute`
|
|
284
|
+
([research.md Claim 1][r-claim1]). A malformed built-in call makes
|
|
285
|
+
`validateToolArguments` **throw** before the `tool_call` event ever fires
|
|
286
|
+
([Claim 2][r-claim1]), so a repair that keys on `tool_call` cannot see it; and
|
|
287
|
+
even for a call that does pass, the `tool_call` event only propagates a handler's
|
|
288
|
+
*in-place* mutation of `event.input`, never a reassignment ([Claim 3][r-claim3]).
|
|
289
|
+
pi-tool-repair's built-in repair keys on `tool_call`; this project overrides the
|
|
290
|
+
built-in tool so its repair runs in `prepareArguments`, the one seam ahead of
|
|
291
|
+
validation. That is why the two hooks reach different cases — see the cited
|
|
292
|
+
research entries for the source. Conversely, the capabilities assimilated here
|
|
293
|
+
(value strips, grammar recovery) live on hooks that *do* propagate their results
|
|
294
|
+
(`prepareArguments`, `message_end` — [Claim 4][r-claim4]), which is why adapting
|
|
295
|
+
them is sound.
|
|
296
|
+
|
|
297
|
+
[r-claim1]: docs/research.md#claim-1--loop-ordering-preparearguments-runs-before-validation-which-runs-before-the-tool_call-event
|
|
298
|
+
[r-claim3]: docs/research.md#claim-3--tool_call-event-in-place-input-mutation-propagates-reassignment-does-not
|
|
299
|
+
[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
|
|
300
|
+
[r-claim7]: docs/research.md#claim-7--stopreason-length-causes-pi-to-fail-all-tool-calls-in-the-message
|
|
301
|
+
|
|
302
|
+
## Limitations
|
|
303
|
+
|
|
304
|
+
- Only pi's seven built-in tools are wrapped. Custom tools from other
|
|
305
|
+
extensions aren't (pi has no API to wrap another extension's execute), but
|
|
306
|
+
they can import `repairToolInput` from `src/repair-engine.ts` and use it in
|
|
307
|
+
their own `prepareArguments`.
|
|
308
|
+
- Wrong-but-optional fields are invisible to validation by design: pi's schemas
|
|
309
|
+
allow extra keys, so `grep {pattern, directory: "src"}` validates and
|
|
310
|
+
silently searches the cwd. No validate-then-repair layer can catch this
|
|
311
|
+
(commandcode's included); it would take strict schemas or key-similarity
|
|
312
|
+
heuristics, both with false-positive costs.
|
|
313
|
+
- Unrepairable input falls through to pi's stock validation error, which is
|
|
314
|
+
already model-readable (per-path issues plus the received arguments).
|
|
315
|
+
- If another extension also overrides a built-in tool, load order decides which
|
|
316
|
+
override wins — they don't compose.
|
|
317
|
+
- Phantom tool calls (a tool-use stop reason with zero tool-call blocks) are not
|
|
318
|
+
handled. On pi 0.80.6 that state is a clean terminal stop, not a recoverable
|
|
319
|
+
retry ([research.md Claim 6][r-claim6]), so normalizing it would be cosmetic;
|
|
320
|
+
it will be revisited only if a concrete stuck state is reproduced on a
|
|
321
|
+
provider.
|
|
322
|
+
|
|
323
|
+
[r-claim6]: docs/research.md#claim-6--stopreason-error-and-aborted-is-terminal-not-a-retry
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@r3b1s/pi-repair-layer",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/r3b1s/pi-repair-layer.git"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.ts",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "src/index.ts",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"pi-package",
|
|
23
|
+
"pi-extension",
|
|
24
|
+
"pi-coding-agent",
|
|
25
|
+
"tool-repair",
|
|
26
|
+
"validation"
|
|
27
|
+
],
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./src/index.ts"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"typebox": "1.1.38"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@biomejs/biome": "2.4.8",
|
|
38
|
+
"@earendil-works/pi-ai": "^0.80.3",
|
|
39
|
+
"@earendil-works/pi-coding-agent": "0.80.3",
|
|
40
|
+
"@types/node": "^24.0.0",
|
|
41
|
+
"eslint": "^9.25.1",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"typescript-eslint": "^8.31.1",
|
|
44
|
+
"vitest": "^4.1.10"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"check": "tsc --noEmit",
|
|
54
|
+
"lint": "biome check . && eslint src/ test/ index.ts",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"format": "biome check --write .",
|
|
58
|
+
"clean": "rm -rf dist node_modules coverage"
|
|
59
|
+
}
|
|
60
|
+
}
|