# src/pi

Pi integration. Cross-cutting contracts live in the root `AGENTS.md`; this file maps the modules and holds constraints specific to them.

## Registration

`src/index.ts` loads the config once and passes it explicitly to each tool factory, which keeps that snapshot for its lifetime. When `enabled` is false it registers nothing. Otherwise it registers tools from their `make*Override` / `makeReplaceTool` factories through one helper that appends `NEXT_GUIDELINE` from `report-schema.ts` (Pi prints the shared guideline once). When `actionFusion` is on, the three mutation tools also receive the Action Fusion executor and are wrapped with `withMutationStatus`; their shared `mutationToolBase` (`mutation-runner.ts`) applies `withThenRunDescription`, which appends the then_run selection instructions, as `withThenRunSchema` adds the field, and the field's `command` description carries the Windows path note only when `process.platform` is `win32`.

## Argument funnel

- Tool files (`read-tool.ts`, `grep-tool.ts`, `edit-tool.ts`, `replace-tool.ts`, `write-tool.ts`) define TypeBox schemas and each mutation tool's description names its alternative (edit ↔ replace ↔ write). Each tool's `prepareArguments` comes from `argument-validation.ts`, then Pi validates against the schema again, and only then calls `execute`. Caller arguments are never rewritten. Each tool supplies only the semantic checks the schema cannot express or name precisely: edit rejects fields an operation's `action` does not take or requires but lacks, empty bodies, body lines that cannot be written as UTF-8, line numbers beyond the safe-integer range, anchors of another hash length, and reversed ranges; replace rejects unwritable replacement text; replace and grep share `checkRegexArguments`, which reports flags V8 rejects and compiles regex-mode patterns; write rejects unwritable content. File-dependent checks stay in execution. Shared integer rules live in `schema.ts`. Every tool declares `STRICT_SAMPLING` from `schema.ts` (mutation tools through `mutationToolBase`) and keeps its parameters convertible by Pi's strict converter: no object or array unions, so grep's multi-valued fields are arrays only and each edit item is one flat object whose `EDIT_OPS` table, not the schema, fixes the fields per action. Constrained sampling and some gateways emit keys alphabetically, so the item's discriminator keeps the alphabetically first key.
- `argument-validation.ts` owns every tool's `prepareArguments` hook, issue collection, bounded diagnostic output, and the Pi schema validation adapter. Tools report semantic field issues through its callback and do not throw during collection. Successful schema conversion informs those checks without changing caller arguments. Observe prepared failures without reimplementing coercion or optional-null rules.
- `argument-diagnostics.ts` owns failure-only schema projection and structured aggregation. Top-level tool fields have independent schemas; failure diagnosis derives field schemas with TypeBox and keeps Pi's coercion and per-field error limits. Derive choices and the explanations of missing, undeclared, and pattern-mismatched fields from the declared schema: when a schema reason does not say how to correct the value, a missing or pattern-mismatched field's `fix` is its description (each choice's value and description), and an undeclared field's `fix` lists its object's accepted fields. A declared field is applicable even when absent, so a tool check may require it. An issue on a whole object leaves its fields' diagnostics in place. Suppress only inapplicable or already explained issues, and retain independent fields with bounded native diagnostics.
- `argument-error.ts` owns the JSON argument-error envelope and its byte budget: retain exact field paths, omit whole issues with explicit counts, and never truncate serialized JSON or repeat the call's arguments. Preparation failures use the same envelope.

## Mutations

- `mutation-runner.ts` owns the definition the mutation tools share (`mutationToolBase`: name, then_run field, description and guideline, argument preparation, strict sampling, and `execute`); each tool supplies its schema, text, semantic checks, and `run`, and adds its own renderers. It also owns the call sequence shared by `edit`/`replace`/`write` after Pi has validated the arguments: separating `then_run`, path resolution, `withFileMutationQueue`, the hand-off to Action Fusion, and finalization. Each tool's `run` returns a tool-indexed `MutationOutcome` (commit facts, details, schema-derived outcome facts), Action Fusion returns `FusedMutation` facts, and the runner's single `finalizeMutation` call turns them into a result; nothing downstream reads publication or revisions back from result details. `runTextMutation` owns the read-modify-write for `edit`/`replace`, so each tool supplies only a `TextChange` (new text, outcome facts).
- `file-read.ts` provides guarded stream and whole-file reads; byte revision checks stay in the search and commit owners.
- `file-revision.ts` defines a revision once: the SHA-256 hex of exact bytes, whole (`byteRevision`) or streamed. `text-stream.ts` returns the revision of the bytes each scan read, so callers never hash.
- `file-commit.ts` is the only publication path: file revisions (`fileRevision`), no-op detection, a temp file in a sibling dir, then `link` (create) or `rename` (overwrite). It handles symlinks and hard links, preserves existing file modes (a new file follows the umask), and syncs the directory on POSIX. A failure with a publication status to state throws `FileMutationError`; every other failure stays a plain `HashlineError`, reported as `NOT_PUBLISHED`. A failed step is the `message`, its underlying error the `cause`. `edit`/`replace` commit with `baseRevision`, the revision of the bytes they read; `CommitOptions` admits it only with `overwrite`. Its target type distinguishes missing from existing files with required revision and mode facts; commit results distinguish creation, replacement, and no-op. It also owns freshness: `commitFreshness` from the commit result and `observeFreshness` for Fusion's later observations; only unknown freshness carries an observation error.
- `mutation-result.ts` owns tool-indexed outcome facts, final success/failure details, and progress details. It builds diffs and finalizes one mutation report from commit, tool, freshness, and command facts. On failure, `describeMutationFailure` states what only a mutation knows (publication, and through `CommandNotRun` why a fused command never ran) and leaves classification to `describeFailure`. `failure-context.ts` turns anchor failures into a `HashlineError` with bounded facts (`failures`, `candidateNeighborhoods`); `anchor-format.ts` owns the `LINE#HASH` syntax, its schema pattern, and row parsing.

## grep

- Split into `grep-tool` (schema and execution order), `grep-scope` (paths and glob admission), `grep-candidates` (candidate strategy, fallback, whole-scope scan budget, and the empty-scope guard), `grep-search` (queue backpressure, worker pool, per-file matches with their revisions), `grep-reread` (content mode's bounded second read of each matched file and its revision check), `grep-output` (payload rows, file-header format and its parser, and every grep report fact, including empty and incomplete searches), and `grep-render` (TUI rows). Grep scans each candidate file once through `text-stream.ts` (via `scanTextFile`) for NUL, UTF-8 validity, its revision, and the lossy LF view it matches; content mode reads each matched file again in `grep-reread`. Each stage returns its warnings, ordered within the stage, in its result rather than appending to a shared list; `grep-tool` joins them in stage order. README defines resource limits and physical-line reporting; document file and worker budgets there.
- Without `multiline`, `rg-regex.ts` translates each pattern into rg patterns that list every line the query matches: keep a construct only where both engines give it the same meaning over the original line bytes (CRLF, malformed UTF-8, surrogate halves included), and otherwise widen it; never narrow. Bound string expansion and emitted patterns. Preserve every alternative. A multiline query, unsupported or oversized translation, or pattern rg rejects before reporting a file uses the file prefilter.
- `rg-process.ts` is the ripgrep process layer: spawning, bounded reading, and parsing rg's output protocols (`runRgPaths` reads NUL-delimited paths; `runRgLines` reads the per-file line numbers of `LINE_LISTING_ARGS`). It knows no candidate strategy. Keep the injectable `GrepBackend` and completed-run diagnostics here. Scope flags are produced only in `grep-scope.ts` and never parsed back.
- `grep-candidates.ts` decides which rg runs to make: line candidates through `rg-regex.ts` (workers re-check only those lines through `searchLines`), else candidate files (`--files-with-matches` over the necessary literals, or `--files` when none is necessary or they exceed `MAX_RG_PATTERN_UNITS`). Byte-level pattern flags are defined once there.
- Keep grep matching in the bounded, cancellable worker pool of `grep-search.ts`: a lazily started pool, each worker sending `core/search.ts` whole lines in bounded requests, or the whole file for a multiline query. Commit file results, warnings, and fatal failures in listing order; retire a worker after a fatal file result, but propagate cancellation without waiting for earlier files.
- `grep-scope.ts` classifies scope errors: only `ENOENT` means `PATH_NOT_FOUND`; preserve other filesystem errors.

## Regex workers

`regex-worker.ts` owns timeout, cancellation, and cleanup for grep and replace workers; each caller awaits a request before sending another to the same worker. Their `.mjs` bootstraps load TypeScript through `jiti`, because workers do not inherit Pi's loader; workers never import the Pi runtime and trust inputs validated at tool entry. `replace-regex.ts` owns the replacement request and sends regex batches to `core/replace.ts`; literal-only batches remain in-process. `config.ts` owns both timeout settings. The host validates exactly-one-result replies with `regex-worker-contract.ts`, using code-specific facts from `report-schema.ts` and each engine's result schema (`core/replace-schema.ts`, `core/search-schema.ts`). Bound worker failures only in the receiving tool.

## Reports and rendering

- `report-schema.ts` is the single definition of the result protocol: every report field in output order (each with one owning layer), every error code with its facts schema and recovery (`next.default` plus named alternatives), and the success recovery order (`OUTCOME_NEXT`). Checked report types retain their tool or error-code variant; absent fields remain readable as undefined. Types derive from it. Its closed wire schemas validate text-only reports, enforce legal tool names, and require code-specific facts.
- `report.ts` builds reports and owns their one model-facing serialization (`renderReport`: one JSON object, fields in schema order) and omits the call's own `tool`/`path` from every result text while retaining them in `details.report`; renderers pass their tool name to decode text-only reports. `reportResult` appends a success report after the tool's payload; every tool's `execute` runs inside `runTool`, which classifies a failure (`describeFailure`, structured `cause`, `next` from code and publication) and returns it as an `isError` result whose `details.report` the TUI renders. This module receives a mutation's failure facts from `mutation-result.ts` and does not depend on the commit layer.
- `render.ts` (`reportLines`) is the one TUI renderer of a report and holds the shared tool renderers. Render inputs admit partial details independently of the current execution result. Failure rendering uses `details.report`; Pi raises argument errors before execution, so text-only argument rejections decode from the final content block with `decodeReport`. Report text omits its tool, so every `decodeReport` call supplies the originating call's tool name. Invalid text-only reports remain raw.
- `report-cause.ts` transports structured causes without Pi runtime imports, preserving cause chains with cycle protection; reports and worker replies share it. Native failures state the original diagnostic and errno once in `message`, retaining only an actual underlying `Error.cause`.
- `error-text.ts` holds the Pi-layer error forms (cancellation, file changed during read or search). `diagnostic-buffer.ts` bounds stderr and search diagnostics.

## Budgets and config

- `budgets.ts` derives model-facing byte budgets from the selected model's declared `contextWindow`, with bounded defaults for unknown limits. Standalone tool factories default to its provider; internal helpers require the resolved budget or explicit byte limit. `model-budgets.ts` derives the budgets from the live selected model on each request; preparation shares its provider, and each execution captures its budgets before its asynchronous work. Model-derived content budgets share the `MAX_CONTENT_KIB` ceiling that the explicit `read.maxKiB` range also uses.
- `config.ts` defines the `hashlineEdit` settings as one TypeBox schema: top-level `enabled`, `actionFusion`, `hashLen`, `shiftRadius` control shared behavior; the `read`, `grep`, and `replace` groups hold tool-specific settings. Missing or invalid optional `read.maxKiB` selects automatic model sizing.

## Action Fusion

`action-fusion.ts` implements `then_run`. After a successful mutation it checks freshness against `publishedRevision`, then runs the command through the session Bash tool. Its own per-file queue wraps mutation and command around Pi's `withFileMutationQueue`; the module header explains the nesting. Fusion states only the command (`then_run` status and output, `terminate`) and returns it as facts without building a result; freshness and publication come from the mutation report. Fusion progress derives from `ACTION_FUSION_PROGRESS_SCHEMA` and retains the session command's original display content and details; `fusion-card.ts` validates saved command cards against that schema before restoring them. It renders the separate mutation and command TUI cards (`withMutationStatus`, `registerFusionCards`) and derives a not-run reason from the same facts.

Command cards reuse Pi's public `ToolExecutionComponent`, with the real TUI supplied by a temporary empty widget factory. History may be rebuilt before `session_start`, so retain a deferred component until the UI is bound. Cache each native component by call ID and apply global expansion only when that setting changes, so custom-entry rebuilds preserve mouse expansion. Keep spacing adaptation separate from native mouse handling and retain the built-in Bash presentation; the supported Pi API does not expose another tool's renderer.
