Reproducible configuration for pi, installed by
mise bootstrap through the bootstrap:pi task.
Tracked here:
- Pi settings and pinned package sources, one file per host
- custom keybindings (e.g. Opt+Enter inserts a newline)
- approval-guardian policy
- local TypeScript extensions, plus the tooling to check them
- prompt templates, and the vendored design skills behind
/artifact
settings.<host>.json is linked to ~/.pi/agent/settings.json by
bootstrap:pi, named after hostname -s. A new machine is seeded from
settings.default.json, which holds only the machine-neutral preferences — no
providers, packages, skills, or terminal capabilities.
This directory is published, and some hostnames are asset tags. To pick the
name yourself, write it into .pi-agent/host (untracked) or export
PI_SETTINGS_HOST; either one wins over hostname -s.
One shared settings.json does not survive two machines. Pi rewrites the file
as you work (model switches, lastChangelogVersion, dismissed warnings), so
every host carried a permanent uncommitted diff of it and every pull was a
conflict waiting to happen. Splitting it also lets each host enable only what
it can actually reach: the Linux box has no local oMLX server and no Copilot
credentials, and naming them there printed a warning on every launch.
The runtime writes land in a tracked file on purpose — git diff after a week
shows exactly what pi changed on its own.
~/.pi/agent/settings.json is a symlink to the file above, so anything that
replaces it rather than writing into it — jq … > tmp && mv tmp ~/.pi/agent/settings.json is the easy way to get this wrong — swaps the link
for a regular file. Nothing complains: pi reads the new file happily, and the
edit appears to have worked. Then bootstrap:pi finds a real file where a link
belongs, moves it aside as settings.json.bak, and relinks — and every change
made since is in the .bak, not in effect. The tell is a session that comes
back missing packages you know you installed.
Write through the link:
jq '…' ~/.pi/agent/settings.json > /tmp/s.json && cat /tmp/s.json > ~/.pi/agent/settings.jsonBetter, edit settings.<host>.json here and commit it — the change is then on
every host rather than one. pi install and pi remove are safe either way:
they rewrite in place, so their edits land in the tracked copy.
The same holds for every other file bootstrap:pi links — keybindings.json,
models.json, approval-guardian.json and prompts/. Each one has a
bootstrap step that will quietly restore the link over whatever replaced it.
Three things now say so out loud, because the gap between the mistake and the symptom is what made it expensive:
bin/dotfiles-link-check ~/.pi # or with no argument, the whole tree
bin/pi-profile-check --links-only # the same audit, scoped to the profilesbin/pi-launchtests three managed paths on every launch — three shell builtins, no subprocess — and prints a warning before starting pi. A clobber is announced at the next session rather than at the next bootstrap.- The
pre-dotfileshook runs the audit before mise relinks, so the paths it is about to rename are named while their contents still matter. - The audit also catches the quieter variants: a link left pointing at another
host's
settings.<host>.jsonafter a rename, and a dangling one.
The pia profile was a second agent directory at ~/.pi/assistant, linked by
bootstrap:pi-assistant from a private source and launched by a pia() shell
function that also synced enabledModels between the two settings files. It
was retired in favor of assistant work running in this profile: the
/assistant prompt template (in the private prompts directory) loads the
assistant skills on demand and carries the confirm-before-send rules.
bin/pi-profile-check audits the profile's packages and links, delegating the
link audit to bin/dotfiles-link-check. Run it by hand after changing the
profile:
bin/pi-profile-check # packages and links
bin/pi-profile-check --links-only
bin/pi-profile-check --packages-onlyThe list of managed paths is read out of mise.toml's dotfiles table rather
than repeated in the checker, so a link added there is audited without touching
the script — including the symlink-each directories, whose contents are the
links. The links the bootstrap tasks make themselves are the exception and are
named in dotfiles-link-check: the two per-host ones (settings.json,
models.json, whose host it resolves the same way bootstrap:pi does), and
the VA context file.
bin/pi-ext-check # typecheck + smoke tests
bin/pi-ext-check --typecheck-only
bin/pi-ext-check --test-onlyThe extensions are typechecked against the globally installed pi rather than a
vendored dependency: pi-ext-check symlinks .pi-agent/node_modules to that
install (@earendil-works/pi-coding-agent, pi-tui, typebox, @types/node)
and runs tsc from npx. tsconfig.json sets erasableSyntaxOnly, because pi
loads .ts extensions through Node's type stripping — syntax that needs real
compilation (parameter properties, enums, namespaces) fails at load time
otherwise.
A pre-push hook runs the same check automatically. It is wired up globally in
.gitconfig as a config hook (git 2.36+ [hook "pi-extensions"]) and scopes
itself by exiting silently in any repo without a bin/pi-ext-check, so pushes
elsewhere are unaffected. Two details worth knowing:
- It checks a detached worktree built from the pushed sha, never the working
tree. Several agent sessions share this clone, so the tree usually holds
someone else's half-finished file; the commit that lands is what has to pass.
This is also why it does not use
git stash, which mutates shared state. - It only runs when the pushed commits touch
.pi-agent/(0.2s otherwise, ~15s when it does), and it lets the push through with a warning on a machine with no globally installed pi, where the check cannot run at all.
tsconfig.json, package.json and test/ deliberately sit beside
extensions/ rather than inside it: mise links that directory as a whole to
~/.pi/agent/extensions, so anything in it becomes something pi tries to load.
A whole-directory link also makes extensions added by a later git pull appear
immediately; the old symlink-each layout required another bootstrap run for
every new file. During that one-time migration, the pre-dotfiles hook preserves
the previous directory as ~/.pi/agent/extensions.symlink-each.bak.
extensions/bg.ts adds background: true to the Bash tool and automatically
backgrounds commands that exceed the foreground budget without an explicit
timeout. Explicit foreground timeouts remain hard deadlines.
Running background jobs appear in a compact widget above the prompt, not in the footer:
● 2 background jobs · rspec 1m12s · vite 12s · /bg
The widget updates elapsed times without waking the model, shows up to three
jobs plus an overflow count, and disappears when none remain. Foreground jobs
stay hidden unless they are moved to the background. /bg (or Ctrl+Shift+B in
a compatible terminal) opens the job list and live logs; x stops a job.
There are no periodic model wakeups. Completion still sends the exit status
and log tail, controlled by PI_BG_WAKE=followUp|nextTurn|off (default
followUp). User-stopped jobs only notify the UI.
extensions/footer.ts renders the status line (dir, provider, model, git,
context/cost, boot time) plus /bypass, /boot and /footer. It also watches
pi's on-disk version: when an install lands while instances are running — via
pi update, npm i -g, anything — each running footer shows a green, right-aligned
"Update installed v0.52.1 → v0.53.0 · Restart to update" line above the prompt
within ~30s, like Claude Code.
The same detection kicks off bin/pi-bundle in a detached background process,
since an update leaves the bundle stale and every launch ~115ms slower until it
is rebuilt. Concurrent pi instances serialize on a lock directory; output lands
in ~/.pi/agent/auto-bundle.log. Set PI_NO_AUTO_BUNDLE=1 to opt out.
The model chip names the upstream provider OpenRouter picked: or novita/oxa,
or z/oxa, or modal/oxa. OpenRouter reports the decision in
openrouter_metadata, opted into per request with X-OpenRouter-Metadata: enabled, and delivers it in the response body — the last SSE chunk before
[DONE]. pi hands extensions only status and headers
(after_provider_response), so extensions/openrouter-route.ts wraps
globalThis.fetch instead: pi-ai builds its OpenAI client per request and
resolves fetch through the SDK's getDefaultFetch(), which reads the current
global. The wrapper adds the header, streams the body through a pass-through
transform that watches for the metadata line, and stashes the selected provider
for the footer. Non-OpenRouter requests, error responses and empty bodies are
handed back untouched.
The documented alternative costs an API call per turn: pi records OpenRouter's
generation id on the assistant message as responseId, and
GET /api/v1/generation?id= reports provider_name. Cache hits never carry
routing data either way, so the chip keeps the last known route for the model.
bin/pi-launch also points node's V8 compile cache at ~/.cache/pi/v8, worth
another ~75ms. Compiling the bundle is the largest single thing pi does before
main() — 8.7MB in one file — and it produces the same bytes every launch:
| phase | cost |
|---|---|
| node itself | 30ms |
| compiling the bundle | ~320ms |
createAgentSessionRuntime |
131ms |
interactiveMode.init |
161ms |
| all 21 extensions | 45ms |
Node namespaces the cache by version, arch and build id and keys entries by
source hash, so an upgrade misses rather than running stale code, and
pi-bundle clears it on each rebuild so it does not grow by ~1.4MB per
release. PI_NO_COMPILE_CACHE=1 opts out; pi-bundle --status shows its size.
Interleaved A/B on one machine, best of 6 launches each:
| entrypoint | boot |
|---|---|
stock dist/cli.js |
693ms |
| bundle only | 590ms |
| bundle + compile cache | 528ms |
bootstrap:pi also symlinks the system fd and rg into ~/.pi/agent/bin.
pi probes for both with spawnSync(tool, ["--version"]) on every TUI launch and
downloads its own copies if they are missing, but getToolPath() checks that
directory first — so seeding it turns three spawns into two existsSync hits,
worth ~17ms.
What is left, from node --cpu-prof over a 552ms launch (default sampling
interval; --cpu-prof-interval 100 inflates blocking syscalls badly enough to
report a 339ms spawnSync that is really 17ms):
| cost | |
|---|---|
| evaluating the bundle | ~149ms |
| idle, waiting on I/O | ~106ms |
!security find-generic-password for the Copilot key in auth.json |
24ms |
| compiling the extensions | 22ms |
mergeModels over models.<host>.json |
14ms |
probeTmuxHyperlinks |
14ms |
| grapheme width measurement | 12ms |
| GC | 11ms |
Nothing below the top two is worth chasing, and both belong to pi rather than to anything configured here.
Every cold start is appended to boot-times.jsonl with the gap back to the
previous launch and the 1-minute load average, and /boot stats reports the
two cohorts separately. This is not decoration: relaunching pi a few times in a
row boots ~350ms faster than a one-off launch, purely from a warm page cache
and an idle machine. Measured on one machine, same commit, minutes apart:
| launch | gap since previous | boot |
|---|---|---|
| one-off | 743s | 972ms |
| relaunch | 13s | 600ms |
| relaunch | 6s | 607ms |
That spread is wider than most changes worth measuring, so an undivided p50 mostly reports how you happened to be using pi that day — and a benchmark burst sitting next to real launches reads as a regression that was never there. Compare cohort to cohort.
extensions/aa-info.ts prints one dim status line into the chat whenever a
model is selected (startup, /model, Ctrl+P) and lets it scroll away with the
conversation:
Claude Opus 5 — int 62.5 · cod 77 · 53t/s · $10/1M · $1.80/task (AA)
Grok 4.6 — int 60 · cod 75.9 · 60t/s · $3/1M · $0.78/task@med (AA)
It lands in place of pi's own Switched to … line, because pi overwrites its
last status line rather than appending; models only cycled past stay quiet.
int, cod, t/s, and $/1M use AA's row for pi's current thinking level,
falling back to AA's bare/max row only when that effort has no row. $/1M is
the sticker rate (every model has one); $/task is what one Intelligence Index
task cost AA to run, which folds in how many tokens the model burns thinking.
AA only measures one or two effort levels per model for the task-cost endpoint,
so a trailing @med marks a task cost measured at a different effort than the
session runs — it swings ~4x across the ladder. Latency is omitted on purpose:
AA's own site and API disagree about it by more than 2x for the same variant.
Data comes from two free Artificial Analysis endpoints (data/llms/models for
quality, speed and rate; language/models/free for $/task), fetched once a
week and cached together in ~/.pi/agent/cache/aa-models.json. The cost endpoint
declares its Intelligence Index version (intelligence_index_version,
major.minor); the cache keeps it and the briefing shows it as (AA v4.3),
falling back to plain (AA) for caches written before versions were kept. The
quality endpoint declares no version and the API serves current scores only, so
a past index can't be pinned — when the site and the briefing disagree, the tag
says which index the briefing's numbers belong to. The fetch is
fire-and-forget — neither startup nor the model switch waits on it — and a
model the API does not know (local oMLX weights) or a failed fetch shows
nothing. The key resolves from $ARTIFICIAL_ANALYSIS_API_KEY, then fnox get
(Keychain), like the web providers; nothing touches the LLM context.
extensions/color.ts adds /color, Claude Code's trick for telling four
identical panes apart:
/color # picker
/color blue # red orange yellow green cyan blue purple pink gray
/color #ff0088 # any hex, long or short (#f08)
/color 204 # xterm palette index; bare digits beat hex shorthand
/color auto # derived from the session name, stable across /reload
/color list # the palette, swatched
/color off # back to the theme
It recolors the editor border only, by cloning the live theme with the seven
thinking-level tokens overwritten — nothing else in pi reads those, so the
transcript, tools and syntax colors stay exactly as the theme author wrote
them. bashMode is left alone, so ! still flips the border to its own color.
The footer paints the session name in the same color, but only a name you set
with /name: a name pi-claude-link derived for the peer registry stays dim,
since it says "nobody named this" and a bright color would claim otherwise.
Like Claude's, the choice is not persisted: it lives in the process (through
/reload, via a globalThis stash) and dies with it.
Two side effects of handing pi a theme instance instead of a name, both
cleared by /color off: the theme file watcher stops, so editing the active
custom theme's JSON no longer hot-reloads, and light/dark auto-switching
stops following the terminal. Picking a theme in /settings drops the tint;
the next /color re-tints from whatever is current.
Every reply ends with a numbered "Next steps:" block, so extensions/next-steps.ts
makes the number itself the command:
/2 # puts step 2 in the editor, verbatim
/13 # step 1, then AND, then step 3
/31 # same two steps, in the order asked for
/2 but ssh # step 2 with an extra instruction appended
It expands rather than sends: the step lands in the editor as ordinary text, to
be trimmed, argued with, or abandoned with Ctrl+C, and Enter sends it like
anything else. Tab on the completion does the same thing one keystroke earlier,
and it works mid-prompt too — write it up, then /2 + Tab swaps the token in
place and leaves the sentence around it alone, on any line, at any depth. A
slash only counts when it opens a word, so 1/2 and src/2 are still a
fraction and a path.
Any digit string in any order works, de-duplicated left to right. The steps are unwrapped back into one paragraph each — the line breaks in a reply are the terminal's width, not the instruction — and the sentence after the list ("which one do you want?") stays out of it. Steps come from the newest reply on the branch that actually has a numbered list, looking back at most three, so a one-line answer in between does not lose the menu; reaching back says so.
It hangs off the input event rather than pi.registerCommand, because the
commands would have to be registered for every permutation (15 for a three-step
list) before any reply exists to number. The trade is discovery — extension
commands appear in the / menu and this does not — so it adds an autocomplete
provider that lists the steps with their own text as the description: /1 then
offers /12 and /13.
One sharp edge is load-bearing there. pi's editor applies the highlighted
completion when Enter is pressed and then submits it — but only when the
autocomplete prefix starts with a slash, which is what makes Enter on /mod run
/model. Reporting the prefix as 13 instead of /13 opts out of that
fall-through, so Enter expands and stops. It also fixes pi's highlight, which
matches the prefix against item values and so never matched anything while the
slash was still attached.
Mid-prompt behaves slightly differently, and for the same kind of reason. pi
only auto-opens the popup for a slash in column zero of line one, so there is
no menu as you type; and it applies a forced completion (any Tab outside a
start-of-line slash command) without drawing one when exactly one item comes
back. So mid-prompt the provider returns only the exact selection: one Tab,
expanded, no menu. The combo entries stay a start-of-line affordance — type
/12 mid-sentence and it expands both. Two smaller edges: pi's built-in
provider vetoes forced completion for a line that is only a slash command,
which would kill Tab on a /2 alone on line two, so the provider overrides
shouldTriggerFileCompletion for its own invocations; and the input backstop
stays anchored to the whole message, so …and /2 submitted without Tab reaches
the model as typed rather than being rewritten out from under it.
extensions/ask.ts registers one ask tool, the pi equivalent of Claude's
AskUserQuestion: a single question with 2–4 options, optional one-line
descriptions, and a Type something. row that opens an input dialog for a free
answer. multiSelect: true turns the rows into checkboxes (space/click
toggles, a toggles all, enter submits with a live count); questions: [...]
asks several questions sequentially in one call and keeps the transcript so far
if one is cancelled midway.
Single-select reuses pi's SelectList, which already handles mouse
press/click/wheel; the multi-select dialog renders its own rows and hit-tests
clicks zone-style, click-only so transcript drag-select keeps working (the same
trade next-steps chips make). RPC mode falls back to the dialog protocol — a
single select, or one input round-trip of comma-separated numbers for
multi — and print/JSON mode gets an error instead of a hang. The schema stays
small on purpose (one tool, short description) and the extension does no work
at session start, so the boot and per-turn cost is near zero.
Pure helpers (buildItems, parseMultiPicks, formatAnswerLines) are
exported for test/ask.test.mjs; the dialogs themselves need a terminal.
extensions/web-chat.ts is an optional loader for the bridge maintained in
~/Code/github.com/ericboehs/psst-web/bridge/. It does not copy the bridge,
start the web server, read session history, or submit a prompt on load.
Once pi emits session_start, the bridge automatically connects saved local
TUI sessions. New sessions are checked again after an agent run, once saved. Machines without that checkout stay quiet until /web-chat is used.
The existing ~/.pi/agent/extensions directory link makes the loader available
without editing per-host settings. Restart pi to load the updated native bridge
implementation; eligible sessions then connect without a command. Reload http://127.0.0.1:8900/discovery and open that session. The separate
psst-web server must already be running with discovery explicitly enabled.
/web-chat off disconnects and pauses auto-connect for this pi process,
including reloads and session switches. /web-chat on enables it again;
restarting pi restores the automatic default. No preference file is written.
Reload/session replacement closes the old socket before connecting the new
eligible session; shutdown closes it. Restart pi after changing the bridge's
.mjs implementation, since native dependencies may remain cached on reload.
Do not also pass pi -e .../bridge/index.ts: that would load the bridge twice.
Browser Send uses that session's existing tools and permissions. Busy sessions
queue follow-ups; permission dialogs stay in pi. Only canonical saved sessions
under ~/.pi/agent/sessions are supported, not RPC or remote sessions. Selected
text/tool results may contain private data; the companion is local, ephemeral,
and not a secret-redaction layer. Auto-connection only makes the session
available locally; it never starts/resumes an agent or submits a prompt.
node --test .pi-agent/test/web-chat.test.mjsextensions/image-preview.ts makes read show pictures inline under tmux. pi
deliberately disables inline images inside tmux (images: null in
terminal-image.js), and tmux strips the raw Kitty APC sequences a terminal
would need unless they are DCS-wrapped; the upstream attempt is still gated
behind PI_TMUX_IMAGES (earendil-works/pi#2374).
The extension renders in userspace instead: chafa encodes the image with
--format=kitty --passthrough=tmux, which wraps each Kitty command in
\x1bPtmux;…\x1b\\ and uses U=1 Unicode placeholders, so the terminal gets
real pixels anchored to text cells (the yazi/ranger approach) instead of
character art.
pi-tui rewrites every row of the alt-screen viewport whenever an image line
changes, so every editor growth or autocomplete pop-up used to re-send chafa's
raw RGBA transmission — 791 KB collapsed, 4.8 MB expanded for a screenshot —
and typing became unusable. The fix is to transmit once per rendered size and
then render only the placeholder rows: after the first frame the DCS prefix is
stripped (lastIndexOf("\x1b\\")) and steady-state frames are ~330 bytes, so
shifts and scrolling cost the same as text. Re-transmission happens only when
the width or expand state changes, which is when a new encoding is needed
anyway.
The fallback chain is pixels → braille → pi's built-in text renderer. The
braille pass uses --symbols=half+braille rather than chafa's all set, whose
legacy-computing glyphs many fonts lack; every line was checked against
pi-tui's visibleWidth so it matches the cell count chafa targeted. It only
takes over when tmux and a Kitty-capable outer terminal (Ghostty, Kitty,
WezTerm, Warp) are detected and pi has no native image support; outside tmux pi
renders images itself. PI_INLINE_KITTY_IMAGES=0 forces the braille path.
/preview [path] opens the last-read image (or a path) in a tmux display-popup at 90% × 90%, using the same chafa kitty+passthrough encoding
for a full-size view; any key dismisses it. Outside tmux it hands the path to
open (or xdg-open).
One-shot transmission has one visible cost: rows rendered while off-screen
never sent their transmission, so images from a restored session can be blank
until the file is read again or viewed with /preview. Boot cost is a single
small module with no work at load; chafa is spawned lazily and cached per
width/expansion.
/artifact builds one self-contained HTML page and publishes it to a shareable
URL — Claude's Artifacts "Publish" button, reproduced with a prompt template and
two scripts.
/artifact a dashboard for my solar production data
Skills announce themselves in the system prompt on every turn; a name and
description sit in context forever whether or not they are ever used. Prompt
templates cost nothing until typed. Building an artifact is always a deliberate
act — never something the model should decide to start on its own — so
prompts/artifact.md is the right shape: ~940 tokens that are free until
invoked, and which then pull in one skill file for ~3,000 tokens on a typical
run.
artifact-skills/ holds Anthropic's own design skills, copied verbatim from
anthropics/skills (Apache 2.0) and
pinned by commit in MANIFEST, and every file by sha256 as well. Only SKILL.md
and LICENSE.txt are taken; the upstream scripts and assets are not.
| Skill | Tokens | Read when |
|---|---|---|
frontend-design |
~2,060 | always — the core methodology |
web-artifacts-builder |
~770 | React, state, shadcn |
algorithmic-art |
~4,940 | generative visuals, p5.js |
canvas-design |
~2,980 | static poster or PDF |
brand-guidelines |
~560 | Anthropic brand |
artifact.md names that table and tells the agent which file to read, so a
plain data dashboard never loads the 4,940-token art skill. Pinning matters for
the same reason it does for packages: upstream edits should not silently change
what the prompt does.
bin/artifact-skills-sync # sync to the pinned commit
bin/artifact-skills-sync --verify # files match MANIFEST, and git carries them?
bin/artifact-skills-sync --check # has upstream moved?
bin/artifact-skills-sync --update # repin, then review the diff
bin/artifact-skills-sync --list # token estimates--verify is the one CI runs, and it touches nothing but the working tree: MANIFEST
records the sha256 of every vendored file, so holding the pin needs no network and no
gh auth. Upstream moving is a reminder to repin by hand, never a red build.
This directory is published, and .gitignore here is a deny-by-default
allowlist, so the vendored files need explicit rules to be tracked at all. --verify
checks that too — a vendored file with no ! rule works locally and is simply
missing in CI, which is the one failure a local run otherwise cannot predict.
Do not hand-write a design system for this. The first version did — fixed
:root tokens plus six named layout archetypes — and every page it produced
looked the same, because a fixed token set is a house style with extra steps.
Worse, it landed on a warm cream background with a serif display face and a
terracotta accent, which is the first of the three AI-default clusters
frontend-design calls out by name. The real skill inverts the approach: name
the subject and the page's single job, invent a bespoke palette and type scale
per brief, then critique that plan against a generic answer to the same prompt
before writing any code.
The test that it is working is that two artifacts from the same pipeline share no palette, typeface, hero pattern or layout axis.
Rendering and looking at the result caught ten defects across the first two
artifacts that were invisible in the source, two of them CSS specificity bugs
of exactly the kind the skill warns about: a font: shorthand on a parent
silently overriding a child rule that set only size and weight, and a CSS
fill rule beating an SVG fill attribute. Both produced valid, error-free
pages that were simply wrong.
Playwright cannot load file:// on macOS, so serve the directory first:
python3 -m http.server 8899
playwright-cli -s=art open http://localhost:8899/page.htmlCheck 375 / 768 / 1440, both color schemes, and every interactive state including the empty one.
pub report.html # publish; URL printed and copied
pub -u <gist-id> file.html # revise in place, URL unchanged
pub -l # list
pub -r <gist-id> # deletebin/pub writes the file to a secret gist as index.html and hands back a
gistpreview.github.io/?<id> URL. Transport limits worth knowing: secret gists
are unlisted, not private; the renderer is third-party and volunteer-run
(bl.ocks.org, the same idea, is dead); there is no control over CSP, so a
page could beacon data out; and there is no versioning or expiry. artifact.md
therefore ends with an explicit check for secrets, tokens, internal hostnames,
PII and client-internal material before anything is published — anything that
fails it stays local.
Moved out to its own package: pi-scheduler,
installed from packages in the per-host settings. /once and /loop are
session timers that fire a prompt into the current conversation; /schedule
plus the pi-scheduler CLI are durable tasks that run in their own pi -p
whether or not pi is open. Neither registers an LLM tool or adds anything to
model context. The full reference lives in that repo's README.
bin/pi-scheduler here is a two-line shim: ~/bin is a symlink to bin/, so
the shim is what keeps the command on PATH on every host without each one
needing a hand-made symlink into ~/.pi/agent/git. It execs the package's
CLI rather than wrapping it, because pi-scheduler install bakes an absolute
path into the launchd job and that path must be the package's, not the shim's.
The registry stays machine-local at ~/.pi/agent/scheduler/ — see below.
Intentionally left as machine-local runtime state:
auth.jsonand other credentialssessions/- downloaded
npm/andgit/packages - generated model catalog and cache files
models.json, which may contain machine-specific provider configuration- trust decisions
boot-times.jsonl, the launch log behind/boot statsscheduler/, the durable task registry and run history behind/scheduleauto-bundle.logandpi-bundle.lock, written by the footer's automatic bundle rebuild~/.cache/pi/v8, the V8 compile cachebin/pi-launchpoints node at.pi-agent/node_modules/, the symlinksbin/pi-ext-checkcreatesdist/bundle.mjsinside the pi install, whichbin/pi-bundlerebuilds