# Coding agent tools (first-party package)

## What it does

`@arnilo/prism-coding-tools/agent` is an optional first-party package that provides host shell/filesystem/repository tools as Prism `ToolDefinition` objects. It ships nine default coding tools — `shell`, `read`, `write`, `edit`, `repo_list`, `repo_search`, `glob`, `delete`, `move` — plus opt-in structured Git/check set (`createGitTools`), opt-in `createAskUserDecisionTool({ ask })`, and bounded coding-plan/checkpoint helpers. The tools are **inert** until a host imports them and registers them into a `ToolRegistry`. Hosts may register any subset, omit aggregators entirely, or mix first-party tools with host-owned `ToolDefinition`s. Behavior for shell/read/write/edit is a behavioral port of the pi coding agent's tools, adapted to Prism's `ToolDefinition` / `ToolResult` contracts (no `@earendil-works/*` or `typebox` dependencies; only `diff` plus the Node standard library). List/search/glob/Git are native Prism tools with no picomatch/ripgrep/Git-library dependency (hand-rolled `*`/`?`/`**` glob matcher).

| Export | Purpose |
| --- | --- |
| `createShellTool(cwd, options?)` | `shell` tool: run a shell command and return combined output + exit code. |
| `createReadTool(cwd, options?)` | `read` tool: read a text or image file into `TextContent` / `ImageContent`. |
| `createWriteTool(cwd, options?)` | `write` tool: create or overwrite a file, creating parent directories. |
| `createEditTool(cwd, options?)` | `edit` tool: precise exact-then-fuzzy text replacement in an existing file. |
| `createAcpFilesystemOperations(client)` | Map an ACP-shaped text-file client to `read`/`write`/`edit` operations; no local-disk fallback, binary/image support, or remote `mkdir`. |
| `createRepoListTool(cwd, options?)` | `repo_list` tool: bounded deterministic repository listing. |
| `createRepoSearchTool(cwd, options?)` | `repo_search` tool: bounded literal text search (`outputMode`: content / files_with_matches / count). |
| `createGlobTool(cwd, options?)` | `glob` tool: bounded filename-pattern match (`*` / `?` / `**`; opt-in bounded `{a,b}` brace expansion via `braceExpansion`). |
| `createDeleteTool(cwd, options?)` | `delete` tool: high-risk delete of a file, empty directory, or (opt-in `recursive: true`) a directory tree (no trash). |
| `createMoveTool(cwd, options?)` | `move` tool: high-risk rename/move within the workspace (`overwrite` default false). |
| `createReadPathSet()` | Session-scoped path set for optional `requireReadBeforeWrite` soft guard. |
| `createCodingTools(cwd, options?)` | Default nine tools (`shell`, `read`, `write`, `edit`, `repo_list`, `repo_search`, `glob`, `delete`, `move`). |
| `createReadOnlyTools(cwd, options?)` | Read-only subset: `read`, `repo_list`, `repo_search`, `glob`. |
| `createAllTools(cwd, options?)` | Identical to `createCodingTools` (Git tools remain opt-in via `createGitTools`). |
| `createGitTools(cwd, options?)` | Opt-in Git tools (`git_status`/`git_diff`/`git_branch`/`git_worktree`/`git_apply`/`git_commit`/`git_pr_handoff`) plus optional `coding_check`. |
| `createCodingCheckTool(cwd, options)` | Named host-declared checks; model selects only a name. |
| `createAskUserDecisionTool(options)` | Opt-in user decision tool (`ask_user_decision`); host supplies `ask` callback. Not in default aggregators. |
| `createLocalRepositoryOperations(limits?)` | Default streaming Node filesystem backend for list/search/glob. |
| `createGitAwareRepositoryOperations(cwd, options?)` | Optional Git `ls-files` ignore-aware enumeration with native fallback; host-only `includeIgnored`. |
| `createLanguageIntelligence(options)` | Optional host-activated LSP language intelligence (symbols/definitions/references/diagnostics/hover/rename); see [Language intelligence](language-intelligence.md). |
| `createProcessSessions(options)` | Optional managed long-running process sessions (start/output/input/wait/signal/kill/release); see [Process sessions](process-sessions.md). |
| `createGitHubForge(options)` | Optional reference GitHub forge adapter (issue context, push, PR create/update, review comments, checks, handoff reconcile) with `ToolEffectStore` idempotency; see [Forge integration](forge-integration.md). |
| `createGitOperations(options)` | Typed Git operations backend (argument arrays, safe config, finite output). |
| `buildCodingCheckpointMetadata` / `validateCodingCheckpointMetadata` / `assertCodingResumeAllowed` | Bounded durable coding-task metadata for workflow `state.coding` (no second runtime). |
| `writeCodingPlanFile` / `readCodingPlanFile` / `createCodingPlanMarkdown` / `parseCodingPlanTodos` | Workspace plan/todo Markdown helpers with finite byte/todo caps and hash verification. |
| `fingerprintJson` / `CODING_STATE_KEY` | Stable tool/policy fingerprints and the shared-state key for coding metadata. |
| `detectSupportedImageMimeType(buf)` / `detectSupportedImageMimeTypeFromFile(path)` | Magic-byte image MIME detection (PNG/JPEG/GIF/WebP/BMP) used by `read`. |
| `DEFAULT_MAX_IMAGE_BYTES` | Default `read` image size ceiling (10 MB). |
| `DEFAULT_*` / `HARD_*` coding limit constants | Published text-scan, image, write/edit, shell, repository, Git, check, handoff, and plan/checkpoint ceilings. |
| `ReadTextOptions` / `ReadTextResult` | Bounded text-page contract required by custom `ReadOperations`. |
| `RepositoryOperations` / `RepositoryLimitOptions` | Pluggable list/search backend and finite caps. |
| `TransformImage` / `TransformImageInput` | Types for the optional `read` `transformImage` callback. |
| `withFileMutationQueue(path, fn)` | Per-path serialization primitive re-exported for hosts. |

Each factory returns a plain `ToolDefinition` (no auto-registration). Register what you need:

```ts
import { createToolRegistry } from "@arnilo/prism";
import { createCodingTools } from "@arnilo/prism-coding-tools/agent";

const tools = createToolRegistry(createCodingTools(process.cwd()));
```

Every tool carries an explicit `kind` (`shell`→`execute`, `read`/`repo_list`→`read`, `write`/`edit`→`edit`, `repo_search`/`glob`→`search`, `delete`→`delete`, `move`→`move`) so ACP `tool_call` updates and other consumers can classify tools without name heuristics.

### ACP editor-buffer operations

`createAcpFilesystemOperations` adapts any client with `readTextFile({ path, line?, limit? })` and `writeTextFile({ path, content })` methods to the `ReadOperations`, `WriteOperations`, and `EditOperations` seams. All reads and writes stay client-backed; `mkdir` is a no-op, `statFile` measures a bounded UTF-8 text read, and image MIME detection is always `null`.

```ts
import { createAcpFilesystemOperations, createCodingTools } from "@arnilo/prism-coding-tools/agent";

const operations = createAcpFilesystemOperations(clientFilesystem);
const tools = createCodingTools(cwd, {
  read: { operations: operations.read },
  write: { operations: operations.write },
  edit: { operations: operations.edit },
});
```

This is an editor-buffer adapter, not a repository backend: `repo_list`, `repo_search`, `glob`, `delete`, and `move` remain disk-backed unless separately overridden. Binary/image and document reads are not silently delegated to local disk.

## When to use it

Use this package when a host wants ready-made coding tools for an agent, session, or run, registered explicitly into a `ToolRegistry` and dispatched through the normal Prism tool harness. The tools perform **real** shell and filesystem operations on the host — they are not mocked or sandboxed. Use the individual factories when you need per-tool options or custom operation backends; use the aggregators when you want the default set.

Do not use this package as a sandbox, permission policy, secret store, or provider loop. Prism gates tool dispatch with `PermissionPolicy` / `ToolValidator` / trust policies; pass an optional `ExecutionPolicy` (for example from `@arnilo/prism-coding-tools/security`) for path/command approval before side effects. Do not register these tools for an untrusted provider.

```ts
import { createCodingTools } from "@arnilo/prism-coding-tools/agent";
import { createCodingApprovalPolicy } from "@arnilo/prism-coding-tools/security";

const tools = createCodingTools(workspaceRoot, {
  executionPolicy: createCodingApprovalPolicy({
    roots: [workspaceRoot],
    approve: async ({ action }) => host.confirm(action),
  }),
});
```

### pi name mapping

| Prism (`@arnilo/prism-coding-tools/agent`) | pi coding agent |
| --- | --- |
| `shell` | `bash` |
| `read` | `read` |
| `write` | `write` |
| `edit` | `edit` |
| `repo_list` / `repo_search` / `glob` | _(native; no pi equivalent)_ |
| `delete` / `move` | _(native; no pi equivalent)_ |

### Tool selection guide

| Need | Prefer | Avoid |
| --- | --- | --- |
| Enumerate directories | `repo_list` | `shell` `find`/`ls` |
| Match filename patterns | `glob` | `shell` `find` |
| Find text in files | `repo_search` | `shell` `grep`/`rg` |
| Read one file (paged) | `read` | `shell` `cat` |
| Create / full overwrite | `write` | — |
| Targeted replace | `edit` | full `write` rewrite when a small edit works |
| Remove file / empty dir | `delete` | `shell` `rm` |
| Rename / relocate | `move` | `shell` `mv` |
| Arbitrary process | `shell` | dedicated tools above |

### Phase 4 non-goals (0.0.21)

These are **out of scope** for the 0.0.21 package baseline (see roadmap Phase 9 / later for LSP and process work):

- **No PDF / document reader** — text and supported images only via `read`.
- **No trash / recycle daemon** — `delete` / `move` are permanent; host undo is not automatic.
- **No PTY / interactive process control in `shell`** — `shell` stays one-shot; optional `createProcessSessions` covers long-running attach/input with a host-selected PTY backend (`pty: true` requires the `ptyBackend` host option; without one it fails closed as unsupported — see [Process sessions](process-sessions.md)).
- **LSP language-server tools** — not in default aggregators; optional `createLanguageIntelligence` is Phase 9 (see [Language intelligence](language-intelligence.md)).
- **Managed process sessions** — not in default aggregators; optional `createProcessSessions` is Phase 9 (see [Process sessions](process-sessions.md)).
- **GitHub forge adapter** — not in default aggregators; optional `createGitHubForge` is Phase 9 (see [Forge integration](forge-integration.md)); no octokit dependency, no multi-forge abstraction.
- **No recursive directory delete by default** — `delete` refuses non-empty directories unless the per-call `recursive: true` flag is set (plan 018 closeout `delete-glob`; bounded fan-out, symlink children unlinked but never followed).
- **No brace-expansion globs by default** — `glob` supports only `*`, `?`, and `**`; `{a,b}` expansion is opt-in (`braceExpansion`) and bounded (max 128 alternatives / 4096 expanded bytes, fail-closed).

## Inputs / request

### `shell`

Run a shell command and return combined stdout+stderr. Prefer dedicated coding tools (table above) when they fit.

**Inputs:**

| Field | Type | Purpose |
| --- | --- | --- |
| `command` | `string` | Shell command to execute (required). |
| `timeout` | `number` | Timeout in **seconds** (optional; defaults to 600, hard maximum 3600). |

**Outputs:** a `ToolResult` whose `content[0]` is a `TextContent` with the combined output. Non-zero exit is **not** a tool error: it is returned as a normal result with `[Command exited with code N]` appended to the content and `exitCode` in metadata. Timeout and abort are error results that still carry the partial output captured so far.

`shell` result `metadata`:

| Field | Present when | Purpose |
| --- | --- | --- |
| `exitCode` | always | Process exit code, or `null` when the process was killed by timeout/abort. |
| `truncation` | always | `TruncationResult` from the bounded output accumulator. |
| `fullOutputPath?` | successful and truncated only | Host-owned path to retained output. Failed/aborted/timed-out/output-limited calls remove unpublished spills. |
| `totalOutputBytes` | shell executed | Raw bytes retained, never above `maxTotalOutputBytes`. |
| `outputLimitExceeded` / `outputStorageFailed` | shell executed | Attributable resource failure flags. |

Shell resolution honors `options.shellPath` → `SHELL` env → `/bin/bash` → `sh`. The process group is killed on timeout, caller abort, spill failure, or total-output overflow (`process.kill(-pid)` on Unix, `taskkill /F /T` on Windows). Combined output defaults to a 64 MiB total cap (1 GiB hard cap). Spill files use random exclusive creation and Unix mode `0600`; hosts own and must delete a successful result's `fullOutputPath` after consumption.

### `read`

Read a text or image file.

**Inputs:**

| Field | Type | Purpose |
| --- | --- | --- |
| `path` | `string` | Path to the file (relative or absolute; `~` and `file://` expanded). Required. |
| `offset` | `number` | Line to start reading from (1-indexed). |
| `limit` | `number` | Maximum number of lines to read. |
| `findText` | `string` | Literal substring to search for (no regex). When set, the tool pages through the file from `offset` and returns the page starting at the **first matching line** (re-read at the hit line so the match is the first line). No match is an error result with no file body. |
| `findMode` | `"exact" \| "case-insensitive"` | Match mode for `findText` (default `"exact"`). Two literals only — no regex, no fuzzy. |

**Outputs:** text files are scanned incrementally until one requested page, `maxLines`/`maxBytes`, EOF, or `maxScanBytes` (default 64 MiB scanned per call; 1 GiB hard cap). The default path never loads the complete file and returns a `Use offset=N to continue` footer when more remains. Exact total line count is reported only when EOF was already reached in the bounded scan. When `findText` is set, the tool pages through `readText` output (in `maxLines`-sized pages) from `offset`, returns the page whose first line is the first match, and stops at the same `maxScanBytes` scan cap — the search is a literal substring scan (per `findMode`), never regex. Image files (PNG/JPEG/GIF/WebP/BMP by **magic bytes**, not extension) become `[TextContent note, ImageContent]` with base64 `data` and `mimeType`. Oversize images are rejected by `stat` (when available) or `buffer.length` against `maxImageBytes` (default 10 MB) before base64 encoding. An optional `transformImage` callback lets hosts resize or re-encode images without adding image-processing dependencies to the base package. Read failures (missing file, offset beyond end, oversize image, abort, findText scan limit) are error results.

`read` tool options (via `createReadTool(cwd, options)` or `ToolsOptions.read`):

| Option | Default | Purpose |
| --- | --- | --- |
| `maxImageBytes` | `DEFAULT_MAX_IMAGE_BYTES` (10 MB) | Reject image reads larger than this many bytes. |
| `transformImage` | — | Host callback `( { buffer, mimeType } ) => Promise<Buffer>` run after read, before base64. |
| `maxLines` / `maxBytes` | 2000 / 50 KiB | Text page display limits (hard: 100,000 / 1 MiB). |
| `maxScanBytes` | 64 MiB | Raw bytes scanned to reach one page (hard: 1 GiB). |
| `documentReader` | — | Optional host-selected `DocumentReader` (see [Document reader](document-reader.md)): after the image sniff and before the text page, supported PDF/DOCX files are extracted as literal text with `metadata.document = { format, pages, truncatedBy }`. Additive; absent reader = unchanged 0.1.5 behavior. |
| `operations` | local fs | Pluggable bounded `ReadOperations` backend. |
| `executionPolicy` | — | Structured pre-execution policy (see [Coding security](coding-security.md)). |

```ts
import { createReadTool, DEFAULT_MAX_IMAGE_BYTES } from "@arnilo/prism-coding-tools/agent";

const read = createReadTool(cwd, {
  maxImageBytes: DEFAULT_MAX_IMAGE_BYTES,
  transformImage: async ({ buffer, mimeType }) => host.resizeImage(buffer, mimeType),
});

// jump to the first line containing the needle
await read.execute({ path: "src/edit.ts", findText: "createEditTool" }, ctx);
// case-insensitive search starting at offset 50
await read.execute(
  { path: "src/edit.ts", findText: "edittool", findMode: "case-insensitive", offset: 50 },
  ctx,
);
```

`read` result `metadata`:

| Field | Present when | Purpose |
| --- | --- | --- |
| `truncation` | text reads | `TruncationResult`. |
| `image` | image reads | `{ mimeType, resized, bytes }`. `resized` is `true` when `transformImage` ran. |
| `document` | document reads | `{ format, pages, truncatedBy }` when a `documentReader` extracted the file. |

> `autoResizeImages` was removed in 0.1.5; untyped callers now fail closed with a `TypeError` naming `transformImage` before any filesystem access.

### `write`

Create or **overwrite** a file (full replace), creating parent directories as needed. Prefer `edit` for targeted changes.

**Inputs:**

| Field | Type | Purpose |
| --- | --- | --- |
| `path` | `string` | Path to the file to write (relative or absolute). Required. |
| `content` | `string` | Content to write (empty string creates an empty file). Required. |
| `force` | `boolean` | Bypass optional read-before-write guard when the host enabled `requireReadBeforeWrite`. |

**Outputs:** a `TextContent` confirmation naming the **absolute path** with UTF-8 byte and line counts (e.g. `Successfully wrote 42 bytes (3 lines) to /abs/path.txt`). `maxInputBytes` defaults to 8 MiB (64 MiB hard cap); oversized UTF-8 input fails before policy evaluation, directory creation, or write. Write failures and abort are error results. Empty `content` is valid.

Default local `writeFile` uses same-directory temp + `rename` so a crash mid-write cannot truncate the target; custom `WriteOperations` should provide equivalent durability.

`write` result `metadata`: `{ bytes, lines, path }` (absolute path). Concurrent writes to the same path serialize through `withFileMutationQueue`; writes to different paths run in parallel.

### Optional read-before-write guard

Hosts may opt in to a session-scoped soft guard: share one `createReadPathSet()` across `read` / `write` / `edit` and set `requireReadBeforeWrite: true` on write/edit options. Successful `read` marks the path; unread existing-file writes/edits fail with a clear error unless `force: true`. Default is **off** (no behavior change for hosts that ignore it).

```ts
import { createReadPathSet, createReadTool, createWriteTool, createEditTool } from "@arnilo/prism-coding-tools/agent";

const readPaths = createReadPathSet();
const read = createReadTool(cwd, { readPathSet: readPaths });
const write = createWriteTool(cwd, { requireReadBeforeWrite: true, readPathSet: readPaths });
const edit = createEditTool(cwd, { requireReadBeforeWrite: true, readPathSet: readPaths });
```

Since 0.1.3 (plan 015 Task 4) hosts may opt in to persisting the set across restarts via the host-owned `CheckpointStore`:

```ts
import { createReadPathSet, createReadPathSetPersistence } from "@arnilo/prism-coding-tools/agent";

const readPaths = createReadPathSet();
const persistence = createReadPathSetPersistence({ checkpoints, key: sessionId, ownership });
await persistence.restore(readPaths); // on session attach (returns restored count)
await persistence.save(readPaths);    // after reads, before session close
```

Names only (paths are bounded at 1024 entries / 1024 chars each; larger sets fail closed with no partial write). Records live under the `prism.coding-agent.read-path-set` namespace keyed by session id, and `ownership` is part of the trust boundary: restoring under a different tenant/user throws instead of leaking paths. Default is **off** — the set stays in-memory unless the host wires the helper explicitly.

### `edit`

Precise text replacement in an existing file via exact-then-fuzzy matching.

**Inputs:**

| Field | Type | Purpose |
| --- | --- | --- |
| `path` | `string` | Path to the file to edit. Required. |
| `edits` | `Array<{ oldText: string, newText: string }>` | Targeted replacements, each matched against the **original** file (not incrementally). No overlapping/nested edits. Required, non-empty. |
| `force` | `boolean` | Bypass optional read-before-write guard when enabled. |

Each `edits[].oldText` must match a unique, non-overlapping region of the original file. Matching is exact first, then fuzzy (unicode normalization / whitespace collapse).

**Fuzzy silent-success tradeoff (loud):** when exact match fails, fuzzy may still apply a replacement and is **reported** — both the confirmation text (`Successfully replaced N block(s) in {path} (fuzzy match).`) and `metadata.fuzzy: true`. That can edit the wrong region if `oldText` is slightly off (extra/missing whitespace, unicode lookalikes). Prefer exact `oldText` copied from a fresh `read`. On **no match**, the error lists up to 3 nearby lines (1-indexed, clipped to 120 chars) whose first-line substring matches the edit's first non-empty `oldText` line, so the model can correct its `oldText` (skipped when the needle is shorter than 4 chars or no line contains it). Duplicate / non-unique matches already **fail closed** and leave the file unchanged — ambiguity is not silently resolved by picking the first hit.

A BOM is stripped before matching and re-prepended on write; original line endings are restored. Defaults reject targets over 8 MiB, aggregate old/new UTF-8 input over 2 MiB, or more than 100 edits (hard caps: 64 MiB, 16 MiB, and 1,000). Stat and bounded read checks run before matching or mutation. Default local `writeFile` uses same-directory temp + `rename` (crash-safe replace).

**Outputs:** a `TextContent` confirmation (`Successfully replaced N block(s) in {path}.`, with ` (fuzzy match)` appended when the replacement applied via fuzzy matching) plus `metadata`. Any failure — missing/unreadable file, no match (with nearby line context), duplicate (non-unique) match, overlap, empty `oldText`, no-op edit, or abort — is an error result, and the file is left **unchanged** (the match runs before the write).

`edit` result `metadata`: `{ path, diff, patch, firstChangedLine, fuzzy? }` — the absolute path written, a display-oriented diff, a standard unified patch, the first changed line in the new file, and `fuzzy: true` present only when the replacement applied via fuzzy (not exact) matching. These are host-readable; the model only sees the short confirmation (keeps model context small).

### `repo_list`

List repository entries with deterministic relative paths. Uses Node `opendir`/`lstat` only — no glob dependency. Prefer `glob` when you already know a filename pattern. Prefer `repo_search` to find text inside files. Does not follow symlinks; rejects path escapes outside the workspace root. Hidden names and excluded basenames (default `.git`, `node_modules`, `dist`) are skipped unless `includeHidden` is set / host `exclude` is overridden.

#### Git-aware enumeration

`createGitAwareRepositoryOperations(cwd, options?)` is an optional `RepositoryOperations` backend that enumerates via fixed `git ls-files --cached --others --exclude-standard -z` (honors nested `.gitignore`, `$GIT_DIR/info/exclude`, and exclude-standard rules). Inject it through `ToolsOptions.repository.operations` (or per-tool `repository.operations`).

- **Detection:** cached `git rev-parse --is-inside-work-tree`. Outside a Git work tree, or when detection fails, delegates to `options.fallback` (default: `createLocalRepositoryOperations`).
- **Fail closed:** after successful detection, `ls-files` errors throw `RepositoryError` — no silent mid-session fallback.
- **Ignored paths:** stay excluded unless the host sets `includeIgnored: true` (factory option only; never a model-facing tool argument). Tracked-but-ignored files remain visible via `--cached` (Git semantics).
- **Bounds:** at most two Git invocations per operation; stdout capped by `DEFAULT_MAX_LS_FILES_OUTPUT_BYTES` (8 MiB, hard 64 MiB). Existing repo depth/entry/file/result/time caps still apply. No per-file Git spawn; no hand-rolled ignore parser; argv is never model-supplied.
- **Security:** `.git` internals never listed; paths re-checked against the workspace root; symlink escapes match native fail-closed behavior.

```ts
import { createCodingTools, createGitAwareRepositoryOperations } from "@arnilo/prism-coding-tools/agent";

const operations = createGitAwareRepositoryOperations(cwd); // native fallback outside Git
const tools = createCodingTools(cwd, { repository: { operations } });
```

**Inputs:**

| Field | Type | Purpose |
| --- | --- | --- |
| `path` | `string` | Workspace-relative directory or file to list (default root). |
| `includeHidden` | `boolean` | Include dot names (default false). |
| `maxDepth` | `number` | Directory depth cap (default 32, hard 128). |
| `maxResults` | `number` | Page size (default 1,000, hard 10,000). |
| `offset` | `number` | Entries to skip before retaining (default 0). |

**Outputs:** text lines `kind\trelative/path[\tsize]` plus metadata (`truncated`, `truncatedBy`, `nextOffset`, `entries`, scan counts). Continue with `offset=nextOffset` when truncated by results.

### `repo_search`

Search text files under the workspace using literal substring match. Binary files (NUL in a bounded prefix) and oversize files are skipped. Aggregate scanned bytes, matches, line bytes, pattern bytes, and wall time are finite.

**Inputs:**

| Field | Type | Purpose |
| --- | --- | --- |
| `query` | `string` | Literal substring (required). |
| `path` | `string` | Workspace-relative start path. |
| `mode` | `"literal"` (default) \| `"indexed_literal"` \| `"semantic"` | Literal substring by default. Indexed modes exist only when the host enables them (`createRepoSearchTool({ modes })` with an indexed operations composite); missing capability, stale/failed index, or disabled mode returns a stable `ERR_PRISM_INDEX_*` error — never a silent fallback that changes query meaning. `regex` removed in 0.0.18. |
| `caseSensitive` | `boolean` | Default false. |
| `includeHidden` | `boolean` | Default false. |
| `context` | `number` | Context lines before/after each match (default 5, hard 20). Ignored for non-content `outputMode`. |
| `maxMatches` | `number` | Match cap (default 1,000, hard 10,000). |
| `outputMode` | `"content"` \| `"files_with_matches"` \| `"count"` | Result shape (default `content`). |

**Outputs:**
- `content` (default): ripgrep-like lines `path:line:column:text` with optional `path-` / `path+` context.
- `files_with_matches`: unique matching paths only.
- `count`: totals (`N matches in M files`) without line bodies.

Metadata includes `matches`, `truncated`, scan/skip counts; non-content modes also expose `fileCount`. Indexed modes add `untrusted_index`, `indexMode`, `indexState`, `indexRevision`, `indexUpdatedAt` and per-match `[score N.NNN]` suffixes — index text is untrusted and must be re-read before mutation. Full contract: see [Indexed code search](indexed-code-search.md).

### `glob`

Find workspace files by filename pattern without shell `find`. Hand-rolled matcher: `*` (one path segment), `?` (one char), `**` (directories). Brace expansion (`{a,b}`) is **rejected by default**; set `braceExpansion: true` (per call or as the host option) for bounded expansion — max 128 alternatives and 4096 total expanded bytes, unbalanced/nested/empty braces and overflow fail closed. Expansion is textual only (never touches the filesystem) and result patterns still match workspace-relative full paths under the same exclude/hidden/depth/page/time caps. Patterns match workspace-relative full paths (e.g. `src/util/a.ts`). Returns **files only** (directories traversed but not listed). Same exclude/hidden/depth/page/time caps as `repo_list`.

**Inputs:**

| Field | Type | Purpose |
| --- | --- | --- |
| `pattern` | `string` | Glob pattern (required). |
| `path` | `string` | Workspace-relative start directory (default root). |
| `includeHidden` | `boolean` | Default false. |
| `braceExpansion` | `boolean` | Opt-in bounded `{a,b}` expansion (default: host option `createGlobTool(cwd, { braceExpansion })`, else false). |
| `maxDepth` | `number` | Depth cap (default 32, hard 128). |
| `maxResults` | `number` | Page size (default 1,000, hard 10,000). |
| `offset` | `number` | Matches to skip (default 0). |

**Outputs:** one relative path per line plus metadata (`truncated`, `truncatedBy`, `nextOffset`, scan counts). Continue with `offset=nextOffset` when truncated.

### `delete`

High-risk: permanently delete a **single file or empty directory**, or — with the per-call opt-in `recursive: true` — a whole directory tree. Non-empty directories fail closed without the flag. The recursive walk never follows symlinks: symlink children are unlinked as links, so a link pointing outside the workspace root can never drag the deletion out (the outside target is untouched). Every entry counts against a per-call fan-out cap (`maxEntries`, default 10,000, hard 100,000); exceeding it stops with an error naming the cap (partial deletion is reported, never silent). **No trash daemon** — host undo is not automatic; gate with approval policy.

**Inputs:**

| Field | Type | Purpose |
| --- | --- | --- |
| `path` | `string` | File, empty directory, or (with `recursive: true`) directory tree to delete. Required. |
| `recursive` | `boolean` | Per-call opt-in recursive directory delete (default false). |
| `maxEntries` | `number` | Per-call fan-out cap for recursive deletes (default 10,000, hard 100,000). |

**Outputs:** confirmation with absolute path, or error (missing, non-empty dir, escape, abort).

### `move`

High-risk: rename or move a file within the workspace. Dual-path mutation queue (lexicographic lock order). `overwrite` defaults **false**; when true, replaces an existing destination **file** only. Does not create parent directories. **No trash** — host undo is not automatic.

**Inputs:**

| Field | Type | Purpose |
| --- | --- | --- |
| `from` | `string` | Source path. Required. |
| `to` | `string` | Destination path. Required. |
| `overwrite` | `boolean` | Replace existing destination file (default false). |

**Outputs:** confirmation with absolute from/to, or error (missing source, dest exists without overwrite, escape, abort).

### Structured Git tools (`createGitTools`)

Opt-in tools over a host-pinned Git executable (`gitPath`, default `/usr/bin/git`) or sandbox `execFile`. Every invocation uses argument arrays with safe config (`core.hooksPath=/dev/null`, empty credential helper, pager disabled, `GIT_TERMINAL_PROMPT=0`). Shell is never used internally. Git tools are **not** included in `createCodingTools()` / `createAllTools()`.

| Tool | Purpose |
| --- | --- |
| `git_status` | `status --porcelain=v2 -z --branch` → structured branch + entries + `dirty`. |
| `git_diff` | Bounded `--no-ext-diff --no-textconv` diff; oversized output may spill via `artifactWriter`. |
| `git_branch` | `validate` / `list` / `create` / `switch` with `git check-ref-format --branch`. Switch refuses unrelated dirty trees unless `createCheckpoint=true`. |
| `git_worktree` | `list` / `add` / `lock` / `unlock` / `remove` within finite worktree caps; list exposes `locked`/`lockReason` from porcelain. One-shot tool: durable multi-repository worktree lifecycle (create/verify/cleanup with ownership, fencing, and cleanup policy) lives in `createCodingWorkspaceLifecycle` — see [Coding workspaces](coding-workspaces.md). |
| `git_apply` | `check` / `apply` / `reverse`; always `--check` before mutating apply. Apply requires clean/checkpoint; failures restore. |
| `git_commit` | Explicit-path `add` + `commit --no-verify -F <tempfile>`; requires host `commitIdentity`. Allows dirty entries that are exactly the requested paths; unrelated dirt requires checkpoint. Never pushes. |
| `git_pr_handoff` | Bounded `{ base, head, commits, changedPaths, diffstat, checks, artifact? }` for host PR creation. Never authenticates or opens a PR. |
| `git_pr_handoff` (0.2.6 review) | Handoff output feeds `createCodingPatchReviewManifest` — the review binds to base/head, the patch artifact digest, check summaries, and diagnostic summaries (`diagnosticDelta` output) with pending/accepted/rejected/superseded states; see [Coding review and diagnostics](coding-review-and-diagnostics.md). |
| `coding_check` | Included when `checks` are declared: model selects only a name; executable/args/env are host-fixed. |

```ts
import { createGitTools } from "@arnilo/prism-coding-tools/agent";

const gitTools = createGitTools(workspaceRoot, {
  gitPath: "/usr/bin/git",
  commitIdentity: { name: "Prism Bot", email: "bot@example.com" },
  checks: {
    test: { file: "/usr/bin/npm", args: ["test"] },
  },
});
```

### Ask-user decision (`createAskUserDecisionTool`)

Opt-in `ask_user_decision` for ambiguous, high-impact direction choices. Model must pass a question plus 2+ options, each with **exactly 3 pros and 3 cons**. Host supplies `ask` (blocks until the user picks). Not in `createCodingTools` / `createAllTools` / `createReadOnlyTools`.

| Mode | How |
| --- | --- |
| Single (default) | `selectionMode: "single"` → host returns `{ selectedId }` (or length-1 `selectedIds`) |
| Multi | `selectionMode: "multiple"` → `{ selectedIds: [...] }` (non-empty, known ids) |
| Free-text | `allowCustom: true` → host may return `{ customText }` **XOR** selection (never both) |
| Blocking tool | `createAskUserDecisionTool({ ask })` — in-process UI callback |
| Durable workflow | `suspendAskUserDecision(request)` + `createAskUserDecisionResumeValidator()` / `validateAskUserDecisionResume` on `resumeWorkflow` |
| Agent durable adapter | `validateAskUserDecisionAgentResume({ request, answer })` — same validation; **no** new `AgentRunInterruption` kinds in 0.0.11 |

Custom-text caps match question defaults (2 KiB / hard 8 KiB). Options default max 6 (hard 16).
`allowCustom` defaults to `false` on **both** paths when omitted — the tool
path (`parseAllowCustom`) and the workflow suspend path
(`toAskUserDecisionSuspendData`) normalize at accept time, so the persisted
suspension always carries a boolean and survives JSON checkpoint round-trips;
a non-boolean value throws `allowCustom must be a boolean` at accept time,
never at resume time.

```ts
import { createToolRegistry } from "@arnilo/prism";
import {
  createAskUserDecisionTool,
  createCodingTools,
  suspendAskUserDecision,
  createAskUserDecisionResumeValidator,
} from "@arnilo/prism-coding-tools/agent";

const tools = createToolRegistry([
  ...createCodingTools(workspaceRoot),
  createAskUserDecisionTool({
    ask: async ({ question, options, selectionMode, allowCustom }) =>
      ui.ask({ question, options, selectionMode, allowCustom }),
  }),
]);

// Workflow node:
return suspendAskUserDecision({
  question: "Ship sqlite or postgres?",
  options: [/* ≥2 with 3 pros + 3 cons each */],
  selectionMode: "single",
  // allowCustom optional — defaults to false (tool-path parity)
});
// resumeWorkflow(..., { validateResume: createAskUserDecisionResumeValidator() })
```

### Goal → verify helper (`runCodingGoalVerify`)

Thin composition over existing plan Markdown, named checks, workflow `suspend`/`resumeWorkflow`, and bounded PR handoff. **No Goal table / second runtime.** Peer `@arnilo/prism-core/runtime/workflows`. Example: `examples/coding-goal-verify.ts`.

```ts
import { runCodingGoalVerify } from "@arnilo/prism-coding-tools/agent";

const result = await runCodingGoalVerify({
  goal: "Fix the flake",
  cwd: process.cwd(),
  taskId: "flake-1",
  baseBranch: "main",
  branch: "fix/flake",
  checkNames: ["test"],
  checkDefinitions: { test: { file: "/usr/bin/npm", args: ["test"] } },
  runCheck: hostRunCheck,
  buildHandoff: hostBuildHandoff,
  approval: { validateResume: hostValidate },
  checkpoints,
  ownership,
  redactor,
});
```

### Durable coding plans and checkpoints

There is no `CodingRun`, todo database, or second approval engine. Persist executable plan/todos as ordinary workspace Markdown (for example `plans/<task>.md`) and store only bounded metadata under workflow `state.coding`:

| Field group | Stored in checkpoint | Not stored |
| --- | --- | --- |
| Plan / workspace export / patch artifacts | URI + SHA-256 + byte count | File contents, credentials, raw command output |
| Branch / worktree / base | Paths and ref names | Full diffs |
| Named checks | Name + exit code + short summary | Full stdout/stderr |
| Fingerprints | Workflow revision, definition hash, tool/policy fingerprints, optional image digest | Browser storage state, secrets, env |

Use `writeCodingPlanFile` / `readCodingPlanFile` for the workspace artifact, `buildCodingCheckpointMetadata` before `ctx.updateState({ coding })`, and `assertCodingResumeAllowed` before import/resume. Wrong owner/revision/hash/fingerprint fails closed. See `examples/durable-coding-workflow.ts` for a network-free plan → branch → edit → check → approval → handoff composition over `runWorkflow` / `resumeWorkflow` / `startWorkflowBackground`.

## Outputs / response / events

Every tool returns a `ToolResult` with `toolCallId`, `name`, `content` (`readonly ContentBlock[]`), optional `error`, and optional `metadata`. `write` and `edit` serialize per realpath through `withFileMutationQueue` so concurrent calls targeting one file do not interleave. `shell` is marked `exclusive`; tool dispatch serializes it at the turn level. The package emits no events of its own; hosts observe tool execution through the normal Prism `AgentEvent` stream via `dispatchToolCall`.

## Request/response example

```json
// edit request
{ "path": "src/app.ts", "edits": [{ "oldText": "const x = 1;", "newText": "const x = 2;" }] }
```

```json
// edit success result
{
  "toolCallId": "call_1",
  "name": "edit",
  "content": [{ "type": "text", "text": "Successfully replaced 1 block(s) in src/app.ts." }],
  "metadata": { "diff": "...", "patch": "--- src/app.ts\n+++ src/app.ts\n...", "firstChangedLine": 3 }
}
```

```json
// edit no-match result (file unchanged)
{
  "toolCallId": "call_2",
  "name": "edit",
  "error": { "message": "Could not find edits[0] in src/app.ts. The oldText must match exactly including all whitespace and newlines." }
}
```

## Implementation example

Minimal drop-in for any Prism app:

```ts
import { createToolRegistry } from "@arnilo/prism";
import { createCodingTools, createReadOnlyTools } from "@arnilo/prism-coding-tools/agent";

// Full coding set (shell + read + write + edit + repo_list + repo_search + glob + delete + move):
const tools = createToolRegistry(createCodingTools(process.cwd()));

// Or a read-only set for inspection-only agents (read + repo_list + repo_search + glob):
const ro = createToolRegistry(createReadOnlyTools(process.cwd()));
```

Customizing a single tool (force bash, cap output, delegate writes to a remote backend):

```ts
import { createShellTool, createWriteTool } from "@arnilo/prism-coding-tools/agent";

const shell = createShellTool("/repo", {
  shellPath: "/bin/bash",
  commandPrefix: "set -euo pipefail",
  maxLines: 500,
  timeout: 600,
  maxTotalOutputBytes: 64 * 1024 * 1024,
  // Optional: scrub the environment the spawn hook and child process see (default: full process.env clone).
  envAllowlist: ["PATH", "HOME", "LANG"],
});

const remoteWrite = createWriteTool("/repo", {
  operations: {
    writeFile: async (abs, content) => { /* ship to remote */ },
    mkdir: async (dir) => { /* mkdir -p remotely */ },
  },
});
```

Packed capability demo: `examples/coding-tools-capability-gaps.ts` (search modes, glob, read-before-write, delete/move).

## Extension and configuration notes

- **Long coding sessions.** Use `createCodingCompactionStrategy()` from optional `@arnilo/prism-memory/compaction/llm` when history needs a bounded coding handoff. It is selected explicitly through normal `session.compact()` / agent compaction configuration, preserves raw session entries, and prioritizes file paths, patch intent, checks, plan/todo state, blockers, and verification steps. It does not read files, retain full diffs, or create a second coding runtime.
- **Pluggable operation backends.** Every tool accepts an `operations` seam. Custom `ReadOperations` must implement bounded `readText` plus `statFile`; custom `EditOperations` must implement `statFile`; read/write methods receive caps/signals. `BashOperations` must stream through `onData` and honor `signal`/`timeout`. Custom `RepositoryOperations` must honor depth/entry/file/match/scan/time caps and abort (including `glob`). Custom `DeleteOperations` / `MoveOperations` must honor containment and abort. A hostile custom backend can still violate its host-owned contract, so isolate it separately.
- **Per-tool options.** `ShellToolOptions` adds `timeout` and `maxTotalOutputBytes`; `ReadToolOptions` adds `maxScanBytes` and optional `readPathSet`; `WriteToolOptions` / `EditToolOptions` add input caps plus optional `requireReadBeforeWrite` / `readPathSet` / `force`; list/search/glob accept `repository` limits and shared aggregator `ToolsOptions.repository`.
- **Aggregator options.** `ToolsOptions` (`{ executionPolicy?, shell?, read?, write?, edit?, delete?, move?, list?, search?, glob?, repository? }`) threads each sub-object to the matching tool. `createCodingTools()`, `createAllTools()`, and `createReadOnlyTools()` apply the shared policy unless that tool has an explicit per-tool override. Full membership is nine tools; read-only is `read` + `repo_list` + `repo_search` + `glob`.
- **Sandbox composition.** Prefer `@arnilo/prism-coding-tools/security` `createSandboxCodingComposition(cwd, { workspaceMode, sandbox, ... })` (or tools-only wrappers). `workspaceMode` is required: `"sandbox"` keeps shell/read/write/edit/list/search/glob/delete/move on one disposable tree; `"host"` runs against host cwd and never claims containment. Mixed sandbox-shell + host-FS wiring throws unless `allowMixedWorkspaceWiring: true`. Same-tree Git: `createGitTools(composition.workspaceRoot, { execFile: sandbox.execFile, commitIdentity })`.
- **`ToolsOptions`** and the per-tool option types are exported from the package barrel for host configuration.
- No auto-discovery or manifest registration: import and register explicitly. This package registers no extensions and owns no globals (the mutation queue is a process-wide per-path map — see `ponytail:` note in the source).

Read/list/search/glob are observation effects; write/edit/delete/move are optional local mutations; shell/check are unsupported external mutations. `reconcileCodingToolEffect` proves local postconditions or returns `unknown`. See [tool effects](tool-effects.md).

## Security and performance notes

- **Host shell/filesystem access.** These tools run real commands and read/write/list/search/glob/delete/move real files. They provide **no sandbox**. Gate them with Prism `PermissionPolicy` / `ToolValidator` / trust policies before registering them for any provider turn. Shared `executionPolicy` applies to both full and read-only aggregators before filesystem/process side effects. See [Host security guide](host-security.md) and [Security/auth/trust](settings-auth-trust-security.md).
- **High-risk mutations.** `delete` and `move` are permanent (no trash). Prefer host confirmation via `ExecutionPolicy` before allowing them. Do not instruct models to bypass policy/sandbox.
- **Non-zero exit is not an error.** A failing command is a normal `shell` result (exit code in metadata); only timeout/abort/spawn failures are error results. Do not assume `error == undefined` means the command succeeded.
- **Bounded I/O.** `read` streams one page and bounds scan bytes; image/edit reads use stat plus a shared cap-enforcing reader; write/edit inputs are measured before mutation. `repo_list`/`repo_search`/`glob` stream walks and charge depth/entry/file/match/scan/time before retention. Structured Git tools use argument arrays with finite output/path/ref/message/patch caps, disable hooks/credential prompts/external diff by default, and never push or open PRs. `shell` retains only a rolling display tail and synchronously spills accepted raw chunks so stream backpressure cannot grow heap; wall time and total raw output remain finite.
- **Per-path serialization.** Concurrent mutations to the same file serialize; concurrent mutations to different files do not block each other. `move` locks both paths in lexicographic order. The queue is a process-wide map — across sessions in one process, same-path writes still serialize (upgrade path: scope per registry if throughput matters).
- **Bounded image reads.** `read` rejects images over `maxImageBytes` (default 10 MB) by `stat` before read when possible; MIME is detected from magic bytes only. Optional `transformImage` is host-owned — the base package has no image-processing dependency.
- **Fuzzy edit risk.** Silent fuzzy success can mis-apply edits; duplicate matches fail closed. See the `edit` section above.

### Resource-limit defaults and hard caps

| Boundary | Default | Hard cap | Failure point |
| --- | ---: | ---: | --- |
| Display lines / bytes | 2,000 / 50 KiB | 100,000 / 1 MiB | tool construction |
| Text scan per read | 64 MiB | 1 GiB | bounded scan before more input is retained |
| Image | 10,000,000 bytes | 32 MiB | stat and bounded read before base64/transform result use |
| Write UTF-8 input | 8 MiB | 64 MiB | before policy/filesystem mutation |
| Edit target / input / count | 8 MiB / 2 MiB / 100 | 64 MiB / 16 MiB / 1,000 | before target read/matching/write |
| Shell wall time | 600 seconds | 3,600 seconds | process-tree kill |
| Shell total stdout+stderr | 64 MiB | 1 GiB | process-tree kill; spill removal |
| Repo depth / entries / files / page | 32 / 10,000 / 10,000 / 1,000 | 128 / 100,000 / 100,000 / 10,000 | before descending/retaining next entry |
| Search scan / file / matches | 64 MiB / 8 MiB / 1,000 | 1 GiB / 64 MiB / 10,000 | before next file/match retention |
| Search pattern / line / context / time | 512 B / 50 KiB / 5 / 30 s | 4 KiB / 1 MiB / 20 / 300 s | before pattern compile / line retain / deadline |
| Git paths / refs / message | 1,000 / 1 KiB / 64 KiB | 10,000 / 4 KiB / 256 KiB | before process/temp-file creation |
| Git output / diff lines / changed files / patch | 4 MiB / 10,000 / 1,000 / 16 MiB | 64 MiB / 100,000 / 10,000 / 64 MiB | stream before retain; artifact spill optional |
| Worktrees | 4 | 16 | before add |
| Named checks (names / concurrency / time / lines / output) | 8 / 1 / 10 min / 2,000 / 4 MiB | 32 / 4 / 60 min / 100,000 / 64 MiB | construction / before start / line retention |
| PR handoff JSON / commits | 256 KiB / 100 | 1 MiB / 1,000 | before result exposure |
| Plan markdown / todos / todo text | 256 KiB / 1,000 / 512 B | 1 MiB / 10,000 / 4 KiB | before write/parse/checkpoint |
| Coding checkpoint metadata / artifact refs / artifact bytes | 64 KiB / 16 / 256 MiB | 512 KiB / 64 / 2 GiB | before state save / resume verify |
| Check summary text | 1 KiB | 8 KiB | before checkpoint retention |

Every configurable value is a positive safe integer (context may be zero); Prism rejects rather than clamps invalid values. Limits control resources, not authority: they do not replace root containment, approval, validation, or a sandbox.

## Related APIs

- [Language intelligence](language-intelligence.md): optional host-activated LSP contract (`createLanguageIntelligence`) — symbols/definitions/references/diagnostics/hover/rename.
- [Process sessions](process-sessions.md): optional managed long-running processes (`createProcessSessions`) — start/output/input/wait/signal/kill/release.
- [Forge integration](forge-integration.md): optional GitHub adapter (`createGitHubForge`) — issue context, authenticated push, PR create/update, review comments, checks, bounded handoff reconcile; effect-store idempotency, no duplicate PRs/comments on retry, tokens never in argv/logs/events.
- [Tools](tools.md): the host-owned tool harness — `createToolRegistry`, `dispatchToolCall`, filtering, `toolNarrowing` per-turn menus, and the `ToolDefinition` contract these factories satisfy.
- [Public contracts](public-contracts.md): `ToolDefinition`, `ToolResult`, `ToolExecutionContext`, `ContentBlock`, and `JsonObject` shapes.
- [Host security guide](host-security.md): fail-closed checklist for permission policies, tool validation, and trust boundaries that must gate these tools.
- [Tool conformance](tool-conformance.md): assertions for the tool-dispatch blocked-reason matrix these tools participate in.
- [ACP coding-host interop](acp.md): host editors drive these tools through stable ACP v1 — client fs/terminal adapters, `CodingLifecycleEvent` emission (`file_changed` etc. via the `onEvent` options; `plan_changed` also fires from `writeCodingPlanFile`'s `onEvent`, F5), redacted supervisor `subagent_started` / `subagent_stopped` via `observeSupervisorLifecycle`, and permission/elicitation through the shared four-outcome decision model.
- [LLM compaction package](compaction-llm.md): optional `createCodingCompactionStrategy()` retains bounded paths, patch intent, checks, plan/todo state, blockers, and next verification—not complete diffs or raw command output.
