# 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; each tool captures that snapshot for its lifetime, and a reload registers the tools again. When `enabled` is false it registers nothing. Otherwise it registers the model-budget provider (`registerModelBudgets`) and the Action Fusion cards (`registerFusionCards`, always, so saved then_run cards render while Action Fusion is off), builds the Action Fusion executor from the cards' progress reporter only when `actionFusion` is on, and passes each tool factory one environment: `ToolEnvironment` (`cwd`, `config`, and the `budgets` provider; `config.ts`) for `read` and `grep`, and `MutationToolEnvironment` (the same plus `fusion`, undefined when Action Fusion is off; `mutation-runner.ts`) for `write`, `edit`, and `replace`. Tools register in the order write, edit, replace, read, grep. One helper appends `NEXT_GUIDELINE` from `report-schema.ts` to every tool's `promptGuidelines` (Pi prints the shared guideline once); the mutation tools are also wrapped with `withMutationStatus` when the executor exists. Their shared `mutationToolBase` (`mutation-runner.ts`) adds the `then_run` field (`withThenRunSchema`), its selection instructions in the description (`withThenRunDescription`), and `ACTION_FUSION_GUIDELINES` only when `fusion` exists; 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, an empty `lines` array, `lines` entries 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. Some providers emit keys in declaration order and others alphabetically, so the `edit` and `replace` keys are declared in alphabetical order (`path`, `steps`, `then_run`; `action`, `anchor`, `end`, `lines`; `path`, `replacements`, `then_run`): both orders stream the file and each operation's action and range before the payload.
- `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. On schema failure, prepare diagnostic values with the public pi-schema coercer using Pi's formats and unresolved-reference policy; Pi alone decides acceptance. Do not reimplement 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, pattern-mismatched, and out-of-choice fields from the declared schema: when a schema reason does not say how to correct the value, the `fix` of a missing field, a pattern mismatch, or a value outside the field's choices is the field's 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 ordered `SearchWarning` records (`{ path?, reason }`) in its result rather than composing sentences; `grep-tool` joins stages in order, and only `grep-output` formats diagnostics with cwd-relative paths, de-duplicates them, and applies the field budget. 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. In a backreference's repeated text, membership keeps each node's original scoped case flags, and the reference's case widening is applied afterward through the existing byte translator (`scalarRanges`, `backreference`). 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. Completed-run diagnostics leave this layer as reason-only warning data; path formatting belongs to `grep-output`. 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. Each worker (`grep-worker.ts` runs `core/search.ts`; `replace-worker.ts` runs `core/replace.ts`) is started through its `.mjs` bootstrap, which loads 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), its schema, audience, and model decision purpose, every error code with its facts schema and recovery (`next.default` plus named alternatives), and the success recovery order (`OUTCOME_NEXT`). `FACT_FIELDS` is the required declaration point; model omission is derived from its audience together with the call fields, so `report.ts` does not maintain a second field set. 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). It filters the derived model omission set (`MODEL_OMITTED_FIELDS`) and omits the call's own `tool`/`path` plus TUI-only facts from result text while retaining them in `details.report`; renderers pass their tool name to `reportOf`, which reads `details.report` (checked once by `detailsReport`) or else decodes a text-only report. `reportResult` appends a success report after the tool's payload only when the report states a model-visible fact or the result has no 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. Renderers read reports only through `splitReport` (a success: the checked report and the content before its trailing report block, stripped only when that block equals `renderReport(details.report)`) or `renderToolError` (a failure). Failure rendering uses `details.report`; Pi raises argument errors before execution, so a text-only argument rejection decodes from the final content block through `reportOf`. Report text omits its tool, so every decode 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`.
- To add or change a report field: (1) declare it in `FACT_FIELDS` under its owning layer's group, including its audience and, for model-visible facts, the decision it changes: the concrete call the model makes differently for each value (`decide whether…` and `account for…` name no call); (2) list it in each `SUCCESS_DEFINITIONS` tool or `ERROR_DEFINITIONS` code that carries it; (3) when it needs recovery, add one `OUTCOME_NEXT` rule or the code's `next`, never a sentence at the producing site; when the recovery depends on the call's arguments, the rule names alternatives and the producing layer selects one (`OutcomeVariant`, passed to `reportResult`), so the text never asks the model to branch; (4) produce it in that one layer, bounded by the call's budget, omitting whole items with a count; (5) leave `render.ts` alone unless a card presents the fact another way, and then name it in that card's shown set; (6) check the README passage for the tool. The model omission set is derived from the declarations; do not add a parallel list in `report.ts`. To change an error code, edit `ERROR_DEFINITIONS` and follow the type errors to every throw site and the worker contract.
- `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. Every tool factory receives the provider as `ToolEnvironment.budgets` (`config.ts`); internal helpers require the resolved budget or an 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.
