# pi-tools-suite

Local all-in-one Pi extension package.

This package keeps shared Pi tools as ordinary source folders under `src/` and registers them through one entrypoint.

- `src/coding-discipline` — injects a deduplicated silent-mode and quality-discipline block at the very top of the main-session per-turn system prompt for GLM main-session models only (`isGlmModel`) immediately before the LLM request; text-only GLM models get the `lookup` bridge while vision-capable `zai/glm-5.3-flash` inspects images directly; non-GLM models are left untouched; disabled for async sub-agents
- `src/credential-firewall` — opt-in secret firewall for high-confidence outbound/session credential redaction; disabled by default
- `src/ast-grep` — `ast_grep` / `ast_apply`
- `src/async-subagents` — `subagents` tool and sub-agent slash commands, including oh-my-openagent-style `/ultrawork` (`/ulw`) and `/hyperplan` orchestration prompts; agent roles are Markdown files under `src/async-subagents/agents/*.md` plus project `.pi/agents/*.md`, while `/subagent-preset` selects model-pool presets from the bundled/project `agents/presets.jsonc`; includes the `oracle` profile for strong second opinions; enforces a 30-minute per-agent execution timeout, project-wide concurrency queueing, optional per-agent retry/backoff, and `result.json` structured metadata/chaining fields next to raw `result.md`; stores project-local run files and a registry under `.pi/subagents/` so result/status collection can recover after compaction or reload while the main session remains alive
- `src/lsp` — shared LSP diagnostics hook/library that enriches mutating tool results with diagnostics and shuts down language servers on session shutdown
- `src/comment-checker` — AI-slop comment guard that listens to the `tool_result` event for `write` / `edit` / `apply_patch` mutations, extracts net-new code comment lines, classifies them (filler phrasing, restating code, decorative separators, generic paraphrasing, or — under aggressive strictness — any non-valuable comment), and appends a short nudge to the tool result so the agent removes unnecessary comments on its next turn; TODO/FIXME, license headers, docstrings, pragmas, linter directives, shebangs, and decorators are never flagged; language-agnostic across `//` / `/* */` / `#` / `--` / `<!-- -->` / triple-quote comment styles; per-session deduplication (at most one nudge per 30 s) prevents fix/remark loops; configured via the `commentChecker` section (`enabled`, `strictness`: `conservative` | `balanced` | `aggressive`, default `balanced`) or `PI_COMMENT_CHECKER_ENABLED` / `PI_COMMENT_CHECKER_STRICTNESS`
- `src/session-name` — `session_name` tool for reading or setting the current session title directly from tool calls, without relying on slash-command parsing
- `src/session-recovery` — branch- and compaction-aware `session_overview`, `session_read_section`, `session_search`, and `session_recovery_context` tools for bounded recovery from Pi's raw append-only session history
- `src/context-gateway` — off/observe/enforce result shaping; enforce keeps safe test/build compacts and bounds over-budget structured `web_search` / `web_fetch` provider content while retaining the complete producer details in raw session history for `session-recovery`; active modes also keep a privacy-safe rotated JSONL efficiency log with gross avoided context, recovery/artifact-read tax, conservative net estimates, and finalized provider usage
- `src/repo-discovery` — `/idx-init`, `/idx-update`, and idx-backed `repo_architecture` / `repo_structure` / `repo_ast` / `repo_search` / `repo_explain` / `repo_deps` plus the unified `repo_knowledge` contract/wiki tool; repo tools and repo-aware mutation guidance register only when the launch project has `.indexer-cli` **and** an executable `idx` is available on `PATH`
- `src/antigravity-auth` — `antigravity` custom provider with Google Antigravity OAuth login, startup account list, auth.json-only runtime account loading, `/antigravity-add-account` OAuth append into rotation, `/antigravity-account` status display, account rotation/failover, model registration with live route mapping (current Antigravity catalog: Gemini 3.5/3.6/3.7/3.8 Flash, Gemini 3.1 Pro, Claude Sonnet 4.6 Thinking, Claude Opus 4.6 Thinking, GPT-OSS 120B Medium, plus legacy Antigravity aliases and Gemini CLI mirrors), and streaming through the Cloud Code Assist unified gateway
- `src/opencode-import` — `/opencode-import` for bounded migration of supported OpenCode OpenAI/Codex, GitHub Copilot, Z.ai, and Antigravity credentials into Pi; existing entries are preserved unless `--force` is passed
- `src/todo` — `todo` tool, `/todos`, `/todos-persist`, `/todos-scope`, and `/todos-clear` (also `/todos clear`); supports parent/subtask hierarchy, blockers, ready-task filtering, deferred out-of-scope items, batch operations, JSON/Markdown import/export, automatic clearing when all visible todos are completed, and optional project persistence via `/todos persist on` or `/todos-persist on`; localization/i18n has been removed
- `src/model-tools` — model-specific tool aliases such as Claude/GLM-style `Read` / `Edit` / `Write` / `Bash` / `Grep` / `Glob` / `LS`, GPT/Codex-style `shell`, and model-gated `apply_patch`
- `src/usage` — `/usage` command and startup hint for read-only AI quota checks across OpenAI, Zhipu AI, Z.ai, and Google Antigravity, including Antigravity quota by model
- `src/web-search` — `web_search` and `web_fetch` tools migrated from `@ollama/pi-web-search`; uses local Ollama by default or the official Ollama cloud API when an API key is configured, supports Tavily Search/Extract fallback, provides `/web-credentials` for secure user-level key storage, honors `OLLAMA_HOST`, supports request timeouts via `timeout_ms` / `PI_WEB_SEARCH_TIMEOUT_MS`, and reports provider-specific errors
- `src/dcp` — headless Dynamic Context Pruning ported from `opencode-dynamic-context-pruning` for the Pi SDK: explicit `compress` tool with range and message modes, `/dcp` commands (context, stats, sweep, manual, decompress, recompress, compress), same-call overlap validation, recoverable compressed-block rollups, grouped message-mode skip diagnostics, stable raw-message anchors when available, protected user/tool preservation, deduplication, error purging, and context nudges; visualization is left to `compress` tool responses and the renderer-owned context-percent click dialog
- `src/prompt-commands` — user slash-command builder: `/prompt-commands` opens a CRUD menu for saved prompt-backed slash commands, stores them under `promptCommands` in `~/.config/pi/pi-tools-suite.jsonc`, reloads after edits, and runs each saved prompt as a normal user message
- `src/resource-registry` — `/registry` manages one private Git registry with global reusable `skills/` and `agents/` plus project-scoped `projects/<project-key>/tasks.jsonc`, `projects/<project-key>/task-attachments/`, `projects/<project-key>/plans/`, and `projects/<project-key>/TODO.md`; skills install into `.pi/skills/<name>/`, agents into `.pi/agents/<name>.md`, while `push tasks|plans|todo|project` and `pull tasks|plans|todo|project` synchronize `.pi/tasks.jsonc` (with referenced `.pi/task-attachments` rebased through portable registry-only markers), `.pi/plans/`, and `.pi/TODO.md` only for the current project; the project key is normally derived from Git `origin` (SSH and HTTPS forms normalize to the same key) and can be overridden per project with `/registry project-key <key>` in `.pi/pi-tools-suite.jsonc`; `status` fetches and groups skills, agents, tasks, plans, and TODO by state (up to date, updates, conflicts, local-only, remote-only, and so on) using `.pi/registry.json` provenance, and the tasks hash includes referenced attachment bytes; reusable `install`, `update`, `push`, remote-only `remove`, and local-only `uninstall` support TUI flows and bulk `all` forms, `delete`/`rm` alias remote `remove`, `remove-local`/`local-remove` alias `uninstall`, destructive bulk operations require interactive confirmation, and every successful skill/agent mutation reloads resources once; `/registry configure <git-url> [branch]` stores the registry remote in the user config; Git authentication is delegated to the user's normal Git/SSH credential setup and the local clone is only a disposable cache

`index.ts` is intentionally only a thin auto-discovery shim that re-exports `src/index.ts`. There is no `pi.extensions` manifest here, so local Pi auto-discovery loads the suite once via `~/.pi/agent/extensions/pi-tools-suite/index.ts` and does not double-register tools.

Registration order is preserved in `src/index.ts`: coding-discipline, ast-grep, async-subagents, lsp, comment-checker, session-name, session-recovery, repo-discovery command/tool gate, antigravity-auth provider, OpenCode import, todo, model-tools, usage, web-search, context-gateway, truncation-metadata-normalizer, dcp, prompt-commands, resource-registry, credential-firewall, then codex-reasoning-fix. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. Repo-aware mode requires both project `.indexer-cli` state and an executable `idx`; when enabled, `repo_*` tools stay active ahead of overlapping lower-level aliases. If `idx` is unavailable, the suite falls back to ordinary Read/Grep/LSP/sub-agent guidance and does **not** implicitly install, initialize, or create index state. `/idx-init` is the explicit setup/repair path and should be run only with user permission.

## Repository knowledge and spec maintenance

When repo-aware mode is available, `repo_knowledge` is the single model-facing
surface for behavioral specs/contracts. It wraps the first-class `idx context`
and `idx wiki` knowledge layer while ordinary file tools remain responsible for
editing the primary spec documents themselves.

Read/query actions:

- `context` — primary contract + implementation ranges + tests + freshness in a
  bounded response;
- `search` / `show` — find or inspect authoritative project knowledge;
- `status` / `audit` / `catalog` — freshness and project knowledge health;
- `discover` — classify new/moved/changed document candidates, including
  low-signal documents through the explicit all-unclassified mode;
- `impact` — review known and uncovered contract impact for this task's changed
  paths (preferred) or a Git base fallback.

Metadata mutation actions are explicit: `record`, `relate`, `verify`, and
`remove`. They do not edit primary documents. The wrapper requires a source
review acknowledgement before `record`, concrete semantic evidence review before
`relate`/`verify`, and an explicit metadata-only acknowledgement before `remove`.

For a **material behavior-changing implementation** in repo-aware mode, the
model-facing contract is:

1. Find the existing primary behavioral contract before or while implementing.
2. Keep that primary spec aligned with the intended behavior in the same task.
   If no suitable primary contract exists, create a focused spec with the normal
   Edit/Write/`apply_patch` tools before recording its metadata.
3. After implementation, run task-scoped `repo_knowledge` `impact` on the files
   changed by this task; review uncovered paths and new/moved documents.
4. Repair only evidence-backed relations. Similarity or graph proximity alone
   never authorizes a durable relation, and a reviewed no-impact result is valid.
5. `record` means classified/indexed, **not verified**. Run `verify` only after
   reading the primary source and checking relevant code/tests/evidence. Changed
   code never automatically rewrites spec semantics.

Mechanical refactors, typo/formatting edits, exact renames, and other changes
that do not alter project behavior do not require this knowledge-maintenance
lifecycle. When `idx` is unavailable or the project is not indexed, none of
these requirements are injected; use the normal repository workflow instead.
For repo-aware projects, completing a todo when only the final active todo
remains adds one compact reminder to reconcile affected specs and repo knowledge
before closing that final todo. File mutations themselves do not emit knowledge
reminders.

## Session recovery

When context compaction obscures the task, start with `session_overview`, inspect a
relevant ID with `session_read_section`, and use `session_search` once a concrete
phrase, path, symbol, tool, or error is known. `session_recovery_context` is the
compact convenience view for original/latest user instructions, file evidence,
recent errors, pending calls, and the last meaningful action. All four tools read
through Pi's active `SessionManager`; they do not accept arbitrary session paths.
They default to the active branch, while `scope: "all"` includes abandoned branches.
See [`docs/session-recovery.md`](docs/session-recovery.md) for the full contract and
limits.

## Disabling modules

Disable suite modules without editing `src/index.ts` via config or environment variables. On startup, `~/.config/pi/pi-tools-suite.jsonc` is created if it is missing with a commented `disabledModules` template. Config is loaded from that file, then `$PI_CONFIG_DIR/pi-tools-suite.jsonc`, then the nearest project `.pi/pi-tools-suite.jsonc`; later layers win.

```jsonc
{
  "disabledModules": ["web-search"]
}
```

Environment overrides are applied last:

```bash
PI_TOOLS_SUITE_DISABLED_MODULES=web-search pi ...
PI_TOOLS_SUITE_DISABLED=1 pi ...   # disables all pi-tools-suite modules
```

`disabledExtensions`, `enabledModules`, `enabledExtensions`, and an `extensions` map are accepted as aliases for the same module names. Use `*` or `all` in `PI_TOOLS_SUITE_DISABLED_MODULES` to skip every registered module.

`credential-firewall` is disabled by default. Enable it explicitly with `"modules": { "credential-firewall": true }`. When enabled it replaces high-confidence secret material in the final provider payload with stable placeholders such as `<SECRET:github_token:1>`. `secretFirewall.sessionHygiene` (default `true`) applies the same redactor to tool results and completed messages before they remain in session history; `secretFirewall.notify` controls warnings. Entropy-only detection is intentionally not used yet to avoid corrupting hashes, IDs, minified assets, and other high-entropy non-secrets.

Saved prompt slash commands are stored under `promptCommands`. Use `/prompt-commands` to create, edit, rename, delete, list, show the config path, or run them from an interactive menu. After a CRUD edit the module reloads Pi resources so the slash-command list reflects the config. Each saved command sends its saved prompt as a user message.

```jsonc
{
  "promptCommands": {
    "commands": {
      "review": {
        "description": "Run a focused code review prompt",
        "prompt": "Review the current change. Focus on correctness and risks."
      }
    }
  }
}
```

Todo thinking can be enabled globally and forced to a fixed level for selected models. `todoThinkingOverrides` keys accept exact `provider/model` or bare-model names plus `*` and `?` wildcards. Full provider/model matches beat bare-model matches, exact matches beat wildcards, and the more specific wildcard wins. The override is applied at runtime to create/update and batch create/update mutations even when the model requests another level or omits `thinking`. Unsupported levels are normalized to the nearest level supported by the current model. Later config layers can remove an inherited entry with `null`.

```jsonc
{
  "todoThinking": true,
  "todoThinkingOverrides": {
    "zai/glm-5.3": "max",
    "cheap-provider/*": "high"
  }
}
```

DCP settings are stored only under `dcp` in the user shared config file `~/.config/pi/pi-tools-suite.jsonc`. Legacy standalone `dcp.jsonc`, `$PI_CONFIG_DIR`, and project-local `.pi/pi-tools-suite.jsonc` DCP settings are intentionally ignored by the ported headless DCP module.

```jsonc
{
  "dcp": {
    "enabled": true,
    "compress": {
      "minContextPercent": "20%",
      "maxContextPercent": "55%",
      "maxContextLimit": 160000,
      "nudgeFrequency": 1,
      "iterationNudgeThreshold": 6,
      "nudgeForce": "strong",
      "protectedTools": ["compress", "write", "edit", "subagents"],
      "autoCompress": {
        "enabled": false,
        "patience": 2,
        "summarizerModel": [],
        "summarizerFallbackModels": [],
        "timeoutMs": 20000
      }
    },
    "strategies": {
      "emergencyCurrentTurnPruning": {
        "enabled": true,
        "hardContextPercent": 0.82,
        "targetContextPercent": 0.70,
        "patience": 2,
        "keepRecentToolPairs": 8,
        "minOutputTokens": 500,
        "maxSuggestions": 8,
        "protectedTools": []
      }
    },
    "modelOverrides": {
      "openai-codex/gpt-5*": {
        "compress": {
          "minContextPercent": "26%",
          "maxContextPercent": "46%"
        }
      },
      "openai-codex/gpt-5.4-mini": {
        "compress": {
          "minContextPercent": "20%",
          "maxContextPercent": "38%"
        }
      },
      "zai/*": {
        "compress": {
          "minContextPercent": "16%",
          "maxContextPercent": "30%"
        }
      },
      "antigravity/*sonnet*": {
        "compress": {
          "minContextPercent": "22%",
          "maxContextPercent": "40%"
        }
      },
      "antigravity/gemini-3.1-pro*": {
        "compress": {
          "minContextPercent": "24%",
          "maxContextPercent": "42%"
        }
      },
      "antigravity/gemini-3-flash*": {
        "compress": {
          "minContextPercent": "18%",
          "maxContextPercent": "34%"
        }
      },
      "antigravity/gemini-2.5-flash*": {
        "compress": {
          "minContextPercent": "18%",
          "maxContextPercent": "32%"
        }
      },
      "antigravity/antigravity-claude-opus-4-6-thinking": {
        "compress": {
          "minContextPercent": "26%",
          "maxContextPercent": "44%"
        }
      }
    }
  }
}
```

`minContextPercent` / `maxContextPercent` accept legacy fractions (`0.25`), percent strings (`"25%"`), or absolute token counts when Pi knows the current model context window. `minContextLimit` / `maxContextLimit` and `modelMinContextLimits` / `modelMaxContextLimits` are explicit absolute-or-percent aliases. `modelOverrides` and the `modelMin*` / `modelMax*` maps support exact model keys plus `*` / `?` wildcard patterns; matching is applied from generic to specific so exact bare-model matches override bare wildcards, and exact `provider/model` matches override provider wildcards. Array fields are union-merged, so model-specific `protectedTools` extend the defaults instead of replacing them. If `compress.protectUserMessages` is enabled, range compression appends selected user messages verbatim instead of rejecting the range; individual message compression still skips protected raw user messages. Protected tool outputs are copied into summaries for tools protected by name or `protectedFilePatterns`; protected `subagents` result reads also try to include the saved `result.md` artifact when available.

`compress.autoCompress.enabled` is `false` by default. When explicitly enabled, its `patience` counts completed correlated main-provider opportunities, not repeated context transforms; `summarizerModel` supplies the primary summarizer list and `summarizerFallbackModels` appends explicit ordered fallbacks. Existing multi-entry `summarizerModel` configs remain supported. When both lists are empty, DCP uses the bounded extractive fallback without a model call. A configured model summarizer receives a bounded sub-deadline (75% of `timeoutMs`) so deterministic extractive fallback and durable publication retain time inside the whole operation budget. After a failed model-backed auto-compression attempt, subsequent automatic attempts in that session skip the model summarizer and use deterministic extraction. Auto commit is accepted only when the full projected replacement has positive gain and meets the current budget-recovery target.

`strategies.emergencyCurrentTurnPruning` is the default-enabled lossy safety floor for a single unfinished turn. DCP first emits emergency reminders and offers only safe old same-turn tool-result candidates. After `patience` completed ignored opportunities, at the model-independent `hardContextPercent`, or when exact auto-compression fails while input capacity is already exceeded, it replaces eligible oldest result bodies until the estimated provider context reaches `targetContextPercent` or a safe capacity margin. User messages, configured/protected data, the newest `keepRecentToolPairs`, and results without **completed** provider evidence are never selected. HTTP 2xx alone is not evidence; DCP promotes eligibility only after an unambiguously correlated successful finalized assistant response, and ambiguous retries/interleaving fail closed. If eligible recovery still cannot fit the request, DCP emits a user-visible blocked diagnostic with the capacity reason before abort/handoff. The raw session transcript is unchanged. Setting `enabled` to `false` disables same-turn candidates and lossy pruning, but keeps the non-destructive emergency reminder.

DCP durable state is journaled as append-only `dcp-journal` custom entries in
the Pi session JSONL. Resume/reload replays that journal; the current
implementation has no legacy sidecar importer or dual-write path. Protected
subagent artifacts are optional bounded recovery input: reads are async, rooted
at the session cwd, reject symlink escapes and oversized files, and never
silently truncate a required protected artifact.

Set `dcp.debug: true` to write a JSONL debug log of DCP context/prune/compress events to `~/.pi/agent/dcp-debug.jsonl` (override the path with `PI_DCP_DEBUG_LOG`, or enable without config via `PI_DCP_DEBUG=1`); off by default. The log is size-limited and rotated: once it reaches `dcp.debugLog.maxBytes` (default `5242880` = 5 MB) it is renamed to `.1`, older backups shift down (`.1`→`.2`, …) and the oldest beyond `dcp.debugLog.maxBackups` (default `3`, minimum `1`) is dropped; override either with `PI_DCP_DEBUG_MAX_BYTES` / `PI_DCP_DEBUG_MAX_BACKUPS`.

## LSP setup

The LSP module reads language-server definitions from `lsp.servers` in the shared config file:

```text
~/.config/pi/pi-tools-suite.jsonc
```

Install the language servers used by the bundled example config. The commands below are written for macOS/Linux with a POSIX shell; most npm, `dotnet`, `pip`, and `rustup` commands also work on Windows, but paths and the GDScript wrapper differ.

```bash
# TypeScript / JavaScript
npm install -g typescript typescript-language-server

# Svelte
npm install -g svelte-language-server

# Vue
npm install -g @vue/language-server

# Python
python3 -m pip install --user python-lsp-server

# Go
go install golang.org/x/tools/gopls@latest

# C / C++ (clangd)
brew install llvm
# or use your distro's clangd package: apt install clangd, dnf install clang-tools-extra, ...

# Lua
brew install lua-language-server

# Bash
npm install -g bash-language-server

# C# / Unity
dotnet tool install -g Microsoft.CodeAnalysis.LanguageServer

# GDScript / Godot
# Install Godot 4.x and make sure the `godot` command is on PATH.
# macOS Homebrew example. On other OSes, use the official Godot installer/package.
brew install --cask godot

# Ruby
brew install ruby-lsp
# or, if using RubyGems directly:
gem install ruby-lsp

# Rust
rustup component add rust-analyzer

# Markdown
npm install -g vscode-langservers-extracted
```

Extra runtime requirements:

- The GDScript wrapper also needs `nc` and `python3`; both are available by default on most macOS/Linux setups. The wrapper starts Godot headless on a free localhost port and the LSP manager kills the whole process group on shutdown/abort.
- C# expects `~/.dotnet/tools` on `PATH`, or an explicit `bin` path such as `~/.dotnet/tools/roslyn-language-server` in the config.
- Rust diagnostics require a Rust project root such as `Cargo.toml`.
- C#/Unity diagnostics require a project root such as `*.csproj`, `*.sln`, or Unity `ProjectSettings/ProjectVersion.txt`.
- Markdown link diagnostics are provided by `vscode-markdown-language-server` when validation is enabled; Mermaid fence checks are supplemented locally by pi-tools-suite, so no separate Mermaid LSP is required for the default diagnostics.

OS notes:

- macOS/Linux: the sample commands and default GDScript wrapper are intended to work as-is once the binaries are on `PATH`.
- Windows: npm, Python, .NET, Ruby, Rust, and Markdown servers can be installed natively, but adjust executable paths, for example `%USERPROFILE%\.dotnet\tools\roslyn-language-server.exe`. The bundled GDScript wrapper uses `bash`, `nc`, POSIX signals, and process groups, so use WSL/Git Bash or replace that server command with a Windows-specific wrapper.
- Package-manager commands vary by distro. Replace `brew install ...` with your OS package manager or the official installer where appropriate.

Minimal shared config shape:

```jsonc
{
  "lsp": {
    "servers": [
      {
        "id": "typescript",
        "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.mjs"],
        "rootMarkers": ["tsconfig.json", "package.json"],
        "bin": "typescript-language-server",
        "args": ["--stdio"],
        "languageIdByExtension": {
          ".ts": "typescript",
          ".tsx": "typescriptreact",
          ".js": "javascript",
          ".jsx": "javascriptreact",
          ".mjs": "javascript"
        }
      }
    ]
  }
}
```

Project-local overrides can be added in `.pi/pi-tools-suite.jsonc`; pi-tools-suite asks for trust before using project-local LSP binaries.

### Popular language-server examples

Copy the entries you need into `lsp.servers` of the shared config. Values mirror the commented templates shipped in the generated config file; where they disagree, the generated file is authoritative. Both diagnostics modes are on by default: servers that support pull diagnostics (`textDocument/diagnostic`) are queried directly, and push diagnostics (`publishDiagnostics`) are awaited for every server — `pullDiagnostics: false` / `waitForPublishDiagnostics: false` disable either side explicitly. Servers start lazily: one spawns only after a mutating tool (Edit/Write/ast-grep/apply_patch) touches a file matching `include`, and diagnostics land in that tool's result.

```jsonc
{
  "lsp": {
    "servers": [
      // Svelte (verified with svelte-language-server): compiler + embedded TS/JS diagnostics
      {
        "id": "svelte",
        "include": ["**/*.svelte"],
        "exclude": ["**/node_modules/**"],
        "rootMarkers": ["svelte.config.js", "package.json"],
        "bin": "svelteserver",
        "args": ["--stdio"],
        "startupTimeoutMs": 30000,
        "diagnosticsWaitMs": 8000,
        "languageIdByExtension": { ".svelte": "svelte" }
      },
      // Vue (Volar)
      {
        "id": "vue",
        "include": ["**/*.vue"],
        "exclude": ["**/node_modules/**"],
        "rootMarkers": ["package.json"],
        "bin": "vue-language-server",
        "args": ["--stdio"],
        "startupTimeoutMs": 30000,
        "diagnosticsWaitMs": 8000,
        "languageIdByExtension": { ".vue": "vue" }
      },
      // Python (python-lsp-server)
      {
        "id": "python",
        "include": ["**/*.py", "**/*.pyi"],
        "exclude": ["**/.git/**", "**/node_modules/**", "**/__pycache__/**", "**/.venv/**", "**/venv/**", "**/.tox/**", "**/.mypy_cache/**", "**/.ruff_cache/**"],
        "rootMarkers": ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "poetry.lock", ".git"],
        "bin": "pylsp",
        "args": [],
        "languageIdByExtension": { ".py": "python", ".pyi": "python" }
      },
      // Go (gopls, push diagnostics)
      {
        "id": "go",
        "include": ["**/*.go"],
        "exclude": ["**/.git/**", "**/vendor/**"],
        "rootMarkers": ["go.mod", ".git"],
        "bin": "gopls",
        "args": [],
        "startupTimeoutMs": 20000,
        "diagnosticsWaitMs": 8000,
        "languageIdByExtension": { ".go": "go" }
      },
      // Rust (rust-analyzer, push diagnostics)
      {
        "id": "rust",
        "include": ["**/*.rs"],
        "exclude": ["**/.git/**", "**/node_modules/**", "**/target/**"],
        "rootMarkers": ["Cargo.toml", "rust-project.json", ".git"],
        "bin": "rust-analyzer",
        "args": [],
        "startupTimeoutMs": 20000,
        "diagnosticsWaitMs": 20000,
        "pullDiagnostics": false,
        "waitForPublishDiagnostics": true,
        "languageIdByExtension": { ".rs": "rust" }
      },
      // C / C++ (clangd, push diagnostics)
      {
        "id": "clangd",
        "include": ["**/*.c", "**/*.cc", "**/*.cpp", "**/*.cxx", "**/*.h", "**/*.hh", "**/*.hpp"],
        "exclude": ["**/.git/**", "**/node_modules/**"],
        "rootMarkers": ["compile_commands.json", "CMakeLists.txt", "Makefile", ".clang-format", ".git"],
        "bin": "clangd",
        "args": [],
        "startupTimeoutMs": 20000,
        "diagnosticsWaitMs": 8000,
        "languageIdByExtension": {
          ".c": "c", ".cc": "cpp", ".cpp": "cpp", ".cxx": "cpp",
          ".h": "c", ".hh": "cpp", ".hpp": "cpp"
        }
      },
      // C# / Unity (Roslyn)
      {
        "id": "csharp",
        "include": ["**/*.cs", "**/*.csx"],
        "exclude": ["**/.git/**", "**/node_modules/**", "**/bin/**", "**/obj/**", "**/.vs/**", "**/Library/**", "**/Temp/**", "**/Logs/**"],
        "rootMarkers": ["*.sln", "*.csproj", "global.json", "Directory.Build.props", "Directory.Packages.props", "Packages/manifest.json", "ProjectSettings/ProjectVersion.txt", ".git"],
        "bin": "~/.dotnet/tools/roslyn-language-server",
        "args": ["--stdio", "--autoLoadProjects", "--logLevel", "Error"],
        "startupTimeoutMs": 30000,
        "diagnosticsWaitMs": 15000,
        "languageIdByExtension": { ".cs": "csharp", ".csx": "csharp" }
      },
      // Ruby
      {
        "id": "ruby",
        "include": ["**/*.rb", "**/*.rake", "**/Gemfile", "**/Rakefile", "**/*.gemspec"],
        "exclude": ["**/.git/**", "**/node_modules/**", "**/vendor/bundle/**", "**/.bundle/**", "**/tmp/**", "**/log/**"],
        "rootMarkers": ["Gemfile.lock", "*.gemspec", "Rakefile", ".ruby-version", ".git"],
        "bin": "ruby-lsp",
        "args": [],
        "startupTimeoutMs": 60000,
        "diagnosticsWaitMs": 10000,
        "languageIdByExtension": { ".rb": "ruby", ".rake": "ruby", ".gemspec": "ruby" }
      },
      // Lua (lua-language-server)
      {
        "id": "lua",
        "include": ["**/*.lua"],
        "exclude": ["**/.git/**", "**/node_modules/**"],
        "rootMarkers": [".luarc.json", ".git"],
        "bin": "lua-language-server",
        "args": [],
        "startupTimeoutMs": 30000,
        "diagnosticsWaitMs": 6000,
        "languageIdByExtension": { ".lua": "lua" }
      },
      // Bash
      {
        "id": "bash",
        "include": ["**/*.sh", "**/*.bash"],
        "exclude": ["**/.git/**", "**/node_modules/**"],
        "rootMarkers": [".git"],
        "bin": "bash-language-server",
        "args": ["start"],
        "startupTimeoutMs": 15000,
        "diagnosticsWaitMs": 5000,
        "languageIdByExtension": { ".sh": "shellscript", ".bash": "shellscript" }
      },
      // Markdown (link validation settings ship in the generated config template)
      {
        "id": "markdown",
        "include": ["**/*.md", "**/*.markdown", "**/*.mdown", "**/*.mkd", "**/*.mmd"],
        "exclude": ["**/.git/**", "**/node_modules/**"],
        "rootMarkers": [".git", "package.json", "README.md"],
        "bin": "vscode-markdown-language-server",
        "args": ["--stdio"],
        "startupTimeoutMs": 15000,
        "diagnosticsWaitMs": 5000,
        "languageIdByExtension": { ".md": "markdown", ".markdown": "markdown", ".mdown": "markdown", ".mkd": "markdown", ".mmd": "markdown" }
      }
    ]
  }
}
```

Notes:

- Svelte resolves `svelte` and `typescript` from the workspace `node_modules`, so project-local versions win; `.svelte.js`/`.svelte.ts` runes modules are not covered because their extensions collide with the TypeScript server.
- Vue requires `@vue/language-server` v2+ (the `vue-language-server` binary) plus the workspace's `vue` package for template type-checking.
- The full commented templates (including GDScript via a headless Godot wrapper and the complete Markdown link-validation `settings`) are written to the shared config file on first run.

## Async sub-agents

Model selection uses the ordered candidates from each agent's Markdown file,
filtered by the selected preset's available models and runtime capabilities.
Explicit task/CLI model overrides bypass the pool. Setting
`ASYNC_SUBAGENTS_FORCE_CURRENT_MODEL=1` (or
`PI_SUBAGENTS_FORCE_CURRENT_MODEL=1`) deliberately selects the parent model and
strips conflicting model arguments; this is not the economical default.

The six built-in modes are `research` (read-only evidence and independent
review), `implement` (bounded code, docs, tests, or UI changes), `verify`
(run checks and diagnose logs without fixing files), `ui-qa` (real browser,
terminal/TUI, and desktop-GUI verification), `frontier-review` (independent
post-implementation review), and `oracle` (deliberate strong second opinion).
Ordinary workers use economical model candidates; no built-in parent-tier
rule promotes them to a flagship. Oracle is the exception, not an automatic
retry for difficult work. Task-specific discipline belongs in the brief.

Delegate when a suitable lower-cost worker can handle bounded work or noisy
intermediate evidence should stay outside the parent context. One sequential
task can qualify. Keep decisions and integration in the parent; read compact
results and verify selectively rather than repeating the worker's investigation.
Do trivial reads/edits directly. Redirect a noisy command to a log without an
extra LLM when no interpretation is needed. `verify`'s no-edit instruction is
a behavioral contract, not a read-only filesystem sandbox for its shell.

Run `/ultrawork` or `/ulw` for orchestration, `/hyperplan` to pressure-test a
plan, or set `ULTRAWORK=1` to apply the orchestration prompt to normal inputs.
`ULTRAWORK_AUTO=1` classifies only the first normal input on non-GPT parents;
GPT-like parents skip that automatic transform, not ordinary delegation.

See [Model pools and migration](docs/subagent-model-pools.md) for the selection
contract, configuration examples, override rules and legacy compatibility.

### Parent-first role selection

The parent normally selects an explicit `subagentType` from the effective
system-prompt catalog, preferring a matching project-local specialist. Valid
explicit types bypass the LLM router entirely; presets, model selection, tools,
and role instructions are still applied by the normal config resolver.
Model/thinking overrides are not substitutes for selecting a role.

The router remains enabled as a fallback for omitted types: use it when the role
is unclear or the user explicitly requests automatic routing. Only omitted
tasks are classified, in one batch; the parent's explicit choices are preserved.
Real UI QA still requires explicit `subagentType: "ui-qa"`. Explicit legacy
`browser-qa` requests normalize to `ui-qa` for compatibility.

Unknown explicit types and failed/incomplete automatic routing reject the
**entire spawn batch before run state or child processes are created**. The tool
returns an error with affected task IDs and available types; the parent should
correct the roles and resubmit the whole batch. Provider error responses are
failures too, not successful routes. Configured fallback router models may be
tried, but missing routes are never silently replaced with `quick`/`defaultType`.

With `routing.enabled: false`, every spawn task must supply a valid explicit
type. `defaultType` remains a preference for genuinely ambiguous LLM choices
and a legacy config-resolver default, not a spawn error fallback. Existing
callers relying on an implicit default must now choose a type explicitly.

### Project-local agents (`.pi/agents/*.md`)

A project can ship sub-agent roles as individual Markdown files in
`<project>/.pi/agents/`. The first such directory found walking up from the
session cwd is used; each top-level `*.md` file becomes a `subagentType` named
after the file. Parent and router see the short `description`; only the child
receives the Markdown body. A project's ordered `models` are filtered through
the same active preset pool as built-in agents.

```markdown
---
description: Use for reviewing this repo's diff — knows the house rules.
icon: eye
models:
  - zai/glm-5-turbo
  - openai-codex/gpt-5.6-luna
thinking: low
tools: read, grep
retry:
  maxRetries: 1
  backoffMs: 2000
---

You are this project's staff reviewer. Apply the repo rules from
AGENTS.md before approving anything; cite file paths first.
```

- Frontmatter keys: `name` (must match the filename), `description`, `icon`, `models`, `thinking`, `tools`, `extraArgs`, `promptAppend`, `promptOverride`, `retry`, `maxResultBytes`, `timeoutMs`. Legacy `model`, `fallbackModels`, and `modelByParent` still load. Unknown keys are rejected with an error naming the file.
- Array fields accept block lists (`- item`), inline arrays (`[a, b]`), or comma-separated strings (`tools: read, grep, bash`). The frontmatter YAML subset is intentionally small: scalars, quoted strings, numbers, comments, lists, and nested maps for `modelByParent`/`retry`. Tabs, block scalars (`|`/`>`), anchors/aliases, and flow maps are hard errors naming file and line.
- The markdown body becomes `promptAppend`: it is appended after the standard generated prompt (parent objective + task + output format), so the agent still receives its task in the usual structure. Use frontmatter `promptOverride` for full prompt replacement.
- Precedence: bundled Markdown defines the built-ins, then the nearest project `.pi/agents/*.md` file overrides the same-named built-in field-by-field. Markdown is the only source of agent role/profile definitions. The removed `asyncSubagents` section and old standalone config-path variables are not read.
- Files without frontmatter are skipped (a `README.md` there is fine). Definition loading is uncached: edits apply on the next config read/spawn without a restart, and the effective system-prompt catalog is rebuilt at parent-agent start.
- Bundled roles use the same format internally under `src/async-subagents/agents/*.md`; built-in and project-local profiles therefore share one parser and normalization path instead of maintaining a second role-description schema in TypeScript.
- `icon` names an agent glyph for UIs that render sub-agent widgets (pix TUI panel, Pix Desktop subagents panel): `agent` (neutral default), `search`, `code`, `flask`, `globe`, `sparkles`, `brain`, `wrench`, `terminal`, `bug`, `book`, `eye`, `zap`, `rocket`. The value is passed through opaquely; unknown names render as the neutral agent icon, and status stays color-coded next to it.

### Real UI QA (browser, TUI, and desktop GUI)

The built-in `ui-qa` role runs on `zai/glm-5.3-flash`, with
`openai-codex/gpt-5.6-luna` as its fallback. Its Markdown body
(`src/async-subagents/agents/ui-qa.md`) is a deliberately thin common contract:
test the real user-facing target, classify only the top-level backend, route
through on-demand guides, run through the unified runner, and keep deterministic
assertions as the oracle with bounded execution, owned cleanup, and private
evidence. Provider/presentation/platform details are intentionally absent from
the initial role body. The normal profile loader appends that body to the QA
child's task prompt; the parent and LLM router receive only the short
`description`. There is no additional QA skill to discover or read.

Backend specifics use progressive disclosure through bundled guides under
`src/async-subagents/agents/ui-qa/guides/`. `browser.md`, `tui.md`, and
`desktop.md` are compact routers; backend-scoped detail topics cover the actual
browser provider, TUI presentation, desktop platform driver, and explicit
browser-auth workflow. The child loads the matching base guide, then only the
routed detail topic read-only via the runner:

```sh
node "$PI_UI_QA_RUNNER" guide --backend browser   # or: tui | desktop
node "$PI_UI_QA_RUNNER" guide --backend browser --topic playwright
# other examples: chrome-devtools, tui/native-terminal, desktop/windows-uia
```

The command resolves the guide from a backend-scoped fixed allowlist bundled
with the suite (never a model-composed path), rejects unknown/cross-backend
topics/options and extra arguments, bounds guide size, and prints only the
requested document. `probe` returns authoritative
`selection.guide = {backend, topic}`; the child reconciles that route before
`run`.

`ui-qa` authors one private declarative flow and invokes the capability-first
runner supplied in `PI_UI_QA_RUNNER`. The runner selects exactly one backend
from the target descriptor and reports candidate capabilities plus its selection
rationale. Browser routing selects the trusted Playwright backend or the
capability-probed Chrome DevTools provider. TUI routing selects PTY semantics or
the native-terminal mirror. Desktop routing selects the capability-probed macOS
Accessibility, Windows UI Automation, or Linux AT-SPI implementation. Evidence
capabilities remain platform-specific and never replace deterministic
assertions. Unsupported platforms or missing required control permissions
return `BLOCKED`. QA does not
install GUI automation dependencies, change OS privacy/accessibility settings,
disable sandboxing, or operate unrelated user windows. Static or mock checks do
not substitute for the requested UI.

Unified flows and native/TUI evidence—including automatic recordings—stay in the owning agent's private
`.pi/subagents/<run>/<agent-id>/ui-qa/` workspace. Pass/fail requires a
product-visible deterministic oracle such as terminal content/state,
accessibility/app-driver state, window/dialog state, or visible control values;
screenshots are supporting evidence, not the sole oracle. Cleanup is scoped to
the PTY/session/app process created by the QA run.

#### Browser backend and project auth

The capability-first runner and all backend resources live under
`src/async-subagents/agents/ui-qa/`; the trusted browser runner, vendor code,
license, and examples are grouped under its `browser/` directory. The launcher
supplies absolute paths in `PI_UI_QA_RUNNER` and `PI_BROWSER_QA_RUNNER`. Normal
probe/run uses the former;
the latter is invoked directly only for browser auth profile discovery and
form-auth scaffolding. These non-secret paths are set only for QA children.
Every async sub-agent launches with `--no-skills`, and skill flags in
`extraArgs` are stripped rather than forwarded. Agent roles are self-contained;
there is no profile field for injecting skills and no built-in QA `--skill`.

Model/thinking/tool-only overrides should use a project `ui-qa.md` and inherit
the bundled Markdown workflow. A legacy project `browser-qa.md` is migrated to
the canonical `ui-qa` profile when no `ui-qa.md` override exists. A project
Markdown body replaces the inherited
`promptAppend` under the usual field-level merge rules; custom QA instructions must preserve the runner-only,
credential, target, and evidence contracts. Runner-enforced isolation and
credential handling remain in code, not in the prompt.

Public browser QA does not require an auth profile or `.pi/qa_auth.jsonc`: run it
with an explicit base URL, whose exact origin becomes the fail-closed allowlist.
The runner neither creates nor requests a credential file for that path.

For targets that actually require login, keep named dev/staging auth profiles in
project `.pi/qa_auth.jsonc` (there is no `/qa-auth` command). The private runner
supports `form`, `cookie`, `localStorage`,
`sessionStorage`, `bearer`, and existing Playwright `storageState` auth. Every
profile must declare exact `allowedOrigins`; select the profile id explicitly only
for authenticated QA. On POSIX, keep the config at mode `0600`. During a run
those origins also form the fail-closed HTTP(S)/WebSocket allowlist and service
workers are blocked. Example:

```jsonc
{
  "profiles": {
    "staging-admin": {
      "description": "Staging administrator",
      "traits": ["role:admin", "plan:enterprise"],
      "baseUrl": "https://staging.example.test",
      "allowedOrigins": ["https://staging.example.test"],
      "auth": {
        "type": "form",
        "loginUrl": "https://staging.example.test/login",
        "fields": [
          { "selector": "input[name=email]", "value": "admin@example.test" },
          { "selector": "input[name=password]", "value": "replace-me" }
        ],
        "submitSelector": "button[type=submit]",
        "success": { "selector": "[data-testid=user-menu]" }
      }
    }
  }
}
```

Do not place credential values in prompts, QA flows, shell arguments, or reports.
The helper reads JSONC internally and emits only redacted statuses. For form auth,
video recording begins on the login page and captures the field-filling and submit
sequence; password inputs remain browser-masked, but the private video may show
other visible login identifiers and must be treated as sensitive evidence. Tracing
starts only after login succeeds and is sanitized before retention. The launcher
provides each UI QA process with its general `ui-qa/` workspace plus the browser
backend's `.pi/subagents/<run>/<agent-id>/browser-qa/` workspace. Unified flows
stay under `ui-qa/flows/`; browser adapter flows, screenshots, video, sanitized
traces, and result manifests stay under the backend workspace, so normal
session shutdown or `subagents cleanup` removes them with the run directory.
The runner validates the owning agent metadata and refuses flows outside that
workspace; reusing an agent id clears stale browser QA files first. Trace archives
have network records and non-image resources removed, then known
configured/runtime credential values are redacted and verified before retention.
Listing profiles when the auth file is absent returns an empty list without
creating a template. Only an explicit authenticated request may create the
private template. Missing, rejected, or expired selected auth returns
`QA_AUTH_UPDATE_REQUIRED`, naming only the profile/file/reason needed for the
parent to ask the user for an update and rerun. See
`src/async-subagents/agents/ui-qa/browser/examples/qa-auth.example.jsonc`
for complete profile shapes and `examples/qa-flow.example.jsonc` beside it for
the declarative, non-executable QA action/assertion format.

Browser QA videos automatically visualize pointer interactions. Clicks and
double-clicks show a transient cursor and pulse. Native drag/drop is replayed
for 450 ms with a large orange cursor, progressively drawn high-contrast path,
and green drop marker. The isolated, pointer-transparent layer is installed for
the whole browser context, including same-origin frames, declared popups, and
form-auth submission, and clears during the normal post-action stable interval
so screenshots and assertions remain state-focused.

Async-subagents also injects a lightweight oh-my-openagent-style system-prompt strategy by model: non-GPT parents get `parallel-first`, an orchestration-first hint that favors ultrawork/subagents for broad work, while GPT-like parents get `deep-work`, a direct deep-worker hint that uses subagents only when clearly useful. Explicit custom system prompts (`--system-prompt`, `SYSTEM.md`, custom templates) are respected and skip this injection by default. Disable it with `PI_AGENT_STRATEGY=off`; force a strategy with `PI_AGENT_STRATEGY=parallel-first` or `PI_AGENT_STRATEGY=deep-work`; set `PI_AGENT_STRATEGY_WITH_CUSTOM_PROMPT=1` to append it even when a custom prompt is present.

For blind-model screenshot/image inspection, use the main-session `coding-discipline` lookup tool; the bundled default uses vision-capable `zai/glm-5.3-flash`. `lookupFallbackModels` is the ordered fallback array for `lookupModel` and defaults to `[]`; lookup tries the configured chain in order. Async-subagents still supports `imagePaths` on tasks when a broader delegated track genuinely needs images, but it no longer ships a dedicated `vision` role. Provider image metadata is supplemented by an internal compatibility list for known blind models so the parent does not falsely claim visual access; this is runtime capability policy, not agent configuration.

When `subagentType` is omitted, the lightweight role router classifies the task
using the descriptions. Explicit types bypass it. Unknown types or failed
routing reject the batch, never substitute `defaultType`. Choosing a worker
model from its candidate list does not involve an LLM call.

### Presets are available-model pools

Each agent declares an ordered `models` list in Markdown. A preset declares
which model references may be used, not another role/model/thinking matrix.
Selection preserves agent order, intersects it with `preset.models`, checks
runtime registration/auth availability, and takes the first usable candidate.
Pool order does not change preference and pool-only models are never appended.
Without a preset, the full agent list is eligible. Candidate order expresses
the configured budget preference; runtime does not infer current API prices.

Image-bearing tasks and `ui-qa` require confirmed image support; configured
blind-model masks override runtime image metadata. Remaining eligible models
form the quota fallback chain, so neither quota history nor image fallback can
escape the pool. Antigravity account rotation still happens before provider
fallback. No match, no usable model, or an explicitly empty list rejects the
whole batch before run directories or child processes are created. A new custom
agent must supply candidates instead of silently inheriting the parent model.

Oracle uses its separate strong-model list and prefers another provider when
available, but also respects the pool. A same-provider choice is allowed when
the pool offers no alternative; cross-provider independence is not guaranteed.
Explicit task/CLI model overrides and `FORCE_CURRENT_MODEL` remain deliberate
escape hatches and disable automatic model fallback for that task. They do not
bypass the image-capability check.

Bundled pools live in `src/async-subagents/agents/presets.jsonc`. Add or override
project pools in the nearest `.pi/agents/presets.jsonc`. Select a saved pool
with `/subagent-preset`; use `AGENTS_PRESET=<name>` or
`/subagent-preset session <name>` for a process-only override and
`/subagent-preset session-clear` to remove it. The saved selection lives in
`~/.pi/agent/subagent-preset-selection.json`. `/subagent-preset path` shows the
bundled and project preset paths. The shipped pools are `cheap` (GLM), `gpt`,
and `deep` (the retained legacy name for the mixed pool, not worker escalation).
Agent descriptions, instructions, model order, thinking, tools, retry, and
timeouts remain in agent Markdown; selecting a pool never changes them.

Example project `.pi/agents/presets.jsonc`:

```jsonc
{
  "cheap": {
    "description": "GLM workers with a strong oracle candidate.",
    "models": ["zai/glm-5-turbo", "zai/glm-5.3-flash", "zai/glm-5.3"]
  },
  "project-gpt": {
    "description": "GPT models approved for this project.",
    "models": ["openai-codex/gpt-5.6-luna", "openai-codex/gpt-5.6-terra"]
  }
}
```

### Legacy configuration compatibility

Old built-in role names are no longer implicit aliases. `quick`, `scan`,
`review`, `deep`, `docs`, `frontend`, and `tests` are valid only when explicitly
defined as ordinary project `.pi/agents/<name>.md` files.

Within an agent Markdown file, legacy `model` plus `fallbackModels` and
`modelByParent` remain readable. `models` is a complete replacement list: it
clears inherited legacy model/fallback/parent routing. New built-ins use ordered
`models` lists and contain no parent-tier escalation maps.

The removed `asyncSubagents` section is not part of the public schema or generated
user config and is no longer read at runtime. Existing files can remain on disk
without being rewritten, but they have no effect: migrate role definitions to
`.pi/agents/*.md` and custom model pools to `.pi/agents/presets.jsonc`. Runtime
retry structures and the separate role router continue to use the term
`fallbackModels` for actual fallback-only lists, not agent candidates.

Sub-agents run with `--no-session` by default to avoid writing duplicate Pi session JSONL files for fire-and-forget background work. Set `ASYNC_SUBAGENTS_ENABLE_SESSIONS=1` to restore persisted per-agent sessions under each agent's `sessions/` directory; this also registers the session-navigation slash commands (`/sub-open`, `/sub-back`, `/sub-where`) needed for switching and deeper post-mortem navigation.

Sub-agent runs are stored in the current project's `.pi/subagents/` directory while the main session is alive. Each spawn updates `.pi/subagents/registry.json` with the latest run and `agentId -> runDir` mappings. Because of that, `subagents({ action: "status" })`, `wait`, and `stop` can omit `runDir` to target the latest run, and `subagents({ action: "result", agentId: "..." })` can resolve the run from the registry even if the exact `runDir` was lost during compaction. Result reads always return a summary-first response with artifact paths; raw `result.md` and `stderr.log` are not inlined, which avoids IPC/socket buffer overflows. Include `runDir` when you need an older or non-latest run, and use `cleanup` with `delete=true` to remove collected old runs before the session ends. On normal main-session shutdown, Pi stops sub-agents and removes the project-local run files/registry to avoid leaving `.pi/subagents/` clutter behind; reload and fork shutdowns preserve them so in-process recovery still works.

Runtime logs are minimized by default: successful agents do not keep `events.jsonl`, and `stderr.log` is discarded unless the agent fails. Set `ASYNC_SUBAGENTS_DEBUG_LOGS=1` / `PI_SUBAGENTS_DEBUG_LOGS=1` to keep diagnostic logs for successful agents too; debug event logs store a compact RPC event summary instead of the full streaming transcript. Defaults are 0 bytes for `events.jsonl` without debug, 32 MiB for debug `events.jsonl`, 8 MiB for retained `stderr.log`, and 8 MiB for a single RPC JSON line; override with `ASYNC_SUBAGENTS_MAX_EVENTS_BYTES` / `PI_SUBAGENTS_MAX_EVENTS_BYTES`, `ASYNC_SUBAGENTS_MAX_STDERR_BYTES` / `PI_SUBAGENTS_MAX_STDERR_BYTES`, and `ASYNC_SUBAGENTS_MAX_RPC_LINE_CHARS` / `PI_SUBAGENTS_MAX_RPC_LINE_CHARS`.

Runtime concurrency defaults to 5 agents per project and the global result summary bound defaults to 100000 bytes. These are internal engine defaults rather than normal user configuration. Per-agent `retry`, `maxResultBytes`, and `timeoutMs` belong in agent Markdown; spawn calls and individual task objects can pass `timeoutSeconds` to shorten the watchdog for synthetic tests or bounded probes. Stop requests mark running, queued planned, and retry-pending agents as `stopped` so queued work is not launched later. Completed agents write `result.json` with status/duration/model/retry metadata plus best-effort `summary`, `findings`, `files`, `risks`, `nextActions`, and `confidence` fields for parent-agent chaining.

## OpenCode credential import

`/opencode-import` migrates only credential formats with an explicit Pi mapping: OpenAI OAuth to `openai-codex`, OpenAI API keys to `openai`, GitHub Copilot OAuth, Z.ai aliases, and one matching OpenCode Antigravity account. It does not migrate OpenCode model definitions, defaults, MCP servers, plugins, instructions, or tool settings.

The default source is `OPENCODE_AUTH_CONTENT`, `OPENCODE_DATA_DIR/auth.json`, or the XDG/default OpenCode data directory. The destination is the active Pi agent directory, including `PI_CODING_AGENT_DIR`. Existing Pi credentials are preserved; pass `--force` only to replace supported entries intentionally. Successful writes are atomic, use restrictive file permissions, and trigger a runtime reload. The command also accepts `--path`, `--auth-path`, `--antigravity-path`, `--skip-auth-json`, and `--skip-antigravity` for controlled migrations.

## Web search

`src/web-search` registers two Ollama-first tools and the `/web-credentials` setup command:

- `web_search` posts `{ query, max_results }` and returns formatted title/URL/snippet results plus structured `details.results`.
- `web_fetch` posts `{ url }` and returns extracted page text plus title/link metadata.
- `/web-credentials` can set, inspect, or clear stored Ollama and Tavily API keys. Notifications and status output never display key values.

Without an Ollama API key, both tools default to `http://localhost:11434/api/experimental/...`; set `OLLAMA_HOST` to point at another Ollama instance. With an Ollama API key and no explicit `OLLAMA_HOST`, they use the official `https://ollama.com/api/web_search` and `/api/web_fetch` endpoints with bearer authentication. Requests time out after 30 seconds by default. Override globally with `PI_WEB_SEARCH_TIMEOUT_MS` or per call with `timeout_ms` (maximum 120000 ms). Tool results include `host`, `timeoutMs`, and truncation metadata in `details`.

Configure a Tavily key to enable automatic fallback for both tools. Ollama remains the primary provider; if any Ollama request or response fails, `web_search` retries through `https://api.tavily.com/search` and `web_fetch` retries through `https://api.tavily.com/extract`. The Tavily key is sent only in Tavily's bearer authorization header and is never accepted as a tool parameter or included in result details. Fallback results set `details.provider` to `tavily` and include the primary Ollama error under `details.fallbackFrom`; ordinary results set `details.provider` to `ollama`.

When Context Gateway is in `enforce` mode, an over-budget `web_search` or
`web_fetch` result with complete structured producer details is delivered to the
next model as a bounded `web-recoverable-compact` view. The original structured
details stay in the raw session tool result (they are not serialized into the
provider request) and can be found by `toolCallId` with `session_search`, then
read or paged with `session_read_section`. Unsupported web result shapes remain
passthrough instead of being irreversibly sliced.

The recommended interactive setup is:

```text
/web-credentials
```

Choose Ollama or Tavily, paste the key, and it becomes active for later calls without a reload. Keys are stored in `~/.config/pi/pi-tools-suite-credentials.json` with mode `0600`. The same command can show configuration sources or clear stored keys without displaying them.

The command displays the official key pages before opening its menu:

- Ollama: <https://ollama.com/settings/keys>
- Tavily: <https://app.tavily.com/home>

Environment variables remain supported and take precedence over stored keys:

```bash
export OLLAMA_API_KEY="..."
export TAVILY_API_KEY="tvly-..."
```

The timeout applies independently to each provider attempt, so a failed Ollama request followed by Tavily can take up to roughly twice the configured timeout. Tavily Search fallback uses basic search and clamps `max_results` to Tavily's documented maximum of 20. Tavily Extract does not return page title/link metadata, so fallback uses the requested URL as the title and reports no links.

Troubleshooting:

| Symptom | Fix |
| --- | --- |
| `Could not connect to Ollama` | Start Ollama and check `OLLAMA_HOST`. |
| `Unauthorized by Ollama` | Run `ollama signin` for local Ollama, or update the Ollama key through `/web-credentials` / `OLLAMA_API_KEY`. |
| `endpoint is not available` | Update Ollama and make sure the experimental web search/fetch feature is enabled for that install. |
| `timed out after ...` | Increase per-call `timeout_ms` or `PI_WEB_SEARCH_TIMEOUT_MS` if the local web endpoint is slow. |
| `invalid JSON` / `unexpected response` | Check the Ollama version and the raw endpoint behavior; the tool reports the bad response shape instead of failing with a generic parser error. |
| `Tavily ... rejected TAVILY_API_KEY` | Update the Tavily key through `/web-credentials` or `TAVILY_API_KEY`. |
| `Tavily ... limit was exceeded` | Check Tavily usage/plan limits in the Tavily dashboard. |

Do not send secrets, tokens, private repository text, or credential-bearing URLs through these tools; Ollama may query external web services to satisfy the request.

## Layout

```text
pi-tools-suite/
  index.ts
  package.json
  src/
    index.ts
    coding-discipline/
    ast-grep/
    async-subagents/
    lsp/
    comment-checker/
    session-name/
    session-recovery/
    repo-discovery/
    antigravity-auth/
    opencode-import/
    todo/
    model-tools/
    usage/
    web-search/
    context-gateway/
    truncation-metadata-normalizer/
    dcp/
    prompt-commands/
    resource-registry/
    credential-firewall/
    codex-reasoning-fix/
  docs/
  licenses/
  scripts/
  test/
```

## Checks

```bash
npm run smoke
npm test
npm run typecheck:async-subagents

# Optional longer/e2e checks
npm run test:async-subagents-e2e
npm run test:async-subagents-selection-e2e
npm run test:e2e
```

### Prompt evaluations

Prompt evaluations are opt-in because they call a real model. They cover model-facing behavior that deterministic tests cannot prove: tool selection for `todo` and `compress`, async-subagent delegation/lifecycle boundaries, default internal role routing, ultrawork classification, and DCP summary retention. They are intentionally excluded from `npm test`.

```bash
# Full prompt-eval suite
npm run test:prompt-evals

# Focused suites
npm run test:prompt-evals:tool-selection
npm run test:prompt-evals:async
npm run test:prompt-evals:dcp
```

The default live model is `zai/glm-5-turbo`. Override it for the whole suite with `PI_TOOLS_SUITE_E2E_MODEL=provider/model`, or use the existing component variables such as `TOOL_SELECTION_E2E_MODEL`, `ASYNC_SUBAGENTS_MODEL`, `ASYNC_SUBAGENTS_ROUTING_E2E_MODEL`, and `DCP_SUMMARY_E2E_MODEL`. The normal deterministic coverage remains `npm test`; run prompt evals after changing tool descriptions, routing/classifier prompts, DCP summary prompts, or the default evaluation model.

### Unified eval harness

`test/evals/` adds a shared deterministic + live-model eval layer. The coverage
gate requires every registered extension and model-facing tool to have a
deterministic contract. The initial live corpus contains 20 cases across tool
selection, coding quality, orchestration/escalation, and negative overuse
controls. Coding-quality fixtures use executable behavioral checks rather than
an LLM judge, while reports compare parent/worker tokens, provider-reported cost,
tool calls, changed files, and elapsed time.

```bash
# Deterministic coverage/contract gate only
npm run test:evals:contracts

# Live matrix as Bun tests. Models are comma/semicolon separated.
PI_TOOLS_SUITE_EVAL_MODELS='zai/glm-5.3,openai-codex/gpt-5.6-luna,openai-codex/gpt-5.6-terra,openai-codex/gpt-5.6-sol' \
  npm run test:evals:live

# Produce JSON + Markdown comparison artifacts.
PI_TOOLS_SUITE_EVAL_MODELS='zai/glm-5.3,openai-codex/gpt-5.6-terra,openai-codex/gpt-5.6-sol' \
  npm run evals:report

# Focus the report runner when iterating
PI_TOOLS_SUITE_EVAL_MODELS='zai/glm-5.3' \
PI_TOOLS_SUITE_EVAL_CATEGORIES='coding-quality,negative' \
  npm run evals:report
```

Live evals are opt-in. The deterministic coverage registry is part of normal
tests, so adding an extension or tool without eval coverage fails the gate. See
[`docs/evals.md`](docs/evals.md) for the architecture, complete 20-case catalog,
fixtures, assertions, metrics, model matrix, report format, environment
variables, CI recommendations, and the procedure for adding new evals.

Supporting docs and historical standalone README content are kept in `docs/`; third-party license texts are kept in `licenses/`.

## SDK pin

This suite runs inside the Pi host process, so its `@earendil-works/*`
peerDependencies (`pi-ai`, `pi-coding-agent`, `pi-tui`) must match the host Pi
SDK version exactly. Otherwise npm can resolve a stale copy in this package's
own `node_modules` and cause a double-load (e.g. `0.75.4` here vs `0.79.4` in
the host).

The host repo keeps these aligned: `npm run sync:sdk-pin` rewrites these
peerDeps to the host version, and `npm run sync:sdk-pin:check` reports drift
(non-zero exit). When you bump the Pi SDK in the host `package.json`, the host
runs `sync:sdk-pin` and then you reinstall here:

```bash
npm install --ignore-scripts
```

The suite deliberately does not bump its own `version` field for SDK changes;
its peerDeps carry the version.

## Third-party notices

Parts of this extension suite are based on or adapted from code by other vendors and projects. The corresponding license texts and notices are included in `licenses/`.
