SessionMode gains 'pi', a first-class backend alongside Claude Code, OpenCode, Codex, Gemini and Antigravity: its own PTY, tmux session, rose tab identity, welcome button, run-mode entry, cron agentType, Docker and remote-SSH command defaults, and clone-repo Brain option. Pi is a different shape of CLI from the other four, and three decisions follow from that: - It has NO permission prompts and no sandbox, so there is no --dangerously-skip-permissions analog and none was invented. The privilege-shaped knob is the tri-state approveProjectTrust, which makes pi load and EXECUTE repo-local .pi/extensions TypeScript and install missing project packages. clampExternalCliBypassForOwner() therefore puts pi in the MATERIALIZE branch: a non-granted multi-user owner gets --no-approve even when no config was sent, because pi's own default is a prompt the session user could answer themselves. That helper had zero test coverage; it now has coverage for all four CLIs. - Only the PI_ prefix joins the env allowlist. Pi's ~34 provider key vars share no prefix and ALLOWED_ENV_PREFIXES is one global list with no mode context, so admitting them would widen the allowlist for every mode at once. Auth goes through pi's /login or the server's own environment. --api-key is deliberately never wired: it would put a provider secret on the spawn command line. - pi stays OUT of isAltScreenStripMode(). Its default TUI renders into the main screen with terminal-owned scrollback, and its 0.84.0 fullscreen mode is runtime-switchable via /settings; that flip was measured to put the pane into the alt screen, which the strip would have corrupted. pi-cli-resolver.ts additionally sanity-probes `pi --version` and requires semver-shaped output, because `pi` is a short generic name a stray binary can shadow; GET /api/pi/status surfaces path and version so a misresolution is diagnosable rather than presenting as a broken mode. Docker installs pi in its own --ignore-scripts step so that flag cannot affect the other four CLIs, and seeds its credentials per-file rather than whole-dir (~/.pi/agent also holds sessions, extensions and package trees). Verified end to end against pi 0.84.1 on an isolated instance: resolver search-dir fallback, flag construction, piConfig persistence across a full server restart, the trust prompt and its --no-approve suppression, the rose Run button on the default daylight-blue skin (the nested skin block eats per-mode gradients unless the rule lives inside it), and the buffer local-echo policy, which pi tolerates where codex did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
58 KiB
Pi (pi.dev) Run Mode: Implementation Plan
Tracking issue: #206 "Plans to support pi.dev?"
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 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 (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/--<cwd with / replaced by ->--/<timestamp>_<uuid>.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:
- There is no
--dangerously-skip-permissionsanalog 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. - The one privileged knob is
--approve/-a(trust project-local files for this run), which makes pi load and execute project.pi/extensionsTypeScript 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-approveexists, which lets the clamp force-deny rather than merely omit (§3, §5.2). - 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 throughALLOWED_ENV_PREFIXESwithout 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:
{ 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
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 <socket> 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<SessionMode, '...'> 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 adefault:arm; a missingcase 'pi'silently drops docker resume.buildSpawnCommand()(:770-825) andbuildPathExport()(: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
runModeaccessor's setter whitelist (session-ui.js:2949-2960) coerces any unknown mode to'claude'. Omittingpithere makes the mode unselectable while every other edit appears to work: this is the single most deceptive omission in the frontend. window.__codemanCliAvailable(injected byrenderIndexHtml,server.ts:1375-1407): the client treats a missing key as available (isCliAvailablein 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 (
:4420area, forog), and one inside the13681block next to codex's (:13787area), using the block's own idiom (or the color is invisible to the average user). mobile.cssphone-toolbar colors need!importantonbackground/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!importantand 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.pirenders 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
/** Pi CLI session configuration */
export interface PiConfig {
/** Model pattern or ID. Supports `provider/id` and a `:<thinking>` 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 <v> |
/^[a-zA-Z0-9._\-/:]+$/ (: for sonnet:high, / for openai/gpt-4o) |
provider |
--provider <v> |
/^[a-z0-9-]+$/ |
thinking |
--thinking <v> |
runtime allowlist of the 7 enum values (defense in depth beyond Zod) |
resumeSessionId |
--session <v> |
/^[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 <key>: ⚠️ never wire this. It puts a provider secret on the spawn command line, which is exactly what the socket-scopedtmux setenvdiscipline exists to prevent (visible inps, 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 <name>(-n): nice for/resumereadability, but names contain spaces and would be the first user-controlled value needing real shell quoting inbuildSpawnCommand. 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-modemessage_updateframing, 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 adefaultToolssetting), 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. (-ematters 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 callingresolvePiDir()); missing-CLI error chain in createSession :1788-1806(+pi, install hintnpm install -g --ignore-scripts @earendil-works/pi-coding-agent; note respawnPanedeliberately has no such check);piConfigthreading 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 <select>
generically and special-cases only shell).
| File | Change |
|---|---|
index.html |
Welcome button welcomePiBtn after Gemini's (antigravity's is :347; there is deliberately no codex welcome button), display:none default, onclick="app.setRunMode('pi'); app.runPi()", text Run Pi; run-mode-option row with .run-mode-dot.pi after antigravity's (:526-528), before the .run-mode-sep :529; cron <option value="pi">Pi</option> after :803; NEW: the clone-repo "Brain" picker (cloneCaseBrain, :2476-2486): add <option value="pi" data-cli="pi">Pi</option> after the antigravity option :2483 (gating is automatic: session-ui.js :2107-2115 hides options whose data-cli fails isCliAvailable, and :2250 reads the value at clone time); docker image hint :2624 (claude/codex/gemini/opencode/agy + pi). No per-CLI remote-command override field needed (only codex has one, :2559) |
session-ui.js |
@fileoverview mode list :2; run() dispatch branch after :400-402; _refreshRunModeAvailability list :468 (+'pi' as a quoted literal, the static test in §6 demands it); short-label ternary :565 (+'Run PI'); the runMode setter whitelist :2949-2960 (§2.8, the deceptive one); new runPi() modeled on runAntigravity() :1170-1219: same remote/docker skip, same _beginSessionLaunchStatus frame, probes /api/pi/status reading (await res.json()).data.available (envelope!), sends no piConfig at all (no bypass exists and trust defaults are pi's own; envOverrides still sent for local cases), install-hint error text matching Phase 1's; isAltMode :1233 and isExternalCli :1263 four-way comparisons (+pi) |
settings-ui.js |
applyWelcomeCliVisibility() :1176-1191: add ['welcomePiBtn', 'pi'] |
app.js |
Response-viewer agent label :1998-2009 (+pi -> 'Pi'); tab badge ternary :3884 (<span class="tab-mode pi" aria-hidden="true">pi</span>; claude stays badge-less); kill-title ternary :5046-5057 (Kill Tmux & Pi) |
panels-ui.js |
Command-palette labels map :430 (+pi: 'Pi'; the || mode fallback means this is cosmetic, not load-bearing) |
mobile-overview.js |
MOBILE_OVERVIEW_RUN_MODES :55-62: { mode: 'pi', label: 'Pi', short: 'Pi' } after antigravity :60, before the shell entry. Nothing else: the Run-button badge (:499) and menu builder (:554-556) consume the list generically, and the buttons carry btn-toolbar btn-run mode-pi, which is exactly why they inherit the §2.9 cascade problem and its fix |
terminal-ui.js |
Badge-row comment :1750 only (the badge itself is a raw s.mode passthrough, no list to extend). _sessionUsesServerMouseStrip unchanged (§2.2). _updateLocalEchoState unchanged for v1 (§2.10: pi lands on 'buffer' via the fallthrough; only touch it if E2E forces the 'off' fallback) |
i18n.js |
'Run Pi': '运行 Pi' in the zh-CN table (:102-107, matches the welcome-button text; short labels like Run PI are deliberately untranslated, as are the other modes') |
styles.css |
Tab badge .session-tab .tab-mode.pi after :2157 (background: rgba(244,114,182,0.2); color: #f472b6;); add .session-tab .tab-mode.pi to the light-skin ink list :325-336 (gemini + antigravity are its precedent, :332); welcome .welcome-btn-pi + :hover after antigravity's :3366 block, rose family (e.g. base linear-gradient(135deg, #33121f 0%, #9d174d 55%, #be185d 100%), border rgba(244,114,182,0.4), text #fce7f3); toolbar gradient pair .btn-toolbar.btn-run.mode-pi, .btn-toolbar.btn-run-gear.mode-pi + :hover after :4420's antigravity block; .run-mode-dot.pi { background: #f472b6; } in the dot list :4506-4516; and the §2.9 rule inside the Daylight block next to codex's :13787 (e.g. background: linear-gradient(135deg, #be185d, #f472b6); border-color: #be185d; color: #fff1f7;). The dot needs no skin-block entry (the block overrides only claude/opencode/codex/shell dots; gemini/antigravity dots already fall through correctly) |
mobile.css |
Phone toolbar block after :910 inside the @media (max-width: 430px) opened at :338: mode-pi base + :active, with !important on background/border-color/color (§2.9; antigravity's block :895-910 omits it and is dead); light-skin override entry after :2985 with the same four-skin html:is(...) prefix as its siblings |
Phase 4: Docker image and installer
Both files are unchanged since the 2026-08-06 verification; all anchors stand.
-
docker/agent.Dockerfile: a separateRUNstep after the antigravity block (:38-45), not a fifth line in the shared npm block (:31-36), because pi documents--ignore-scriptsand that flag must not silently change how the other four install:# Pi (pi.dev). Upstream documents --ignore-scripts (pi needs no lifecycle scripts); # kept out of the shared npm block above so the flag cannot affect the other CLIs. RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent \ && npm cache clean --force \ && pi --versionImplementation checklist item: the gid-0 pre-created dirs at
:64-68include.claude/projectsand.codex/sessions; verify whether the cred-seed copy into~/.pi/agentcreates its target dir in a fresh container or whether.pi/agentmust join thatmkdirline. Rebuild withnode scripts/build-agent-image.mjs --no-cache(the script itself needs no change; nothing in it is CLI-specific). The cached npm layer has silently frozen a CLI at a broken version before; seedocs/docker-cases.md. -
install.sh(six edit sites, all verified):PI_SEARCH_PATHSblock after:125(mirror the resolver's dirs);check_pi/get_pi_pathpair inserted at:531(antigravity's pair spans:504-530); the satisfying-AI-CLI chain:2032-2063(has_pilocal at:2037area, detect block after:2059, widen the five-way test at:2061and the warn text at:2063); the menu option-4 text:2070; the skip-path hints:2115-2116(addnpm install -g --ignore-scripts @earendil-works/pi-coding-agent (Pi)); the final no-CLI reminder:2416-2423(addcheck_pito the condition and a pi line to the echo block). Detection plus a hint only; do not add an auto-install path in this change.
Phase 5: Docs
docs/pi-integration.md(new, user-facing): install (both installers uninstall via npm), auth (/loginOAuth for six providers vs API keys;pi auth checkfor preflight; Claude Pro/Max third-party harness usage bills as Anthropic "extra usage" per token, not plan limits; OpenRouter login supports pasting the redirect URL, which matters over remote SSH), what Codeman wires up and deliberately does not (§3, incl. never passing--tui-mode), the tmux extended-keys note from §2.7 with the manual~/.tmux.conffallback, Docker/remote behaviour (in-container sessions invisible host-side), the trust model in §1 words, known gaps.CLAUDE.md: tech-stack line (six CLIs +SessionModeunion), the env-prefix gotcha bullet, the multi-CLI prefix-discipline bullet, the "External CLI modes" key-pattern paragraph (note it now also carries the codex predictive-echo block; pi's echo-policy decision from §2.10 belongs in the same paragraph), thesrc/utils/resolver list.docs/architecture-invariants.md: the external-CLI-modes section. ⚠️ Its anchor was already renamed once to#external-cli-modes-opencode-codex-gemini-antigravitywhile CLAUDE.md's link text still shows the old name; when renaming again for pi, update every inbound link (CLAUDE.md and this file).docs/docker-cases.md(cred-seeding table + supported modes + image contents),docs/remote-sessions.md(RemoteCommandMode),docs/cron-guide.md+docs/cron-discovery.md(agentTypeenum; note the readiness caveat from §6's cron paragraph),docs/security-architecture.md(env prefix allowlist row).README.md+README.zh-CN.md: six CLIs.package.jsonkeywords:pi.- Update the issue #206 thread when it ships.
5. Security checklist
- Command injection. Every
PiConfigvalue is regex-validated inbuildPiCommand()before entering thebash -c "..."string; anything failing validation is dropped, not escaped (matching the four existing builders). No user string reaches the spawn line unvalidated. Pinned by a "rejects unsafe values" test per field. - Multi-user clamp, materialize branch.
approveProjectTrustis the privilege-shaped field: it makes pi execute repository-supplied TypeScript and install project packages.clampExternalCliBypassForOwner()(session-routes.ts:305-336) has two branches, and pi belongs in the gemini-style materialize branch, not the codex/antigravity only-if-sent branch: pi's absent-config default is an interactive trust prompt the session user can answer themselves in the terminal, so merely omitting--approveis not a clamp. For a non-granted owner, materialize{ ...(piConfig ?? {}), approveProjectTrust: false }sobuildPiCommandalways emits--no-approveand the prompt never appears. Both call sites (:845,:2897) widen. This helper still has zero test coverage (re-confirmed atf39beb3); §6 adds the first tests. - Secrets stay off the command line.
PI_*overrides flow throughapplyEnvOverrides()/ socket-scopedtmux setenv, never inlined into the spawn string. No-eat container create time. And--api-keyis never wired (§3): it would put a provider secret intops/tmux state. - Env allowlist not widened. Only the
PI_prefix is added; the provider keys stay out (§2.4) andALLOWED_ENV_KEYSis untouched. Pinned by a test thatPI_OFFLINEpasses andANTHROPIC_API_KEYstill fails validation. - Docker seeding, not sharing. Per §2.5: RO mount then copy, so refreshed OAuth tokens never
write back to the host; bind mounts stay excluded from
docker commitso exports remain secret-free. - Remote SSH.
pimode goes throughdefaultRemoteCommandForModeand thereforebuildSshConnectionArgs(). No hand-built ssh line anywhere. - No sandbox claims. Pi documents that it has no sandbox and no permission prompts, and that
extensions run with the user's full permissions. Codeman docs must say plainly that a pi session
can read, write and execute anything the Codeman user can, and point at Docker cases as the
isolation story. Do not imply the trust prompt is a safety boundary (upstream itself says it is
not). Worth one doc sentence:
pi auth print-api-key/print-bearer-token(0.83.0) andpi auth check(0.84.1) mean a pi session can print its own provider credentials by design; isolation, again, is Docker. - Loud-vs-silent audit. Before review, walk §2.8's silent list and confirm each site has its pi branch; the loud ones the compiler already caught.
6. Test plan
test/pi-mode.test.ts(new, modeled ontest/antigravity-mode.test.ts, 125 lines, no port; file unchanged since 2026-08-06 so its structure remains the template):CreateSessionSchema/QuickStartSchemaaccept a pi config; unsafemodel/provider/resumeSessionIdvalues are rejected ('pi; rm -rf /'shapes);buildSpawnCommand({ mode: 'pi', ... })emits expected flags, drops invalid ones, emits--no-approveforapproveProjectTrust: falseand--approvefortrue, and skips-cwhen aresumeSessionIdis present;defaultDockerCommandForMode('pi') === 'exec pi'anddefaultRemoteCommandForMode('pi') === 'exec "${SHELL:-/bin/sh}" -i -l -c \'pi\'';isExternalCliMode('pi') === true,isAltScreenStripMode('pi') === false; the env pair (PI_OFFLINEaccepted,ANTHROPIC_API_KEYrejected), mirroring antigravity-mode:49-63.- First-ever coverage for
clampExternalCliBypassForOwner(still nothing intest/touches it): cover pi's materialize branch (absent config still yieldsapproveProjectTrust: falsefor a non-granted owner; a senttrueis forced tofalse; granted owner passes through) and, while there, pin the three existing modes' behavior. Prefer exporting the helper for direct unit tests over a heavier multi-user route fixture; either way it lives undertest/routes/. test/run-mode-ui.test.ts: extendloadUi()'s stub lists (welcome-button ids, mode buttons,ALL_OFF) and add pi welcome/dropdown gating cases; note the static parser test'gates every mode the run-mode menu actually offers'(:433-456) picks up the newdata-mode="pi"from index.html automatically and fails until_refreshRunModeAvailabilitycontains a quoted'pi', which is exactly the regression it exists for. Add adescribe('Pi quick start')modeled on the antigravity one (:840) drivingrunPi()against a stubbed/api/pi/status+/api/quick-start, asserting the posted body hasmode: 'pi'and nopiConfig, and that the envelope is unwrapped. (The short-label assertion pattern is at:82,'Run AG'.)test/render-index-html.test.ts:141: the injectedwindow.__codemanCliAvailableis asserted with an exacttoEqualand now carries seven keys (claude, opencode, codex, gemini, antigravity, cloudflared, and since 1.12+git), so it must gain thepikey (and the resolver mock anisPiAvailable); its comment explains why: a dropped key silently un-gates (§2.8).test/routes/system-routes.test.ts:GET /api/pi/statusshape, modeled on the antigravity describe (:816-838) + resolver mock (:84-87); file unchanged since 2026-08-06.test/mobile-overview.test.ts::375is an exact-arraytoEqualover the run-menu modes and will fail until updated to include'pi'(the second exact-array at:366,['claude', 'shell'], is a gating case and stays as-is); the sibling static parser then covers the new entry automatically. The no-hex-literals guard only scans.mobile-overview*rules, so pi'smode-picolors in mobile.css do not trip it.test/local-echo-codex-gating.test.ts(§2.10): once the buffer-policy decision is confirmed in E2E, add'pi'to theit.each(['claude', 'gemini', 'opencode'])lists (:193,:376) so the chosen policy is pinned.test/skin-themes.test.ts: will NOT trip (it enumerates skins, not modes); run it anyway since styles.css is touched.test/mobile-header-buttons-policy.test.ts: trips only if a header button is added; pi adds none (welcome button and run-menu rows are outsideheader-right).- Cron: schema-level acceptance of
agentType: 'pi'(the service consumesSessionModegenerically;src/cron/is unchanged since the first draft). Known, documented degradation: the readiness poll (cron-service.ts:515) looks for❯/tokens, which pi never prints, so cron pi jobs burn the ready-poll attempts and then send anyway. Acceptable for v1; note it indocs/cron-guide.md. - Sweep with
npm run test:ci. Never barenpm test. No new ports needed (all new/extended suites are portless).
7. End-to-end verification (required before COM)
Unit tests passing is not evidence the mode works (pi is not currently installed on the dev box, so step 1 is a real step). Before shipping:
- Install pi (
npm install -g --ignore-scripts @earendil-works/pi-coding-agent), authenticate once with/login. curl -sk https://localhost:3000/api/pi/status | jqreportsavailable: true, the right path, and a saneversion.- Create a throwaway case, launch a pi session from the Run dropdown, send a prompt from the
browser, confirm the reply renders and scrollback survives a tab switch. Do not touch
w1/w2/w3. - Local-echo policy gate (§2.10): on a phone profile, type into the pi editor through the
buffer overlay (drive with
page.keyboard.type(), neverapp.sendInput(), and forceapp._localEchoEnabled = true; headless Chromium reports touch as false) and confirm pi's composer renders the flushed text correctly on Enter. If it mis-renders, flip pi to the'off'branch in_updateLocalEchoStateand pin that instead. - Visual pass on the default skin (the §2.9 finding makes this the load-bearing check, not a
formality): run-button gradient actually renders rose (not generic claude blue), dot, tab badge,
welcome button, kill-menu label; then a phone profile (toolbar
!importantcolors and light-skin overrides are the usual regressions). - Kill and respawn the session; confirm
piConfiground-trips throughstate.jsonand the pane comes back with the same flags. Then/clear-style respawn via the Respawn tab. - Extended keys (§2.7): in an attached terminal, verify whether Shift+Enter inserts a newline in
pi's editor with and without the socket-scoped options; record the outcome in
docs/pi-integration.mdeither way. While attached, also flip/settingsto the fullscreen TUI and back to confirm the no-strip decision holds (§2.2). - Trust model: point a throwaway case at a repo containing
.pi/extensions, confirm the trust prompt appears interactively and that a multi-user non-granted session instead launches with--no-approve(prompt never shown, extensions not loaded). - NOT RUN in this pass — an honest gap. Docker case with
mode: 'pi': rebuild the agent image with--no-cache, confirmpi --versioninside the container as theagentuser, confirm seeded auth works and a session starts (this is exactly where the antigravity Docker path broke in 1.11.2: the CLI was never installed in the image). - NOT RUN in this pass — the other gap. Remote SSH case with
mode: 'pi': confirm the login-shell wrapper resolves the npm global bin. - Only then: changeset,
COM minor(new capability, additive to the API surface).
Verification actually performed (2026-08-13, pi 0.84.1, isolated CODEMAN_INSTANCE=pi-beta
server on :5055 with its own tmux socket and data dir): steps 1-8 pass. Highlights:
/api/pi/status resolved through the search-dir fallback (pi installed to ~/.npm-global/bin,
deliberately not on PATH) and reported
{available:true, path:'/home/arkon/.npm-global/bin', version:'0.84.1'}; the real spawn line came
out as … COLORTERM=truecolor … && pi --approve --provider anthropic --thinking high; piConfig
round-tripped through state.json across a full server restart; the trust prompt appeared for a
case containing .pi/extensions + .pi/settings.json, and --no-approve suppressed it
(This project is not trusted. Project .pi resources and packages are ignored.); on the default
daylight-blue skin the toolbar Run button computed to
linear-gradient(135deg, rgb(190,24,93), rgb(244,114,182)) — genuinely rose and distinct from
claude's blue, so the §2.9 cascade trap is avoided; and flipping /settings to the fullscreen TUI
put the pane into the alt screen (alternate_on=1), empirically confirming §2.2: had pi been in
the strip list, Codeman would have stripped that switch and corrupted the session. Steps 9-10 need a
Docker daemon and a remote host respectively.
8. Effort estimate
Calibrated against the real antigravity history, which is the honest baseline: the feature commit
26cbbe0 was 24 files, +638/-63, and it then took four follow-up commits (e803186 login-shell
routing, 292ba2c ownership helpers, 5d28999 CLI gating incl. tests, 0d0b772 docs/installer/UI
propagation) totaling roughly +600/-170 across ~43 file-touches to make the mode actually
first-class. Budgeting only the feature-commit shape under-scopes by ~40%. This plan folds all four
follow-up surfaces in from the start (login-shell routing in Phase 1, availability gating in Phases
2-3, installer/docs propagation in Phases 4-5), so expect the full footprint in one pass:
| Phase | Size |
|---|---|
| 1. Backend core | ~260 lines across 9 files, one new file (resolver incl. version probe) |
| 2. Web layer | ~110 lines across 4 files (incl. the clamp widening + availability inject) |
| 3. Frontend | ~175 lines across 10 files (enumerations + CSS in two sheets + skin block + the Brain picker option) |
| 4. Docker + installer | ~45 lines, plus one --no-cache image rebuild |
| 5. Docs | one new doc, ~10 files touched |
| 6. Tests | one new test file, 6 extended (2 of which fail loudly until updated), plus the first clamp coverage |
9. Out of scope, tracked as follow-ups
- A Codeman pi extension for real idle/completion events (highest value, now fully de-risked).
Pi extensions are TypeScript modules with Node built-ins and npm deps available, so an HTTP POST
to
/api/hook-eventis trivial. Theagent_settledevent shipped in 0.84.0 and is documented for exactly this use case (fires only when pi will not continue on its own: after auto-retries, auto-compaction and queued follow-ups;ctx.isIdle()is true inside the handler). That is a genuine idle signal replacing output-silence heuristics, i.e. the same class of upgrade hooks give Claude sessions. The bash tool exposes five env vars (PI_SESSION_ID,PI_SESSION_FILE,PI_PROVIDER,PI_MODEL,PI_REASONING_LEVEL), injected per command. Bonus: an extension can own theproject_trustevent (first yes/no wins, and CLI-eextensions load before trust resolution), so Codeman could answer the trust prompt programmatically, a cleaner mechanism than the--approveflag for both the single-user convenience case and the multi-user deny case. - Response viewer for pi. Sessions are JSONL v3 under
~/.pi/agent/sessions/--<cwd-dashed>--/<timestamp>_<uuid>.jsonlwith anid/parentIdtree and typed content blocks (text, image, thinking, toolCall); the cwd-derived dir name is trivially computable host-side. Feasible, and it would justify flipping the Docker cred policy to sharesessions/RW like Codex. - Mode-aware env allowlist. Would let pi sessions accept provider keys without widening the
global list. Needs
ALLOWED_ENV_PREFIXESto become a per-mode map plus mode context inside the Zod refine. --tools/--exclude-tools/--no-tools/--no-builtin-toolsread-only sessions (plus the 0.84.0defaultToolssetting). Real product value, needs UI.- Predictive echo for pi's composer if the §2.10 buffer decision does not hold up in practice:
teach
PredictiveEchoAddonpi's composer row the wayisCodexComposerRowhandles codex's. --mode json/--mode rpc, and upstream's experimental remote-session client APIs (transport-neutralPiClient, CBOR protocol, Unix-socket transport,RemoteSessioncontroller, still unreleased as of 0.84.1). A potential non-PTY integration path, a different architecture from the tmux+PTY model. Note the already-shipped breaking change tomessage_updateframing (delta-only): any consumer must assemble deltas betweenmessage_start/message_end.--namefor session labels. Blocked on shell-quoting a user string inbuildSpawnCommand.
10. Risks
| Risk | Mitigation |
|---|---|
pi resolves to an unrelated binary |
pi --version + semver-shape check in the resolver (§2.6); path and version shown in /api/pi/status |
| Pi's TUI repaints in a way the browser terminal handles badly | Test scrollback and repaint early (step 3 of §7); pi's default is main-screen with terminal-owned scrollback, which is the friendly case |
| Fullscreen TUI mode (shipped 0.84.0, runtime-switchable) | Already designed for: pi stays OUT of the strip list, so a user flipping /settings to fullscreen gets opencode-like alt-screen behavior, not corruption. §7 step 7 tests the flip explicitly |
| The buffer local-echo overlay fights pi's live composer | §2.10: explicit E2E gate (§7 step 4) with the one-line 'off' fallback; predictive echo for pi is a tracked follow-up, not a v1 blocker |
| Pi moves fast (pre-1.0; 9 releases in the 7 weeks before 0.84.1) | Keep the flag surface small; every flag validated and droppable; nothing pinned in the Dockerfile beyond the --no-cache rebuild cadence. Live example of the hazard: --tui-mode went from main-only docs to released between the two drafts of this plan |
| Docker image grows | Pi is an npm package; the layer is modest next to the ~190MB agy binary |
| Trust prompt blocks a session | Narrower than feared: only fires when .pi/settings.json, .pi/extensions|skills|prompts|themes, .pi/SYSTEM.md/APPEND_SYSTEM.md or .agents/skills exists (bare .pi/ does not). Documented; approveProjectTrust is the opt-in escape hatch; multi-user forces --no-approve (§5.2); the project_trust extension follow-up removes the prompt entirely |
Interactive /login OAuth can't complete headlessly |
Document: authenticate once interactively (or seed auth.json); pi auth check verifies credentials preflight; OpenRouter's paste-the-redirect-URL flow covers remote SSH |
| Provider auth is awkward without key prefixes in the allowlist | /login writes ~/.pi/agent/auth.json once and Docker seeds it; the mode-aware allowlist follow-up removes the friction |
| Cron pi jobs mis-detect readiness | Known degradation, documented in §6; readiness falls through after the poll budget and the prompt still sends |