Skip to content

feat(pi): add Pi (pi.dev) as a sixth CLI run mode (#206) - #282

Merged
Ark0N merged 4 commits into
masterfrom
feat/pi-mode
Aug 13, 2026
Merged

feat(pi): add Pi (pi.dev) as a sixth CLI run mode (#206)#282
Ark0N merged 4 commits into
masterfrom
feat/pi-mode

Conversation

@Ark0N

Copy link
Copy Markdown
Owner

Closes#206.

Adds Pi (@earendil-works/pi-coding-agent, MIT) as a sixth CLI run mode, following docs/pi-integration-plan.md (included in this PR, updated with the measured results).

SessionMode gains 'pi': its own PTY, tmux session, rose tab identity, welcome button, run-mode entry, cron agentType, 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-permissions analog. Its one privilege-shaped knob is the tri-state approveProjectTrust, which makes pi load and execute repo-local .pi/extensions TypeScript 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-approve even 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, and ALLOWED_ENV_PREFIXES is 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 /login or the server process's own environment. A mode-aware allowlist is tracked as a follow-up. Relatedly, --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, 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

  • pi is a short, generic binary name, so unlike the sibling resolvers pi-cli-resolver.ts sanity-probes pi --version and requires semver-shaped output, logging any rejected path. GET /api/pi/status returns { available, path, version } so a misresolution is diagnosable instead of presenting as "the mode just doesn't work".
  • PiConfig maps to --model (its regex admits : and / for sonnet:high and openai/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 no piConfig at all.
  • Docker: pi installs in its own --ignore-scripts step, 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/agent also holds sessions/, extensions/, skills/ and the installed package trees. Trade-off documented: in-container pi sessions are invisible host-side.
  • CSS: the run-button gradient needs a rule inside the nested 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.css uses !important for the same reason.

Verification

Unit + integration: npm run test:ci green (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):

CheckResult
/api/pi/status{available:true, path:'~/.npm-global/bin', version:'0.84.1'} — resolved via the search-dir fallback, pi deliberately not on PATH
Spawn line… COLORTERM=truecolor … && pi --approve --provider anthropic --thinking high
Session launchpi TUI renders in the main buffer, scrollback intact
piConfig persistenceround-trips through state.json across a full server restart
Trust promptfires for a case with .pi/extensions + .pi/settings.json; --no-approve suppresses it (This project is not trusted…)
Default-skin coloursRun button computes linear-gradient(135deg, rgb(190,24,93), rgb(244,114,182)) — rose, and distinct from claude's blue
Fullscreen TUI flipalternate_on 0 → 1 → 0, confirming the no-strip decision

Two open questions from the plan got empirical answers:

  • Local echo (§2.10). The plan flagged the buffer overlay as a real risk, since that policy is what starved codex (Cursor control doesn't work as expected #218/Pasting content doesn't append to existing prompt #219/Long prompt overflows the bottom of the screen #220/Codex: slash command picker not working #222). It does not reproduce: pi's slash picker re-filters on the whole composer content, not per-keystroke deltas, so a one-shot flush of /set filters the picker to settings identically to sending five separate keystrokes, and the delayed \r then selects it. Pinned in test/local-echo-codex-gating.test.ts; the 'off' fallback stays documented but unused.
  • tmux extended keys (§2.7). Measured on tmux 3.4: tmux -L <socket> set -s extended-keys on takes effect on an already-running server with no kill-server (pi's own startup warning is a convenient in-band probe and disappears for the next session). extended-keys-format does 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 to CLAUDE.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.sh detection and package.json keywords.

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>
@Ark0N
Ark0N merged commit 829b202 into masterAug 13, 2026
2 checks passed
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plans to support pi.dev?

2 participants

@Ark0N@claude