Skip to content

Latest commit

 

History

History

README.md

Pi configuration

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

Per-host settings

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.

Edit through the link, never over it

~/.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.json

Better, 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 profiles
  • bin/pi-launch tests 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-dotfiles hook 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>.json after a rename, and a dangling one.

Assistant profile (retired)

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-only

The 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.

Checking the extensions

bin/pi-ext-check                  # typecheck + smoke tests
bin/pi-ext-check --typecheck-only
bin/pi-ext-check --test-only

The 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.

Background jobs

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.

Footer

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.

OpenRouter route chip

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.

Model briefings

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.

Session color

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.

Picking a next step

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.

Asking the user

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.

Local web chat

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.mjs

Inline images

extensions/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.

Artifacts

/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

A prompt template, not a skill

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.

The design guidance is vendored, not written

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.

Screenshot it before publishing

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.html

Check 375 / 768 / 1440, both color schemes, and every interactive state including the empty one.

Publishing

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>             # delete

bin/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.

Schedulers

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.json and other credentials
  • sessions/
  • downloaded npm/ and git/ 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 stats
  • scheduler/, the durable task registry and run history behind /schedule
  • auto-bundle.log and pi-bundle.lock, written by the footer's automatic bundle rebuild
  • ~/.cache/pi/v8, the V8 compile cache bin/pi-launch points node at
  • .pi-agent/node_modules/, the symlinks bin/pi-ext-check creates
  • dist/bundle.mjs inside the pi install, which bin/pi-bundle rebuilds