feat(pi): add Pi (pi.dev) as a sixth CLI run mode (#206) - #282
Merged
Conversation
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>
Review follow-ups on #282. All four are the same failure shape: a list that enumerates run modes, missed by the sweep that added 'pi'. 1. Cron ignored pi's project-trust clamp. The PR widened CronJobBaseSchema's agentType to accept 'pi' but not the matching clamp beside gemini's, so a non-granted multi-user owner's cron pi job spawned bare `pi` (pi's own defaultProjectTrust, an interactive prompt they can answer "yes" to, which loads and EXECUTES repo-local .pi/extensions TypeScript) while the same user's UI/API launch was forced to --no-approve. The clamp is now a pure exported helper, clampCronExternalCliConfigs(), so both it and gemini's previously untested materialization are pinned. 2. POST /api/sessions/:id/interactive auto-enabled the Ralph tracker for pi: its denylist covered opencode/codex/gemini/antigravity only. The tracker is never fed for an external CLI (_processExpensiveParsers returns early), so a pi session reported ralphEnabled and Ralph UI state no sibling backend shows. 3. REMOTE_CLI_BIN had no pi entry, so buildRemoteCliVersionProbeCommand() returned null and Session.cliVersion stayed blank for every remote-SSH pi session, even though the PR wired the remote launch command and the per-mode override schema field. 4. The desktop home rail's badge map had no pi entry, and its lookup falls back to '', which is what claude renders. A pi session read as Claude there while the tab strip and phone overview badged it correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ionale, update the skill Second review pass on #282, the three items left open after f4dcfbe. 1. `codeman doctor` and the run mode disagreed about pi. The registry entry accepted a bare `which pi` hit while pi-cli-resolver demanded semver-shaped `--version` output, so the Dependencies panel could report an installed Pi CLI on a box where Run Pi stays hidden, which reads as a broken mode rather than a missing install. Both sides now share one exported PI_VERSION_REGEX, and PathResolver gains an opt-in `requireVersionMatch` so a binary that fails the shape check is reported MISSING instead of installed-with-unknown-version. Only pi sets it; every other tool keeps its current behaviour. 2. The isAltScreenStripMode comment justified excluding pi with "the alt screen is load-bearing for its fullscreen TUI". That is not what exclusion does: pi is tmux-backed, so it falls through to isMuxAltScreenOnlyStripMode, which strips the alt-screen toggles anyway. What exclusion actually preserves is `\x1b[3J` and the mouse DECSETs, which is the real reason (pi renders into the main screen and is mouse-aware). Comment and changeset now say that, and state the consequence: fullscreen pi paints into the main buffer, like vim in a tmux shell session. 3. skills/codeman still enumerated the five pre-pi modes in nine places, telling agents a backend does not exist and understating class-wide caveats by one mode. All updated, plus stale session.ts line references refreshed. Tests: a new static guard derives the mode set from the Zod schema (not a copy) and fails when a skill enumeration lists a partial set of external CLIs, verified by mutation. It also documents the one legitimate exception it found: the "writes no transcript" lists drop codex, which does write a rollout Codeman reads back. Plus doctor cases for an unrelated `pi` on PATH and registry/resolver regex parity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…family `GET /api/pi/status` shipped undocumented in the agent skill, and only a human reading the doc noticed. Turns out none of its five siblings were documented either, so this adds the whole family in one place: spawning with a mode whose CLI is absent fails with OPERATION_FAILED rather than falling back, which is exactly what an agent picking a backend it did not choose needs to know. Pi's extra `.data.version` is called out, since a false `available:false` there means an unrelated `pi` is in front on PATH. On whether the endpoint scanner should also check registered-to-documented: measured, and NO for the general case. The skill documents 34 of 217 registered endpoints deliberately (it is an agent guide, not an API reference), so a blanket reverse check needs a 183-entry allowlist that would fail CI on unrelated route work and get appended to mechanically, which is worse than the gap it closes. Grouping by path shape does not save it either: the families that yields are things like `DELETE /api/<any>/:id`, lumping cases, webviews and docker hosts together, and it would not have caught this gap anyway (the family had zero documented members). What IS cheap is a family the schema can enumerate with no allowlist: the new assertion derives the agent modes from the Zod enum and requires each one's `/api/<mode>/status` to be documented, so a seventh backend fails here until it is. The sibling scanner still proves the other direction, that nothing documented is a 404. Both mutation-checked: dropping pi's probe fails the new guard, and documenting a nonexistent probe fails the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Ark0N pushed a commit
that referenced
this pull request
Aug 13, 2026
PR #282 added pi across the prominent surfaces but left the enumerations that read as exhaustive: the env-prefix allowlist (missing PI_*), the external-CLI list for stop/blocked, cron's agent types (also missing antigravity), the narrow-strip mode list, and the claude-only caveats in the cron and Read My Mind guides. Both READMEs and the four affected docs now agree with the schema. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes#206.
Adds Pi (
@earendil-works/pi-coding-agent, MIT) as a sixth CLI run mode, followingdocs/pi-integration-plan.md(included in this PR, updated with the measured results).SessionModegains'pi': its own PTY, tmux session, rose tab identity, welcome button, run-mode entry, cronagentType, Docker + remote-SSH command defaults, and clone-repo Brain option. Not a location overlay, not a web tab.Pi is a different shape of CLI, and three decisions follow from that
No bypass flag, and none was invented. Pi has no permission prompts and no sandbox, so there is no
--dangerously-skip-permissionsanalog. Its one privilege-shaped knob is the tri-stateapproveProjectTrust, which makes pi load and execute repo-local.pi/extensionsTypeScript and npm-install missing project packages.clampExternalCliBypassForOwner()therefore puts pi in the materialize branch (gemini's, not codex/antigravity's only-if-sent one): a non-granted multi-user owner gets--no-approveeven when no config was sent, because pi's own default is an interactive prompt the session user could simply answer "yes" to. That helper had zero test coverage; this PR adds the first, for all four CLIs.Only the
PI_prefix joins the env allowlist. Pi reads ~34 provider key vars (ANTHROPIC_API_KEY,OPENAI_API_KEY,HF_TOKEN, …) that share no prefix, andALLOWED_ENV_PREFIXESis one global list applied by a refine with no mode context, so admitting them for pi would widen the allowlist for every mode at once. Auth goes through pi's/loginor the server process's own environment. A mode-aware allowlist is tracked as a follow-up. Relatedly,--api-keyis 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, so there is nothing to strip, and since 0.84.0 the user can flip to a fullscreen TUI at runtime via/settings. I measured that flip: it puts the pane into the alt screen (alternate_on=1), which the strip would have corrupted.Other notes
piis a short, generic binary name, so unlike the sibling resolverspi-cli-resolver.tssanity-probespi --versionand requires semver-shaped output, logging any rejected path.GET /api/pi/statusreturns{ available, path, version }so a misresolution is diagnosable instead of presenting as "the mode just doesn't work".PiConfigmaps to--model(its regex admits:and/forsonnet:highandopenai/gpt-4o),--provider,--thinking,--session/-c, and--approve/--no-approve. Every value is regex-allowlisted and dropped on failure, matching the four existing builders. The Run button sends nopiConfigat all.--ignore-scriptsstep, kept out of the shared npm block so that flag cannot silently change how the other four CLIs install. Credentials are seeded per-file (auth.json,settings.json,trust.json,models.json,models-store.json) rather than whole-dir, because~/.pi/agentalso holdssessions/,extensions/,skills/and the installed package trees. Trade-off documented: in-container pi sessions are invisible host-side.html:not([data-skin="og"])block, not just the base sheet. That block's nesting raises specificity to (0,3,1), which is why gemini's and antigravity's toolbar gradients currently render as generic claude blue on the default skin.mobile.cssuses!importantfor the same reason.Verification
Unit + integration:
npm run test:cigreen (5007 passed). Typecheck, lint, prettier, frontend-syntax, public-assets and lockfile checks all pass.End-to-end against pi 0.84.1 on an isolated instance (
CODEMAN_INSTANCE=pi-beta, own tmux socket and data dir, no live session touched):/api/pi/status{available:true, path:'~/.npm-global/bin', version:'0.84.1'}— resolved via the search-dir fallback, pi deliberately not onPATH… COLORTERM=truecolor … && pi --approve --provider anthropic --thinking highpiConfigpersistencestate.jsonacross a full server restart.pi/extensions+.pi/settings.json;--no-approvesuppresses it (This project is not trusted…)linear-gradient(135deg, rgb(190,24,93), rgb(244,114,182))— rose, and distinct from claude's bluealternate_on0 → 1 → 0, confirming the no-strip decisionTwo open questions from the plan got empirical answers:
/setfilters the picker tosettingsidentically to sending five separate keystrokes, and the delayed\rthen selects it. Pinned intest/local-echo-codex-gating.test.ts; the'off'fallback stays documented but unused.tmux -L <socket> set -s extended-keys ontakes effect on an already-running server with nokill-server(pi's own startup warning is a convenient in-band probe and disappears for the next session).extended-keys-formatdoes not exist before tmux 3.5 and errors, so the options must be issued independently. Decision: Codeman does not set this itself, since it is a server-wide option affecting every backend; it is documented as a user step instead.Not verified in this pass, and called out as such in the plan doc: Docker case with
mode: 'pi'(needs an image rebuild with--no-cache) and a remote-SSH pi case (needs a remote host).Docs
New
docs/pi-integration.md(user guide), plus updates toCLAUDE.md,docs/architecture-invariants.md,docs/docker-cases.md,docs/remote-sessions.md,docs/cron-guide.md,docs/cron-discovery.md,docs/security-architecture.md, both READMEs,install.shdetection andpackage.jsonkeywords.