# Pi (pi.dev) Run Mode: Implementation Plan Tracking issue: [#206 "Plans to support pi.dev?"](https://github.com/Ark0N/Codeman/issues/206) Status: **IMPLEMENTED 2026-08-13** (see `docs/pi-integration.md` for the user-facing guide). Everything below is the design record; the open questions were resolved empirically against pi 0.84.1 and the answers are recorded inline as **RESULT** notes. Originally reworked 2026-08-06; **rechecked 2026-08-13 against master @ `f39beb3` (v1.17.0)**, and every line anchor below was re-verified at that commit (the 1.11.2-era anchors drifted heavily: six releases landed in between, including the settings-surface overhaul and the codex predictive-echo work, both of which added new pi touchpoints, §2.10 and the Brain picker in Phase 3). Upstream facts verified against `@earendil-works/pi-coding-agent` **v0.84.1** (npm latest, published 2026-08-07) and the [`earendil-works/pi`](https://github.com/earendil-works/pi) repo (cite that name: upstream docs still contain stale `pi-mono` links from a repo rename). Line numbers are anchors for orientation, not contracts; they drift. --- ## 1. What Pi is [Pi](https://pi.dev) (MIT) is a minimal, extensible coding-agent harness. Facts below are verified against the upstream docs in `packages/coding-agent/docs/`. | Property | Value | | ---------------- | -------------------------------------------------------------------------------------------------- | | Binary | `pi` (`bin: { pi: 'dist/cli.js' }`) | | npm package | `@earendil-works/pi-coding-agent`, latest **0.84.1** (2026-08-07; 0.84.0 was 2026-08-06); `legacy-node20` dist-tag at 0.74.2 | | Install | `npm install -g --ignore-scripts @earendil-works/pi-coding-agent`, or `curl -fsSL https://pi.dev/install.sh \| sh` (the curl installer also goes through global npm, so both uninstall via npm) | | Config dir | `~/.pi/agent` (override: `PI_CODING_AGENT_DIR`). Holds `auth.json`, `trust.json`, `settings.json`, `models.json` (user-defined providers), `models-store.json` (cached catalogs), `keybindings.json`, `extensions/`, `skills/`, `prompts/`, `themes/`, `AGENTS.md`, `SYSTEM.md`, and the package trees `npm/` + `git/` | | Sessions | `~/.pi/agent/sessions/----/_.jsonl`, tree-structured (`id`/`parentId`), format v3. Overrides: `PI_CODING_AGENT_SESSION_DIR`, `--session-dir` | | Credentials | `~/.pi/agent/auth.json` (OAuth subscriptions + API keys, auto-refresh), plus ~34 provider env vars with **no common prefix**. 0.84.1 adds `pi auth check` (auth preflight with optional credential output) | | TUI | Default: **main screen with terminal-owned scrollback**. Since **0.84.0** an experimental fullscreen mode exists, selectable via `--tui-mode fullscreen` **or at runtime through `/settings`**; the default remains the main-screen mode | | Providers | 15+ (Anthropic, OpenAI, Google, Azure, Bedrock, Mistral, Groq, xAI, OpenRouter, Copilot, Baseten since 0.84.0, ...). OAuth subscription login via `/login` for six: ChatGPT Plus/Pro, Claude Pro/Max, GitHub Copilot, xAI, OpenRouter, Radius | | Permission model | **No permission prompts at all.** No built-in sandbox, no MCP (none planned), no sub-agents, no plan mode, no to-dos, no background bash. Tools run with the user's own permissions | | Trust model | "Project trust" gates **loading** of project-local `.pi/` config/extensions/skills and **installing missing project packages**, not tool execution. Triggered only when the cwd (or an ancestor) contains `.pi/settings.json`, `.pi/extensions\|skills\|prompts\|themes`, `.pi/SYSTEM.md`/`.pi/APPEND_SYSTEM.md`, or `.agents/skills`; a bare `.pi/` directory does NOT prompt. Global `defaultProjectTrust`: `ask` (default) / `always` / `never` | Three consequences shape the whole integration: 1. **There is no `--dangerously-skip-permissions` analog and none is needed.** Pi never prompts for tool approval. The Claude/Codex/Gemini/Antigravity pattern of "send the bypass flag so the session is not stuck on a modal" does not apply. Codeman must not invent a flag here. 2. **The one privileged knob is `--approve` / `-a`** (trust project-local files for this run), which makes pi load and execute project `.pi/extensions` TypeScript **and run an npm install of missing project packages**. That is the field the multi-user clamp has to cover. Its explicit inverse `-na` / `--no-approve` exists, which lets the clamp force-deny rather than merely omit (§3, §5.2). 3. **Provider keys cannot ride the env allowlist.** Pi's provider key vars (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `DEEPSEEK_API_KEY`, `HF_TOKEN`, `BASETEN_API_KEY`, ...) share no prefix, so there is no way to admit them through `ALLOWED_ENV_PREFIXES` without widening the list for every mode (§2.4). --- ## 2. Design decisions ### 2.1 Mode identity `SessionMode` gains `'pi'`. Not a location overlay (unlike Docker/remote-SSH cases), not a web tab: a real sixth CLI backend with its own PTY, tmux session and respawn behaviour, exactly like `antigravity`. Append `pi` after `antigravity` in every enum/list to keep ordering consistent. | Surface | Value | | ---------------- | --------------------------------------------------------------------- | | `SessionMode` | `'pi'` | | Display label | `Pi` | | Tab badge | `pi` (two-letter lowercase, like `sh`/`oc`/`cx`/`gm`/`ag`) | | Run button label | `Run PI` (short-label ternary in `_applyRunMode`, pattern `Run AG`) | | Kill-menu label | `Kill Tmux & Pi` | | Identity color | **`#f472b6` (rose-400)**. Verified free: live computed values on the default skin are claude `#38b6f0`, opencode `#44b993`, codex `#2b8fd9`, gemini `#8ab4f8`, antigravity `#22d3ee`, shell `#98a2b1`, web `#38bdf8`; purple is codex's base hex and amber reads as the shell tab badge, so pink/rose (or orange `#fb923c`) are the only genuinely free hues. No `pi` CSS identifier collides anywhere (`mode-pi`, `.tab-mode.pi`, `.run-mode-dot.pi` all grep clean, re-checked at f39beb3) | | Env prefix | `PI_` | | Dependency id | `pi` | | Status endpoint | `GET /api/pi/status` | ### 2.2 `isExternalCliMode()` yes, `isAltScreenStripMode()` no Pi joins `isExternalCliMode()` (`session.ts:164-167`): its own TUI, its own output format, so the Ralph tracker, `BashToolParser`, token/CLI-info scraping and the `❯` readiness probe all stay off (gates at `session.ts:1100`, `:1701`, `:2000`, `:2103`), and readiness falls back to the output stabilization used by the other external CLIs. Pi stays **out** of `isAltScreenStripMode()` (`session.ts:197-199`, currently codex/claude/gemini; antigravity and opencode are deliberately excluded). Pi's default TUI renders into the main screen with terminal-owned scrollback, so there is nothing to strip. The fullscreen mode **shipped in 0.84.0 and is runtime-switchable via `/settings`**, so Codeman cannot assume a pi session stays main-screen for its lifetime; staying out of the strip list is exactly what makes that safe (the alt screen is load-bearing when the user flips to fullscreen, as it is for `opencode`). Putting pi IN the strip list would corrupt fullscreen sessions. Three mirrors must stay consistent (all unchanged for pi, i.e. pi appears in none of them): the replay-side strip in `session-routes.ts:2275`, the live-stream twin in `session.ts`, and the frontend `_sessionUsesServerMouseStrip()` in `terminal-ui.js` (usages `:3432`, `:3697`). ### 2.3 tmux required, no direct-PTY fallback, no per-mode configurator Same rule as the other external CLIs: `pi` mode throws if tmux is unavailable. Add a fourth block to the guard chain at `session.ts:1751-1768` (antigravity's is `:1765-1768`). **No `_configurePi()` is needed.** Opencode/codex/gemini each have a tmux-`setenv` configurator (`tmux-manager.ts:1709-1727`), but antigravity has none: it relies entirely on the generic `applyEnvOverrides()` (`tmux-manager.ts:1643`, `VALID_KEY = /^[A-Z_][A-Z0-9_]*$/`), which runs for every mode in both create (`:1880`) and respawn (`:2107`) and injects via socket-scoped `tmux setenv`, never the spawn command line. Pi follows the antigravity precedent: `PI_*` overrides flow through `applyEnvOverrides()` and nothing else. Pi joins the truecolor branches: `buildEnvExports()` (`tmux-manager.ts:1604-1609`, `export COLORTERM=truecolor` + `unset NO_COLOR` for codex/gemini/antigravity) and the attach-env condition at `session.ts:1400-1402` (`buildMuxAttachEnv(...)`, whose comment says it must mirror `buildEnvExports`). Add `|| mode === 'pi'` to both, or the tmux session and the attach client disagree about color depth. ### 2.4 Env prefix: `PI_` only Add `'PI_'` to `ALLOWED_ENV_PREFIXES` (`schemas.ts:125`) and to the prose error message at `:163` (two edits: the message hardcodes the list, and since 1.12+ it also names the exact-key allowlist, currently `...ANTIGRAVITY_* keys and CLAUDE_CONFIG_DIR are allowed.`; there is now a separate `ALLOWED_ENV_KEYS` exact-key set alongside the prefix list, which pi does not need to touch). That covers every documented variable pi reads: `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`, `PI_PACKAGE_DIR`, `PI_OFFLINE`, `PI_SKIP_VERSION_CHECK`, `PI_TELEMETRY`, `PI_CACHE_RETENTION`, `PI_SHARE_VIEWER_URL`, `PI_HARDWARE_CURSOR`, `PI_EXPERIMENTAL` (whose meaning 0.84.0 extended to strict JSON-schema tool sampling). (Pi also *sets* `PI_CODING_AGENT=true` and `AI_AGENT=pi` in child processes; those are output markers, not inputs, and need nothing from us.) **Deliberately not added:** `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, `GROQ_API_KEY`, `MISTRAL_API_KEY` and the other ~28 provider keys. `ALLOWED_ENV_PREFIXES` is a single global list applied by one Zod refine with no mode context (`safeEnvOverridesSchema`, `schemas.ts:153-165`), so allowlisting bare provider keys for pi would widen the allowlist for **every** mode at once, violating the multi-CLI prefix discipline in CLAUDE.md. Users authenticate pi through `/login` (stored in `~/.pi/agent/auth.json`, auto-refreshed) or by exporting the key in the Codeman server process's own environment. Making the allowlist mode-aware is the clean fix, listed as a follow-up in §9. Do not smuggle it into this change. ### 2.5 Docker credential policy: seed files, not the whole dir `CRED_STORES` (`docker-hosts.ts:597-605`; file unchanged since the 2026-08-06 verification) gets a `.pi/agent` entry. Nested `rel` paths already work (`.config/gcloud` maps to seed name `.config-gcloud` via the `replace(/\//g, '-')` at `:620`). Unlike antigravity, which needed **no** entry (`agy` nests all state under `~/.gemini/antigravity-cli/`, already covered by the `.gemini` policy, per the comment at `:599-602`), pi has its own top-level dir and needs its own entry. Use `seedFiles`, **not** `seedWhole`: ```ts { rel: '.pi/agent', seedFiles: ['auth.json', 'settings.json', 'trust.json', 'models.json', 'models-store.json'] }, ``` Rationale: `~/.pi/agent` also contains `sessions/`, `extensions/`, `skills/` and the installed package trees (`npm/`, `git/`), which on an active host is easily gigabytes; `seedWhole` would `cp -a` all of it into every container start. The five seeded files are what pi needs to authenticate and behave consistently: `models.json` is in the list because it holds user-defined custom providers, and omitting it would silently strip those inside containers. Seeding (RO mount then copy) also means the in-container pi never writes refreshed OAuth tokens back to the host, which is the whole point of the seeding policy, and bind mounts stay excluded from `docker commit` so exports remain secret-free. Trade-off to accept and document: in-container pi sessions are not visible host-side, so `pi -c` inside a Docker case only sees that container's own history. Codex shares `sessions/` RW precisely because Codeman reads it host-side for the response viewer; there is no such reader for pi yet (the response-viewer follow-up in §9 would justify flipping this). ### 2.6 The `pi` binary name is generic Unlike `agy`/`codex`/`gemini`, `pi` is a short, common name (Raspberry Pi tooling, personal scripts, `$PATH` accidents). The resolver must not blindly trust a hit. None of the existing external-CLI resolvers execute their binary (only `claude-cli-resolver.ts` does, via the cached `getClaudeCliVersion()`, skipped under vitest), so the sanity check is new ground: model it on `getClaudeCliVersion()`. Run `pi --version` once via `execFileSync`, cache the result module-level, skip under `VITEST`, and require output matching `/^\d+\.\d+\.\d+/`; on mismatch treat the binary as unavailable and log the rejected path. Surface `{ available, path, version }` from `GET /api/pi/status` so a misresolution is diagnosable from the UI (additive relative to the sibling endpoints' `{ available, path }`). The `dependency-registry` entry carries `versionArg: '--version'` for `codeman doctor`. ### 2.7 tmux extended keys (a real pi-specific footgun) Pi documents (`docs/tmux.md`, verified verbatim) that without ```tmux set -g extended-keys on set -g extended-keys-format csi-u ``` tmux collapses `Shift+Enter` and `Ctrl+Enter` into a plain `\r` (and `Alt+Enter` into `\x1b\r`), and pi's editor uses those for newline vs submit. `extended-keys-format` requires tmux 3.5+; tmux 3.2-3.4 works with `extended-keys on` alone (pi then falls back to xterm `modifyOtherKeys`). Codeman's own browser input path sends `\r` for submit, so basic use works unconfigured, but newline-in-editor is degraded both for a user typing in an attached terminal (`sc`) and potentially for the browser Shift+Enter path. Upstream recommends `~/.tmux.conf` and notes the setting may need a full `tmux kill-server` restart to take effect. **Codeman must NEVER run `kill-server` on its socket** (it would kill every live session, including `w1`/`w2`/`w3`). Action: attempt to set both options **server-scoped on Codeman's own socket only** (`tmux -L codeman set -s ...`, never `-g` on the user's default socket) at the point the tmux server is first started, verify with `tmux -L codeman show-options -s` and an empirical Shift+Enter test which scope actually takes for the installed tmux version, and fall back to a documented manual step in `docs/pi-integration.md` (a `~/.tmux.conf` snippet plus the kill-server caveat) if it cannot be applied safely to an already-running server. Upstream does not discuss socket- or server-scoped configuration at all, so this verification is original work, not a doc lookup. **RESULT (measured, tmux 3.4 + pi 0.84.1):** `tmux -L set -s extended-keys on` takes effect on an **already-running** server with **no `kill-server`** — pi's own startup warning (`Warning: tmux extended-keys is off…`, a convenient in-band probe) disappears for the next session started afterwards. `extended-keys-format` does **not exist on tmux 3.4** and errors with `invalid option: extended-keys-format`, so the two options must be issued independently rather than chained. Decision: Codeman does **not** set this itself — it is a server-wide tmux option affecting every session of every backend, so silently changing key encoding is not Codeman's call. It is documented as a user step in `docs/pi-integration.md` instead, carrying the measured facts. ### 2.8 The completeness trap: which mode tables fail loud vs silent Adding `'pi'` to the `SessionMode` union makes some omissions compile errors and leaves others silent. The plan calls this out so review can focus on the silent ones. **Loud (typecheck fails until edited):** `getModeLabel()` (`session.ts:168-183`, exhaustive switch with no default), `defaultDockerCommandForMode` and `defaultRemoteCommandForMode` (both typed `Record<...CommandMode, string>`), **but only after** `RemoteCommandMode` (`types/session.ts:48-51`) and `DockerCommandMode` (`:157-161`) are widened: both are `Extract` with every member spelled out, so forgetting the `Extract` lists keeps `tsc` green while docker/remote pi cases silently fall back to `exec bash -l` via the `|| commands.shell` on the lookup. Edit union + both `Extract` lists + both `Record` literals together. **Silent (compiles clean, mode just doesn't work):** - `appendResumeFlag()` (`tmux-manager.ts:1030-1042`) has a `default:` arm; a missing `case 'pi'` silently drops docker resume. - `buildSpawnCommand()` (`:770-825`) and `buildPathExport()` (`:1680-1707`) are if-chains with fallthrough returns; a missing branch spawns pi as a login shell / with no PATH augmentation. - `isExternalCliMode()` / `isAltScreenStripMode()` are boolean chains. - The `runMode` accessor's **setter whitelist** (`session-ui.js:2949-2960`) coerces any unknown mode to `'claude'`. Omitting `pi` there makes the mode **unselectable while every other edit appears to work**: this is the single most deceptive omission in the frontend. - `window.__codemanCliAvailable` (injected by `renderIndexHtml`, `server.ts:1375-1407`): the client treats a **missing key as available** (`isCliAvailable` in settings-ui.js), so forgetting the injection un-gates pi on boxes without the CLI instead of hiding it. ### 2.9 The Daylight skin cascade eats per-mode run-button colors A finding that changes the CSS work (verified empirically with computed styles on the live instance, re-confirmed at f39beb3): `styles.css:13681` opens a nested skin block, `html:not([data-skin="og"]) { ... }`, and the **default skin is `daylight-blue`, not `og`**, so the block is live for every default-skin user. Inside it, `.btn-toolbar.btn-run` is re-declared generically and per-mode only for claude/opencode/codex (codex at `:13787`). CSS nesting adds the wrapper's specificity (the nested rules resolve to (0,3,1) vs (0,3,0) for `.btn-toolbar.btn-run.mode-X`), so **gemini's and antigravity's toolbar gradients are dead on the default skin**: both render the generic claude gradient today, still unfixed as of f39beb3. The base-sheet rules (gemini/antigravity at `:4406`/`:4420`) only ever render on the `og` skin. Since 1.12+ styles.css itself documents this trap in comments (`:9214`, `:11091`), which confirms the mechanism. Consequences for pi: - The toolbar gradient needs **two** rules: one in the base sheet (`:4420` area, for `og`), and one **inside** the `13681` block next to codex's (`:13787` area), using the block's own idiom (or the color is invisible to the average user). - `mobile.css` phone-toolbar colors need `!important` on `background`/`border-color`/`color`, exactly as the CLAUDE.md gotcha prescribes. Antigravity's phone block (`mobile.css:895-910`, inside the `@media (max-width: 430px)` opened at `:338`) has no `!important` and is dead on the default skin; do not copy that mistake. - Three surfaces work from base rules alone (verified): run-mode **dots** (list at `:4506-4516`; the skin block overrides only claude/opencode/codex/shell dots, so a base-sheet `.run-mode-dot.pi` renders as authored), **tab badges**, and the **welcome button** (the skin block overrides only claude/opencode/tunnel welcome buttons). - Optional, separate cleanup (not this change): gemini/antigravity could get the same in-block treatment to resurrect their colors. ### 2.10 Local-echo policy: pi lands on the buffer overlay by default New since the first draft of this plan: the codex predictive-echo work (1.13+) introduced a per-session echo policy in `_updateLocalEchoState()` (terminal-ui.js, `_localEchoPolicy` set at `:2837`): `codex → 'predict'` (write-through predictive echo), `shell → 'off'`, **everything else → 'buffer'** (the `LocalEchoOverlay` that buffers typed text until Enter). Pi therefore gets the buffer overlay on touch devices with zero edits, via the fallthrough. That default is a real open question, not a freebie: the codex history (issues #218/#219/#220/#222) shows that a composer which re-renders per keystroke (live-filtering slash picker, server-side cursor movement, wrap-as-you-type) is starved by buffer-until-Enter, and pi's editor is exactly such a composer. Decision for v1: ship with the default `'buffer'` policy but make phone-profile typing an explicit E2E gate (§7 step 4); if pi's editor mis-renders under the overlay, the cheap fallback is forcing `'off'` for pi (one branch in `_updateLocalEchoState`), and teaching the predict path pi's composer row is a follow-up, not a v1 requirement. `test/local-echo-codex-gating.test.ts` pins the per-mode policy via `it.each(['claude', 'gemini', 'opencode'])` lists (`:193`, `:376`); add `'pi'` to those lists once the buffer decision is confirmed (or pin the `'off'` branch if that is the outcome). **RESULT (measured, pi 0.84.1, iPhone 14 Pro profile + a PTY-level A/B):** the buffer policy **holds**; codex's failure mode does **not** reproduce. Pi's slash picker re-filters on the **whole composer content**, not on per-keystroke deltas: a one-shot literal write of `/set` (what the overlay flush does) filters the picker to `settings` **identically** to sending `/ s e t` as five separate keystrokes, and the delayed `\r` then selects it and opens the settings menu. Prose prompts buffer correctly (`pendingText` right, nothing on the PTY before Enter), flush on Enter, and are accepted as a single prompt. `'pi'` was added to both `it.each` lists. The `'off'` fallback stays documented but unused. --- ## 3. Config surface: `PiConfig` to CLI flags ```ts /** Pi CLI session configuration */ export interface PiConfig { /** Model pattern or ID. Supports `provider/id` and a `:` suffix (e.g. `sonnet:high`). Passed via --model. */ model?: string; /** Provider name (anthropic, openai, google, ...). Passed via --provider. */ provider?: string; /** Reasoning level. Passed via --thinking. */ thinking?: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'; /** Continue the most recent session (-c). Per-cwd scoping is strongly implied upstream but not documented; treat as probable. */ continueSession?: boolean; /** Resume a specific session by ID or partial UUID (--session). Codeman deliberately accepts ids only, never paths. */ resumeSessionId?: string; /** * Tri-state project trust (repo-local `.pi/` settings/extensions/skills, plus installing * missing project packages): * true -> --approve (trust for this run; loads and EXECUTES repository TypeScript) * false -> --no-approve (force-deny; the trust prompt never appears) * absent -> pi's own defaultProjectTrust (ask). * Multi-user: MATERIALIZED to false for non-granted owners (§5.2). */ approveProjectTrust?: boolean; } ``` Flag mapping in `buildPiCommand()` (new, `tmux-manager.ts`, directly after `buildAntigravityCommand` at `:718-736`; every builder there regex-allowlists each user value and silently drops failures because the result lands in a `bash -c "..."` string): | Field | Flag | Validation | | --------------------- | ------------------------------- | --------------------------------------------------------------------------------- | | `approveProjectTrust` | `--approve` / `--no-approve` / nothing | tri-state boolean, clamped (§5.2) | | `model` | `--model ` | `/^[a-zA-Z0-9._\-/:]+$/` (`:` for `sonnet:high`, `/` for `openai/gpt-4o`) | | `provider` | `--provider ` | `/^[a-z0-9-]+$/` | | `thinking` | `--thinking ` | runtime allowlist of the 7 enum values (defense in depth beyond Zod) | | `resumeSessionId` | `--session ` | `/^[a-zA-Z0-9._-]+$/` (same shape as `RESUME_ID_SAFE`, `:1021`; excludes paths on purpose) | | `continueSession` | `-c` | boolean; **skipped when a valid `resumeSessionId` is present** (the two conflict) | **Not** wired in v1, with reasons: - `--api-key `: ⚠️ **never wire this.** It puts a provider secret on the spawn command line, which is exactly what the socket-scoped `tmux setenv` discipline exists to prevent (visible in `ps`, tmux server state, and logs). Listed here so nobody "helpfully" adds it later. - `--tui-mode` (released in 0.84.0): never passed by Codeman. The main-screen default is the friendly case for the browser terminal, and fullscreen remains the user's own runtime choice via `/settings` (§2.2 is designed for that). `--use-theme` (still unreleased) likewise. - `--name ` (`-n`): nice for `/resume` readability, but names contain spaces and would be the first user-controlled value needing real shell quoting in `buildSpawnCommand`. Defer. - `--no-session`: ephemeral mode fights respawn/resume. Defer. - `-p`/`--print`, `--mode json`, `--mode rpc`: non-interactive transports, a different product shape (§9). Note upstream already shipped a breaking change to JSON-mode `message_update` framing, so any future consumer must assemble deltas. - `--tools` / `--exclude-tools` / `--no-tools` / `--no-builtin-tools` (`-t`/`-xt`/`-nt`/`-nbt`): a genuinely useful "read-only session" affordance (0.84.0 also added a `defaultTools` setting), but it needs UI design. Follow-up. - `-r`/`--resume` (interactive picker), `--fork`, `-e`/`--extension`, `--skill`, `--system-prompt`, `--append-system-prompt`, `--export`, `--models`, `--list-models`: not session-manager concerns in v1. (`-e` matters later: §9's extension follow-up notes CLI extensions load before trust resolution.) --- ## 4. Implementation phases ### Phase 1: Backend core | File | Change | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `src/utils/pi-cli-resolver.ts` | **New**, mirror `antigravity-cli-resolver.ts` (65 lines: search-dir list, module-level cache with `''` negative sentinel, `which pi` first). Search dirs: `~/.local/bin`, `/usr/local/bin`, `~/.bun/bin`, `~/.npm-global/bin`, `~/bin`. Add the `pi --version` sanity probe from §2.6 (execFileSync, cached, vitest-skipped). Export `resolvePiDir()`, `isPiAvailable()`, `getPiCliVersion()` | | `src/utils/index.ts` | Re-export the three (resolver block `:30-36`) | | `src/types/session.ts` | `SessionMode` union `:46`; **both `Extract` lists**: `RemoteCommandMode` `:48-51`, `DockerCommandMode` `:157-161` (§2.8); new `PiConfig` after `AntigravityConfig` (`:325-333`); `SessionState.piConfig` after `:486`; `@fileoverview` mode list `:11` + config list `:17` | | `src/mux-interface.ts` | `piConfig?: PiConfig` on `CreateSessionOptions` (config block ends `:78`) and `RespawnPaneOptions` (ends `:109`) | | `src/session.ts` | `isExternalCliMode()` `:164-167` (+pi); `getModeLabel()` `:168-183` (+`'Pi'`); `_piConfig` field decl `:466-470`; ctor option `:556-563` + apply `:652-654`; `toState()` `:1227-1230`; `_buildRespawnPaneOptions()` `:1466-1469` (single source of truth shared by `startInteractive` and `reattachRemote`); `startInteractive()` createSessionOptions `:1680-1683`; COLORTERM attach-env condition `:1400-1402` (+pi); requires-tmux guard chain `:1751-1768` (new block: "Pi sessions require tmux for env override injection via setenv") | | `src/tmux-manager.ts` | `buildPiCommand()` after `:736` per §3; `buildSpawnCommand()` signature `:770-779` + dispatch branch after `:822-825`; `appendResumeFlag()` `:1030-1042` (`case 'pi': return \`${modeCommand} --session ${resumeId}\`;`); `buildEnvExports()` truecolor branches `:1604-1609` (+pi); `buildPathExport()` `:1680-1707` (+pi branch calling `resolvePiDir()`); missing-CLI error chain in `createSession` `:1788-1806` (+pi, install hint `npm install -g --ignore-scripts @earendil-works/pi-coding-agent`; note `respawnPane` deliberately has no such check); `piConfig` threading at the four sites `:1748`, `:1817`, `:2041`, `:2080`. **No `_configurePi`** (§2.3) | | `src/config/dependency-registry.ts` | New entry after antigravity's (`:101-108`; file unchanged since 2026-08-06): `{ id: 'pi', label: 'Pi CLI', category: 'core', required: false, usedBy: ['Pi sessions'], resolvers: [{ match: ALL, resolver: { kind: 'path', bins: ['pi'], versionArg: '--version' } }] }` | | `src/docker-hosts.ts` | `defaultDockerCommandForMode` `:138-149`: `pi: 'exec pi'`. `CRED_STORES` `:597-605`: the `.pi/agent` seedFiles entry per §2.5 (nested `rel` already handled at `:613-645`). File unchanged since 2026-08-06 | | `src/remote-hosts.ts` | `defaultRemoteCommandForMode` `:92-118`: `pi: remoteLoginShellCommand('pi')` (`remoteLoginShellCommand` at `:88-90`). Login-shell routing is mandatory (the #209/e803186 lesson: ssh remote-command exec sees only sshd's minimal PATH, and npm's global bin is usually only on PATH via rc files) | ### Phase 2: Web layer | File | Change | | ---------------------------------- | ----------------------------------------------------------------------------------------------- | | `src/web/schemas.ts` | `'PI_'` in `ALLOWED_ENV_PREFIXES` `:125` **and** the prose error message `:163` (which now also names `CLAUDE_CONFIG_DIR`; the `ALLOWED_ENV_KEYS` exact-key set needs no change); new `PiConfigSchema` after `AntigravityConfigSchema` (`:256-271`), mirroring §3's regexes, `.optional()`, not `.strict()`; `piConfig` on `CreateSessionSchema` (`:299` area) and `QuickStartSchema` (`:712` area); `'pi'` in all three mode enums (`:285`, `:708`, cron `agentType` `:1214`; they are byte-identical and there is no fourth); `pi` key in `RemoteCommandOverridesSchema` `:426-436` (it is `.strict()`, so an unknown key is a hard error today; one edit covers both remote `:501` and docker `:577` reuse) | | `src/web/routes/session-routes.ts` | Thread `piConfig` through create (`POST /api/sessions`): disk-strip exclusion chain `:705-712`, availability gate `:782-790` (+`isPiAvailable` with install-hint error), model resolution `:825-838` (`mode === 'pi' ? body.piConfig?.model : ...`), clamp call `:845`, Session ctor `:860` (`piConfig: mode === 'pi' ? gatedPiConfig : undefined`). Quick-start (`POST /api/quick-start`, handler `:2559`): remote-case config rejection `:2614-2621` and docker-case `:2645-2652` (+`piConfig`: per-CLI config does not cross ssh or the bind mount), hooks-scaffold exclusions `:2801`/`:2809`, availability gate `:2744-2752` (local-case branch only), env-strip chains `:2833`/`:2863`, model resolution `:2885`, clamp `:2897`, ctor `:2913`. **Extend `clampExternalCliBypassForOwner()`** (`:305-336`, doc comment above): fifth param + return field; pi joins the **materialize** branch per §5.2. Alt-screen replay-strip at `:2275` unchanged (pi not in it, §2.2) | | `src/web/routes/system-routes.ts` | `GET /api/pi/status` after the antigravity handler (`:418-426`; file unchanged since 2026-08-06), same shape plus `version` (§2.6); update the "CLI Integrations" prose comment `:377` | | `src/web/server.ts` | Restore path: `piConfig: muxSession.mode === 'pi' ? savedState?.piConfig : undefined` after `:2636`. **`renderIndexHtml` CLI-availability injection `:1375-1407`**: add `isPiAvailable` to the dynamic-import tuple (`:1382`) and a `pi` key to the injected object (`:1399`). Per §2.8 a missing key reads as *available*, so this is a correctness edit, not polish | ### Phase 3: Frontend The antigravity touchpoints are the template. Since the first draft, the settings-surface overhaul moved most anchors and added one **new touchpoint** (the clone-repo Brain picker below). `constants.js`, `api-client.js`, `ralph-wizard.js`, `cron-ui.js`, `webview-tabs.js` and `sw.js` still need **no** changes (re-verified zero mode coupling at f39beb3; cron-ui reads the `