Pi runtime package
Pi (pi, the Pi coding agent) runs under pohunek through a runtime package, not
through compiled daemon code. The package source is runtime-packages/pi
(runtime.toml descriptor and detect.toml manifest, nothing else); its
compatibility lock and captured screens are in compat/pi/. See
runtime packages for the generic install, trust, and
selection commands.
Install
Section titled “Install”Pi must be on the daemon’s PATH (npm install --global @earendil-works/pi-coding-agent@<release>, Node 22.19 or newer).
Since release 0.34.2 every Linux daemon archive ships the Pi package as
runtime/packages/pi.tar.zst with the signed runtime/runtime-catalog.json.
Installing it with --catalog makes it an official package; see installing
official packages.
From a checkout, build the archive and install it with the digest it prints:
cargo xtask package build runtime-packages/pi --output pi.tar.zstpohunek plugin install ./pi.tar.zst --sha256 sha256:<digest> --yespohunek host inspect local --jsonpohunek session new --agent pi --input "Refactor the parser."That trust is local and explicit: the digest you supply authorizes those exact
bytes, and such an install is never official. The runtime id pi is not
reserved, so either way of installing serves it.
What the package declares
Section titled “What the package declares”-
Launch:
piwith the first prompt as a positional argument, and bracketed paste followed by a separate Enter 150 ms later for later input. -
Readiness:
ready = "screen_idle". Pi 1.0.2 enables bracketed paste about 130 ms after spawn, before it draws any screen, so a host profile’s initial input (written into the terminal, not passed as an argument) waits for the idle editor frame, theidle_editor_ruleof the detection manifest. A session whose editor frame does not show within the host’sinitial_startup_grace_msfailssession.newwithsession_input_not_readyand is rolled back; the initial input is not written. The manifest recognises the editor frame from 20 columns up, so such a launch into a narrower terminal is always refused. -
Version:
pi --versionprints exactly the release on standard output (extension noise goes to standard error, which the probe discards). The descriptor accepts[1.0.0, 2.0.0), the verified range’s lower bound up to the next major (only 1.0.2 is verified); any other release, or output that is not oneMAJOR.MINOR.PATCHline, refuses the launch withagent_runtime_unsupported.host.inspectshowsversionandsupported. The probe runs under thePATHthe launch will see (the daemon’s forwarded environment overridden by the profile’sPATH), so a profile that selects a Node installation is probed with that Node; every other variable is isolated. -
Native reference:
strategy = "assigned"withassign_fork = true. The daemon generates a UUID, startspi --session-id <id>(Pi creates the conversation with that id), resumes withpi --session <id>, and forks withpi --fork <id> --session-id <fork id>under a second UUID it generates for the fork. No integration hook is involved. -
Existence check: before resume or fork the daemon looks for a regular file named
*_<id>.jsonlin a project directory below$PI_CODING_AGENT_DIR/sessions/(default~/.pi/agent/sessions/). Pi writessessions/--<cwd with / as ->--/<ISO time with : and . as ->_<id>.jsonl, and only after the first model reply. When the launch environment setsPI_CODING_AGENT_SESSION_DIR(from the profile[env]or the daemon’s forwarded environment), Pi keeps the files directly in that directory and the check looks there instead; the value must be an absolute path. -
Session-dir overrides: Pi picks its session directory as
--session-dir, elsePI_CODING_AGENT_SESSION_DIR, else thesessionDirsetting of$PI_CODING_AGENT_DIR/settings.jsonor the project’s.pi/settings.json. The daemon follows the variable, and refuses a launch it could not recover from withagent_native_store_relocatedbefore any worker starts: a profile whoseargspass--session-dir, a relative variable, or (while the variable is unset) asessionDirset in either settings file, or a settings path that is not a readable JSON file (a FIFO or a directory there is refused without waiting on it). At launch the hint namesPI_CODING_AGENT_SESSION_DIR: setting it in the profile[env]to the directory the setting names satisfies both Pi and the check, because Pi follows the variable over the setting; the flag wins over the variable, so its hint says to remove it. Resume and fork run the same refusal against the session’s arguments and working directory first. The launch passed that refusal, so a refusal there means something changed since: asessionDirsetting appeared, or the variable the launch had is gone (a daemon restarted without forwarding it, or a profile edit accepted with--accept-profile-change) and an older setting now wins. The hint says to undo the change: remove the setting (or make an unreadable settings file readable JSON), or restorePI_CODING_AGENT_SESSION_DIRto the absolute path the session was launched with; an edited profile then needs--accept-profile-change. Pointing the variable at a new directory would not find the conversation. -
Detection: every rule reads the editor frame anchored at the bottom of the screen: an upper border, a body, a plain lower border, then one to four footer lines. A busy Pi writes
──, a braille spinner and its message (Working,Compacting context...,Auto-compacting...,Retrying (n/m) in Ns...) into the upper border, cut to the terminal width, so its closing rule can be a single─; the── <spinner>start alone marks a busy border at any width, and it meansworking. A plain upper border (a rule of 20 or more─, or a↑ N more/↓ N moremarker between runs of four or more) around the draft meansidle. Terminals narrower than 20 columns are never read as idle. Body and footer lines are never borders, so the lower border is the last border on the screen and the working and idle rules cannot both match;scripts/pi-detect-regexgenerates and checks the patterns. -
Dialogs and selectors: Pi draws them in place of the editor, between plain borders and without the busy border even while a turn waits on them, so the body decides.
blocked(priority aboveworking) is a body that holds the key hint or title of a prompt waiting for an answer, whoever opened it:- an extension’s select, confirm, input or editor dialog, also when a permission check raises it mid-turn;
- Pi’s own confirms drawn the same way: the project trust prompt at startup,
the missing session cwd prompt, “Summarize branch?” in
/treeand “Import session”; - the
/trustselector; - every login step (method, provider list, API key, browser or pasted-code login).
The owner-opened ones (
/trust,/login,/tree’s summary question, import) readblockedand notify too: each holds the session on a question, and their screens cannot be told from an extension’s dialog. Hint words may wrap onto padded continuation lines, the startup prompts draw no footer, and a prompt taller than the terminal may push its upper border off the screen; all three still readblocked. A prompt so tall that Pi clips its bottom rows loses its lower border, and in some cases its key hint; it shows no frame and matches no rule. A body that holds the two rows a selector list draws (a cursor row starting→,→or›, and a row indented by exactly two spaces: another item, the(n/m)counter or the selected item’s details) withholdsidle, so the model, session, tree, fork and settings selectors (opened by the owner, asking nothing) match no rule and keep the byte-activity fallback, which publishesworkingfrom process output until the editor returns. A blocked session raises the “Agent needs attention” notification and theagent_blockedturn event, and endssession wait --activity blocked. The package’s unrestricted input policy still accepts a plainsession inputwhile Pi is blocked (and Pi’s submit delay rules out the waited form), so an orchestrator waits foridleorblockedbefore it sends and leaves a blocked session to the owner. -
What was verified: the idle, busy, compaction, retry, scrolled-draft and transcript-rule screens in
compat/pi/screens/(widths 20 to 200 inwidths/), and the dialog,/trust, login and selector screens incompat/pi/screens/overlays/(each at widths 20 to 200, the unframed[y/N]question at 100 only), were rendered by a real Pi driven with real key presses. Not covered, and matching no rule: a dialog an extension draws withctx.ui.custom(), a replacement editor or an extension that changes the spinner, and Pi’s plain-textFork this session into current directory? [y/N]question before its interface starts (Pi asks it only when the session belongs to another directory; pohunek relaunches a session in its own). Draft and transcript text: transcript lines never decide; a quote (> text) or a single line starting with a cursor is draft text and readsidle; a draft that holds both selector rows (a line starting→or›and one indented by two spaces) withholdsidle, so the session staysworkinguntil the draft no longer holds both rows; a draft line that reproduces a dialog’s key hint readsblocked; and a draft line that starts── <spinner>or is itself a complete plain border cannot be told from the frame. Process matchers accept the kernel namepi(Pi retitles its process shortly after start) andnoderunning…/coding-agent/dist/bundle/cli.jsfor the first moments.
Operating notes
Section titled “Operating notes”- A session that never received a model reply has no session file;
resumeis refused withagent_native_reference_missingbefore any worker starts, as is a resume after the file was deleted. - A forked session holds its own assigned reference from the start: Pi writes
the fork’s file
<ISO time>_<fork id>.jsonlat once (its header names the source file asparentSession), before any model turn, so the fork resumes withpi --session <fork id>after a stop or a daemon restart. Pi refuses a fork id that already names a session; the daemon generates a fresh UUID for every fork. Package version 1.0.1 and later declare this; a host with 1.0.0 installed gets it withpohunek plugin update pohunek.runtime.pi <archive>(installed beside 1.0.0 and selected). A session launched from 1.0.0, or from a profile still pinned to its digest, keeps the spec it was launched with, so its forks hold no reference and are not resumable. - Package version 1.0.2 adds the
blockedand overlay-aware idle rules and the session-dir handling. A session keeps the existence check it was launched with: one launched from 1.0.1 or earlier searches only the agent directory’s store and refuses nothing at launch, so it fails closed at recovery when its Pi kept the files elsewhere. Detection uses the manifest of the package version a session was launched from, so a session launched from 1.0.1 keeps reading its dialogs and selectors asidle. - To keep sessions outside the agent directory, set
PI_CODING_AGENT_SESSION_DIR(absolute) in the host profile’s[env], or setPI_CODING_AGENT_DIRto move the whole agent directory; do not pass--session-dirin profileargs. - A stopped session moves to a newer official Pi package version on resume or
fork when three things hold. The newer version keeps the session’s native
reference check and its
--session-idassignment. A session launched from a profile also needs the profile to pin the newer version (pohunek plugin profile migrate <name> --yes;service upgradedoes not repin Pi profiles). A session launched as the barepiruntime follows the selected version. The relaunch is then pinned to the newer digest. A version whose check or assignment differs, such as 1.0.2 over a session from 1.0.1, keeps the session on its own digest. - Pin a profile to the installed digest (
pohunek plugin profile migrate) so a laterplugin updatecannot change an existing profile’s package. A profile[env]may setPI_OFFLINE,PI_SKIP_VERSION_CHECKandPI_TELEMETRY; never put provider keys into docs or logs.
How the package is kept honest
Section titled “How the package is kept honest”crates/cli/tests/pi_package.rsalways runs: it builds the package directory twice and requires identical bytes, parses it with the daemon’s install parser, requires the descriptor’s supported range to equalcompat/pi/compatibility-lock.json, runs the manifest on real Pi screens (compat/pi/screens/, withoverlays/) and process forms, and runs the existence check and the session-dir refusals on Pi’s real file layouts.- The same file holds
#[ignore]d tests that drive a realpithrough an installed package: launch, idle and working detection, input, stop, resume, fork, stop and resume of the fork, refusal without a session file, an extension dialog and the project trust prompt readblocked, a session kept inPI_CODING_AGENT_SESSION_DIRresumes, and asessionDirsetting or the--session-dirflag refuses the launch. A loopback chat-completions stub stands in for the model, so no provider, credential or network is involved. Run them withPOHUNEK_PI_E2E=1 cargo test -p pohunek-cli --test pi_package -- --ignored; the variable and apionPATHare required, otherwise the run fails by name instead of skipping. - The
pi-packageCI job installs exactly the locked release from npm, builds the archive withcargo xtask package build, and runs that file.
Update procedure for a new Pi release: install it, rerun the real-Pi tests, move
the lock (release, and supported if the range changes) and the descriptor’s
min/below together, refresh the captured screens (including the dialog and
selector screens in overlays/) if the interface changed, and bump the package
version.