# Pi Tool Repair Integration

> Build or wrap tools in a pi coding-agent extension using @r3b1s/pi-repair-layer's adaptToolDefinition and the optional-dependency fallback pattern. Use when authoring a pi extension that registers tools, when adding argument repair (aliases, envelope recovery, schema-guided fixes) to an extension-owned tool, or when deciding whether pi-repair-layer should be a hard or optional dependency. Triggers on pi extensions, registerTool, prepareArguments, tool-call repair, or pi-repair-layer integration tasks.

- Skill: `r3b1s/pi-tool-repair-integration` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add r3b1s/pi-tool-repair-integration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/r3b1s/pi-tool-repair-integration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: r3b1s (https://skillmd.com/u/r3b1s)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/r3b1s/pi-tool-repair-integration

---


# pi Tool Repair Integration

Integrate `@r3b1s/pi-repair-layer` into a pi extension's own tools so
malformed model arguments (`{file_path: "/x"}` instead of `{path: "/x"}`,
stringified JSON, stray `null`s) are repaired before pi validation — without
forcing every downstream user to install the repair layer.

Verified baselines: package `^0.3.0`, Node 22+, pi 0.80.6 (probed through
0.80.10).

## The ownership boundary (read first)

pi runs `prepareArguments → validation → tool_call → execute`. Arguments that
fail validation never reach `tool_call`, so repair must run in
`prepareArguments` — and only the extension that owns a tool definition can
install that hook. Installing pi-repair-layer repairs pi's *built-in* tools
only; it never discovers or wraps tools registered by other extensions. Your
extension must opt in per tool. Never attempt to wrap another extension's
tools.

## Step 1 — Choose the dependency posture

| Posture | When | How |
| --- | --- | --- |
| **Optional (default for standalone extensions)** | The tool works fine unwrapped; repairs are an enhancement | Fallback recipe below; package stays out of runtime deps |
| Hard dependency | The tool relies on repair behavior (e.g. legacy-shape migration via preprocessors) | `pnpm add @r3b1s/pi-repair-layer`; import statically |

The optional posture turns "also install pi-repair-layer" into an end-user
opt-in: pi installs all `npm:` extensions into one shared `node_modules` per
scope, so a user who runs `pi install npm:@r3b1s/pi-repair-layer` makes it
resolvable to every consenting npm-installed extension automatically.

## Step 2 — Write the tool and repair options as pure data

Repair options must be plain data validated with a **type-only** import, so
compilation never requires the package at runtime:

```ts
import type { PiToolOwnerAdapterOptions } from "@r3b1s/pi-repair-layer/pi";

const repairOptions = {
  policy: "adaptive",
  preprocessors: [
    {
      kind: "alias",
      selector: "/path",
      aliases: ["file_path"],
      accepts: "string",
    },
  ],
} satisfies PiToolOwnerAdapterOptions;
```

Configure only transforms you know are safe for your tool — see
[resources/preprocessor-catalog.md](resources/preprocessor-catalog.md) for
every `kind`, selector semantics, and the policy profiles. The pipeline never
guesses aliases, fuzzily renames keys, or deletes unknown fields; wrapped
tools get bounded envelope recovery and schema-located repairs for free.

## Step 3 — The optional-integration recipe

Copy [resources/optional-extension-template.ts](resources/optional-extension-template.ts)
(a complete extension) and adapt names. The load-bearing core:

```ts
import type { adaptToolDefinition } from "@r3b1s/pi-repair-layer/pi";

async function loadRepairAdapter(): Promise<
  typeof adaptToolDefinition | undefined
> {
  try {
    const repair = await import("@r3b1s/pi-repair-layer/pi");
    return repair.adaptToolDefinition;
  } catch (error) {
    const code = (error as { code?: unknown } | null)?.code;
    const message = error instanceof Error ? error.message : String(error);
    const packageAbsent =
      (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") &&
      message.includes("@r3b1s/pi-repair-layer");
    if (!packageAbsent) throw error;
    return undefined;
  }
}

export default async function myExtension(pi: ExtensionAPI) {
  const adapt = await loadRepairAdapter();
  if (!adapt) {
    console.error(
      "[my-extension] @r3b1s/pi-repair-layer not found; my_tool running unwrapped",
    );
  }
  pi.registerTool(adapt ? adapt(definition, repairOptions) : definition);
}
```

Every detail matters — do not simplify these away:

- **Both error codes.** `MODULE_NOT_FOUND` is jiti's require path;
  `ERR_MODULE_NOT_FOUND` is native ESM and the compiled pi binary.
- **The message must name `@r3b1s/pi-repair-layer`.** A present-but-broken
  install throws the same codes naming a *transitive* module; swallowing it
  would silently disable repairs the user believes are active. Match the
  package name, not the `/pi` subpath — native ESM reports only
  `Cannot find package '@r3b1s/pi-repair-layer'`.
- **Rethrow anything else.** Any other error is a real failure, not absence.
- **One stderr note on fallback.** The branches differ in coercion behavior;
  a silent divergence cannot be diagnosed from a session transcript.
- **Identity fallback.** Register the unmodified definition — never a partial
  wrapper.

## Step 4 — Consumer `package.json`

```jsonc
{
  "devDependencies": {
    "@r3b1s/pi-repair-layer": "^0.3.0" // typecheck + local tests only
  },
  "peerDependencies": {
    "@r3b1s/pi-repair-layer": ">=0.3.0"
  },
  "peerDependenciesMeta": {
    "@r3b1s/pi-repair-layer": { "optional": true }
  }
}
```

Alternative: authors who want repairs whenever the environment allows can add
`"optionalDependencies": { "@r3b1s/pi-repair-layer": "^0.3.0" }` — a failed
optional install does not fail the extension install. This is also the path
for consumers the shared-root story cannot reach (see caveats).

## Step 5 — Test both branches

Work through [resources/testing-checklist.md](resources/testing-checklist.md).
Minimum bar: with the package absent, activation succeeds, the raw definition
is registered, and the note is emitted; with it present, the adapter branch is
taken silently and each configured repair produces valid arguments.

## Hard caveats (state these to the user when relevant)

1. **Compiled pi binary never takes the adapter branch.** Under the standalone
   (Bun-compiled) pi executable, dynamic `import()` cannot resolve
   npm-installed siblings, so the recipe falls back — safely, with the note —
   even when the package is installed. The optional pattern activates only
   under Node-based pi installs. A hard static dependency works under both.
2. **Scope and install source matter.** Git-installed extensions get their own
   clone-local `node_modules`; project-scope and user-scope installs do not
   see each other's siblings. Those consumers fall back; offer
   `optionalDependencies` instead.
3. **Fallback mode is baseline pi, not "repairs minus notes."** pi's native
   validation runs TypeBox `Value.Convert` first, which silently coerces some
   invalid input (`null` → `"null"`) instead of repairing or rejecting it.
   The tool owner must decide explicitly whether that is acceptable.
4. **No double-wrap.** The installable pi-repair-layer extension only
   overrides pi's built-ins; a tool adapted by this recipe is wrapped exactly
   once in either branch.

## Stability contract you may rely on

- Subpaths (`/pi`, `/core`, `/grammar`) and the
  `adaptToolDefinition(definition, options?)` signature are stable for the
  current major (semver).
- Absence detection semantics (the two codes + module-naming message) are part
  of the contract.
- Unrecognized preprocessor `kind`s are ignored — never fatal, no mutation,
  results still schema-validated — so options written against a newer minor
  degrade gracefully on older installs.

## Going further

- Failing closed: the adapter throws `UnrepairableToolInputError` with a
  model-readable retry message by default; `unrepairable: "passthrough"`
  exists only for deliberate migrations.
- Structured outcomes: pass `onOutcome(result)` for value-free metrics (rule
  IDs, stages, policy, fingerprint — never argument values).
- `<repair_note>` feedback and the `RepairLifecycle`, plus the pure-core
  `runRepairPipeline`, are documented in the package's
  `docs/tool-owner-integration.md`.

