@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,227 @@
1
+ # How repair works
2
+
3
+ This document is the detailed behavior and safety reference for
4
+ pi-repair-layer. For a shorter overview and installation instructions, start
5
+ with the [README](../README.md). Tool authors should also read the
6
+ [integration guide](tool-owner-integration.md).
7
+
8
+ ## Pipeline
9
+
10
+ Tool arguments move through these ordered stages:
11
+
12
+ 1. bounded raw-envelope recovery;
13
+ 2. the tool owner's compatibility `prepareArguments` hook;
14
+ 3. selector-guided preprocessing;
15
+ 4. policy/model-gated cleanup of values that already satisfy the schema;
16
+ 5. strict validation without native conversion;
17
+ 6. bounded iterative repair at reported schema-issue locations;
18
+ 7. native conversion and final validation; and
19
+ 8. structured outcome and result feedback.
20
+
21
+ Every mutation has a stable rule ID, pipeline stage, and explanatory note. A
22
+ `repaired` outcome is impossible unless the final value passes the supplied
23
+ schema. An `unrepairable` outcome preserves the original input and reports no
24
+ claimed changes.
25
+
26
+ ### Profiles
27
+
28
+ | Profile | Envelope, exact preprocessing, schema repair | Truncated completion and model-gated value cleanup | Grammar text | Promotion |
29
+ |---|---|---|---|---|
30
+ | `conservative` | yes | observe only | observe only | never |
31
+ | `adaptive` | yes | schema-validated | strip known tools | never |
32
+ | `recover` | yes | schema-validated | strip known tools | gated known-tool calls |
33
+
34
+ `adaptive` is the default for new and migrated non-recover installs. Existing
35
+ users who explicitly enabled grammar recovery migrate to `recover` without
36
+ losing that choice.
37
+
38
+ Unknown or disallowed tool grammar is preserved under every profile. A separate
39
+ `unknownGrammarText: "strip"` setting can remove it, but can never make the
40
+ unknown call executable. The narrow grammar override supports `off`, `observe`,
41
+ `strip`, and `recover`.
42
+
43
+ ## Repair catalog
44
+
45
+ ### Envelope recovery
46
+
47
+ Before inspecting individual fields, the pipeline can:
48
+
49
+ - decode recursively JSON-stringified argument objects within configured limits;
50
+ - escape raw control characters inside JSON strings;
51
+ - unwrap a singleton array containing one plain object; and
52
+ - under adaptive/recover policy, try a fixed set of closing suffixes for a
53
+ truncated object.
54
+
55
+ A truncated candidate is accepted only if the complete downstream pipeline
56
+ validates it. Non-singleton arrays are never discarded into defaults, and
57
+ unrecoverable input never becomes `{}`.
58
+
59
+ ### Schema-guided repairs
60
+
61
+ The iterative engine repairs only locations reported by strict validation. Its
62
+ fixed rules include:
63
+
64
+ | Rule | Example | Result |
65
+ |---|---|---|
66
+ | exact alias | `{file_path: "/x"}` | `{path: "/x"}` |
67
+ | drop invalid optional null | `{path: "/x", offset: null}` | `{path: "/x"}` |
68
+ | drop empty object placeholder | `{tags: {}}` where array expected | omit `tags` |
69
+ | parse stringified array | `{include: '["a","b"]'}` | `{include: ["a", "b"]}` |
70
+ | parse stringified object | object field contains JSON text | parsed object |
71
+ | wrap string as array | `{include: "foo"}` | `{include: ["foo"]}` |
72
+ | wrap root string | `"echo hi"` for bash | `{command: "echo hi"}` |
73
+ | structural edit fold | flat old/new fields | canonical `edits` array |
74
+
75
+ Aliases and structural changes must be configured by the tool owner. The
76
+ pipeline does not use edit distance, semantic guessing, or generic unknown-key
77
+ deletion. Built-in alias configuration covers common Claude Code shapes such as
78
+ `file_path`, `old_string`, and `new_string`; aider-style `search` and `replace`;
79
+ and generic alternatives including `cmd`, `query`, `text`, and `contents`,
80
+ including configured nested edit fields.
81
+
82
+ ### Selector-guided preprocessing
83
+
84
+ Some bad values pass validation because they have the right JSON type.
85
+ Preprocessors address only configured semantic locations such as `/path`,
86
+ `/files/*`, or `/edits/*/oldText`.
87
+
88
+ Available selector preprocessors include:
89
+
90
+ - markdown auto-link cleanup on filesystem paths;
91
+ - string-or-array and scalar compatibility conversions;
92
+ - exact optional aliases that would otherwise pass as extra keys;
93
+ - model-gated anchor bleed cleanup; and
94
+ - model-gated `<arg_key>` / `<arg_value>` token cleanup.
95
+
96
+ The built-in tools use exact aliases, filesystem-path cleanup, model-gated
97
+ artifact cleanup, and the edit structural fold at their known-safe locations.
98
+ Custom tool owners can opt into the other selector kinds described in the
99
+ [integration guide](tool-owner-integration.md#configure-only-known-safe-transforms).
100
+
101
+ Anchor cleanup is never applied to `grep.pattern`, where `^` and `$` are valid
102
+ regex syntax. Content fields are not parsed or path-cleaned just because their
103
+ text resembles JSON or a filename. Markdown path cleanup unwraps only the
104
+ degenerate auto-link whose link text equals its URL without the protocol; real
105
+ markdown links remain unchanged.
106
+
107
+ The built-in model gates apply anchor cleanup to `kimi-k2`, `minimax`, and `glm`
108
+ families and grammar-token cleanup to `glm`. Custom tool owners choose their own
109
+ selectors and model-family expressions.
110
+
111
+ ## Grammar-leak handling
112
+
113
+ A grammar leak occurs when a model prints its intended call as assistant text
114
+ instead of emitting a real tool call. The parser recognizes ten grammar
115
+ families adapted from pi-tool-repair and ignores examples inside fenced code.
116
+
117
+ - `off` leaves the message alone and records nothing.
118
+ - `observe` classifies recognized candidates without changing the message.
119
+ - `strip` removes recognized known-tool grammar without executing it.
120
+ - `recover` may promote a recognized call into a real same-turn tool call.
121
+
122
+ Promotion turns text into execution, so all of these gates must pass:
123
+
124
+ - the user explicitly selected `recover`;
125
+ - the message role is assistant;
126
+ - the original stop reason is `stop`, never `length`, `error`, or `aborted`;
127
+ - the message contains no real tool call already;
128
+ - the parsed arguments are non-empty; and
129
+ - the named tool is active or explicitly allowed.
130
+
131
+ The stop-reason gate preserves pi's protection that fails tool calls from
132
+ truncated output; see [research Claim 7](research.md#claim-7--stopreason-length-causes-pi-to-fail-all-tool-calls-in-the-message).
133
+
134
+ Unknown-tool text remains visible by default. Even the explicit unknown-text
135
+ strip setting removes text only; it cannot promote an unavailable tool.
136
+
137
+ When promotion succeeds, the generated call ID is associated directly with the
138
+ repair lifecycle so the eventual result receives one `<repair_note>`.
139
+
140
+ ## Why repair runs before validation
141
+
142
+ pi's call sequence is:
143
+
144
+ ```text
145
+ tool.prepareArguments(raw) → validateToolArguments() → tool_call → execute
146
+ ```
147
+
148
+ The exact sequence, replacement propagation, and result lifecycle are verified
149
+ against pi source in [research Claims 1–5](research.md#claim-1--loop-ordering-preparearguments-runs-before-validation-which-runs-before-the-tool_call-event)
150
+ and exercised by upstream-drift tests.
151
+
152
+ A validation failure occurs before `tool_call`, so an event listener cannot
153
+ repair an invalid built-in call. pi-repair-layer instead replaces each built-in
154
+ definition with the original definition plus a chained `prepareArguments`.
155
+ Schemas, executors, prompt metadata, and renderers remain pi's own.
156
+
157
+ This ordering also prevents native conversion from silently changing classic
158
+ failure modes:
159
+
160
+ | Model sends | Native conversion can produce | Risk |
161
+ |---|---|---|
162
+ | `'["a","b"]'` for an array | `['["a","b"]']` | executes with one garbage item |
163
+ | `null` for an optional string | `"null"` | searches for or writes an unintended string |
164
+ | `null` for a required path | `"null"` | targets a file literally named `null` |
165
+ | `null` for an optional number | `0` | silently changes behavior |
166
+
167
+ The pipeline checks strictly first and repairs those issue locations before
168
+ native conversion. Benign conversion remains pi's responsibility when no
169
+ repair rule fires.
170
+
171
+ ## Safety invariants
172
+
173
+ Property tests and deterministic fuzzing enforce that the core:
174
+
175
+ - does not throw or hang on bounded JSON-domain input;
176
+ - does not mutate caller-owned input or object prototypes;
177
+ - is deterministic and idempotent;
178
+ - preserves strictly valid input when no configured valid-value transform applies;
179
+ - records a note and stable rule ID for every mutation;
180
+ - reports repaired only after final schema validity; and
181
+ - terminates within configured byte, depth, attempt, candidate, and work limits.
182
+
183
+ Lifecycle queues are capped, expire stale entries, use FIFO collision handling,
184
+ and clear at session shutdown. Persisted indicators and telemetry contain only
185
+ value-free metadata. See the [operations guide](operations.md#telemetry-and-privacy)
186
+ for the exact record shape.
187
+
188
+ ## Fail-closed behavior
189
+
190
+ If no bounded repair validates, the extension throws a model-readable retry
191
+ error before execution. This avoids returning raw input to native conversion,
192
+ which could turn `content: null` into the literal string `"null"` and write it.
193
+
194
+ `PI_TOOL_REPAIR_PASSTHROUGH=1` restores native behavior for compatibility, but
195
+ it weakens this guarantee and should be used deliberately.
196
+
197
+ ## Prior art and hook placement
198
+
199
+ The value-strip rules and grammar-recovery approach come from Tom X Nguyen's
200
+ MIT-licensed [`monotykamary/pi-tool-repair`](https://github.com/monotykamary/pi-tool-repair).
201
+ This project adapted:
202
+
203
+ - anchor-bleed and grammar-token cleanup, moved to path-scoped preprocessing
204
+ before validation, exempting regex fields, and reporting through the shared
205
+ lifecycle; and
206
+ - grammar parsers, candidate selection, code-fence handling, and range removal,
207
+ with stricter role, tool, empty-argument, and stop-reason promotion gates.
208
+
209
+ Both projects draw on Command Code's validate-then-repair approach. The
210
+ different built-in hook follows directly from pi's sequence: a malformed call
211
+ fails validation before `tool_call`, and a successful `tool_call` propagates
212
+ in-place mutation but not input reassignment. Those facts are documented with
213
+ source citations in [research Claims 1–4](research.md#claim-1--loop-ordering-preparearguments-runs-before-validation-which-runs-before-the-tool_call-event).
214
+
215
+ ## Limitations
216
+
217
+ - The installable extension wraps pi's seven built-ins. Other tools require
218
+ explicit owner integration through the compiled `/pi` adapter or `/core` API.
219
+ - Two extensions that replace the same built-in do not compose; load order
220
+ determines which definition wins.
221
+ - Unconfigured aliases, structural guesses, and unknown-field deletion are out
222
+ of scope.
223
+ - Phantom tool calls are not repaired. At the verified pi version, a tool-use
224
+ stop with no call is a clean terminal state rather than a stuck retry loop;
225
+ see [research Claim 6](research.md#claim-6--stopreason-error-and-aborted-is-terminal-not-a-retry).
226
+ - Grammar promotion cannot recover empty arguments, unavailable tools, existing
227
+ real calls, or truncated output.
@@ -0,0 +1,141 @@
1
+ # Operations and verification
2
+
3
+ This guide covers pi-repair-layer's user settings, local telemetry, diagnostics,
4
+ and test commands. Behavior and safety details live in
5
+ [How repair works](how-it-works.md).
6
+
7
+ ## Runtime binding
8
+
9
+ pi's extension loader aliases `@earendil-works/pi-coding-agent` and `typebox` to
10
+ the running pi installation, so the extension uses the live runtime's own
11
+ modules and validator. This repository's `node_modules/` exists for development,
12
+ typechecking, and tests; it is not a second runtime loaded beside pi.
13
+
14
+ ## Display and policy settings
15
+
16
+ Run this command in pi:
17
+
18
+ ```text
19
+ /repair-settings
20
+ ```
21
+
22
+ The menu controls:
23
+
24
+ - policy profile: `conservative`, `adaptive`, or `recover`;
25
+ - grammar override: `off`, `observe`, `strip`, or `recover`;
26
+ - unknown-tool grammar text: `preserve` or `strip`;
27
+ - the `🔨 ✓ input repaired (rules...)` TUI indicator; and
28
+ - visible repair-note text beneath the result.
29
+
30
+ Settings are stored at:
31
+
32
+ ```text
33
+ ~/.pi/agent/tool-repair/settings.json
34
+ ```
35
+
36
+ Override the location with `PI_TOOL_REPAIR_SETTINGS=<path>`. Existing settings
37
+ migrate in memory and are not rewritten until the user saves them again.
38
+
39
+ An optional `grammarAllowedTools` list restricts names eligible for promotion.
40
+ When it is empty, the active tool set is used. Unknown or disallowed calls are
41
+ never executable.
42
+
43
+ Repair indicators are persisted as value-free custom session entries, so they
44
+ survive `/reload` and session resume. The notes themselves are deliberately not
45
+ persisted.
46
+
47
+ ## Telemetry and privacy
48
+
49
+ Repair outcomes append locally to:
50
+
51
+ ```text
52
+ ~/.pi/agent/tool-repair/telemetry.jsonl
53
+ ```
54
+
55
+ Each JSONL record may include timestamp, tool, model ID, profile, pipeline
56
+ stages, stable rule IDs, outcome, issue summary, grammar family, and an FNV-1a
57
+ fingerprint of the tool/failure shape.
58
+
59
+ Records never include raw or repaired arguments, filesystem paths, commands,
60
+ content, secrets, or value-bearing note text.
61
+
62
+ There are two channels:
63
+
64
+ - Tool records identify `repaired`, `unrepairable`, or `recovered` outcomes.
65
+ - Message records describe observe/strip grammar events that have no tool-call
66
+ ID to attach to.
67
+
68
+ Summarize the local records from pi with:
69
+
70
+ ```text
71
+ /repair-stats
72
+ ```
73
+
74
+ The report groups outcomes by tool and model and tallies rule and grammar
75
+ families. Existing telemetry records remain readable as fields are added.
76
+
77
+ Environment controls:
78
+
79
+ | Variable | Effect |
80
+ |---|---|
81
+ | `PI_TOOL_REPAIR_TELEMETRY=off` | Disable telemetry. |
82
+ | `PI_TOOL_REPAIR_TELEMETRY=<path>` | Write telemetry to another file. |
83
+ | `PI_TOOL_REPAIR_LOG=1` | Print repair decisions to stderr. |
84
+ | `PI_TOOL_REPAIR_PASSTHROUGH=1` | Return unrepairable input to pi's native conversion instead of failing closed. |
85
+
86
+ ## Verification
87
+
88
+ The repository uses Node 22, pnpm, Vitest, TypeScript, Biome, and ESLint. Tool
89
+ versions and combined tasks are managed with mise.
90
+
91
+ ```bash
92
+ pnpm install
93
+ mise run ci
94
+ ```
95
+
96
+ `mise run ci` runs strict typechecking, both linters, and the complete Vitest
97
+ suite. Additional release checks are:
98
+
99
+ ```bash
100
+ pnpm run test:package
101
+ pnpm run test:fuzz
102
+ test/run-chaos.sh
103
+ ```
104
+
105
+ - `test:package` builds and packs the npm artifact, installs it into a clean
106
+ temporary consumer, imports every public subpath, and typechecks the fixture.
107
+ - `test:fuzz` runs the bounded deterministic raw-envelope campaign.
108
+ - `run-chaos.sh` registers a scripted provider and drives malformed calls
109
+ through the real pi binary without network requests or tokens.
110
+
111
+ The in-process upstream-drift suite also drives pi's real agent loop using the
112
+ faux provider. It asserts the ordering, event propagation, result replacement,
113
+ handler composition, schema shapes, truncation protection, and verified pi
114
+ minor version used by this extension.
115
+
116
+ ### Larger and replayed fuzz runs
117
+
118
+ Run 10,000 local cases:
119
+
120
+ ```bash
121
+ pnpm run test:fuzz:large
122
+ ```
123
+
124
+ Replay a reported seed and case budget:
125
+
126
+ ```bash
127
+ PI_REPAIR_FUZZ_SEED=<seed> \
128
+ PI_REPAIR_FUZZ_CASES=<count> \
129
+ pnpm run test:fuzz
130
+ ```
131
+
132
+ A discovered failure prints its seed and minimized input. Confirmed failures
133
+ should become named regression fixtures.
134
+
135
+ ## Upgrading pi
136
+
137
+ `docs/research.md` records every pi behavior the implementation relies on, with
138
+ source locations and the verification date. Before changing the verified pi
139
+ minor version, follow the
140
+ [re-verification checklist](research.md#re-verification-checklist-run-on-any-pi-minor-bump)
141
+ and run all checks above.