Global configuration for pi coding agent at @earendil-works/pi-coding-agent@0.80.3.
Symlinks in ~/.pi/agent/ point into this repo:
ln -sf ~/Developer/pi-config/APPEND_SYSTEM.md ~/.pi/agent/APPEND_SYSTEM.md
ln -sf ~/Developer/pi-config/extensions ~/.pi/agent/extensions
ln -sf ~/Developer/pi-config/settings.json ~/.pi/agent/settings.json
ln -sf ~/Developer/pi-config/skills ~/.pi/agent/skills
ln -sf ~/Developer/pi-config/themes ~/.pi/agent/themes
ln -sf ~/Developer/pi-config/models.json ~/.pi/agent/models.json
ln -sf ~/Developer/pi-config/rules ~/.pi/agent/rules # whole dir (first time: rm -rf the existing ~/.pi/agent/rules dir first)APPEND_SYSTEM.md— Instructions appended to every system prompt (doc/decision/testing discipline, behavioral constraints, landing work, subagent guidance). Placement rule: repo-independent process rules live here; a project's ownAGENTS.mdcarries only what is specific to that project (decisions/settings/002). Seeextensions/append-system-local.tsfor an optional personal/local tail that is not committed.settings.json— Global settings (default provider/model, thinking level, extensions)models.json— Custom provider override — two OpenRouter first-party correctness pins withallow_fallbacks: false(deepseek/deepseek-v4.1-flash-1017→ DeepSeek (the -1017 checkpoint, reachable for manual main-session use; decisions 022/035 — fleet traffic itself moved to OpenCode Go, decision 036) andz-ai/glm-5.3-flash→ Z.AI (the three review seats since 2026-10-22)) plus one preference pin with fallbacks allowed (xiaomi/mimo-v2.6-pro→ Xiaomi first — the owner's main-session seat, decision 042; no fp4 endpoint exists on that slug, so a fallback cannot land on a re-quantization). The fidelity pins' slugs are served by resellers starting at fp4/unknown quantizations, which are not the measured model — the pin is the fidelity guard (decision 022), not a price guard:z-ai/glm-5.3-flashhas 34 endpoints from $0.0113/M and the pinned first-party rate is $0.15/$0.50/$0.03 per M (decisions 022/041).extensions/checkpoint.ts— Archive-and-compact on demand; archives stored under.pi/checkpoints/extensions/baba— A tracked symlink, not a directory: mode120000pointing at/Users/scott/Developer/baba-pi(pcone/baba-pi). The extension's source, its CLI, its MCP server and their bun tests live in that repo, so nothing under this path is editable here and a path likeextensions/baba/src/index.tsnames another repo's tree. One client,baba-pi/src/client.ts, serves three entry points: the pi extension (semantic_search),bin/baba-search.ts(one query from a shell), andbin/baba-mcp.ts(a stdio MCP server registered user-scope in Claude Code, decisionexternal-tools/009).extensions/lib/waits.ts— The session's pending waits (decisioncheckpoint/003): the manual snapshot persisted on the session branch, the derived sources thatsubagent-async(a running child) andpeer-link(an unansweredexpectReply) register, the continuation rulecheckpoint/checkpoint_forkshare, the kickoff prompt that carries a wait forward, and the surface itself — thewait_fortool and the announcement a restart owes a waiting seat (registerWaitSurface). Design:docs/design/checkpoint-waits.md(plain module — underlib/so the extension loader's*.tsauto-discovery skips it)extensions/lib/read-tracking.ts— Read-window tracking shared by the checkpoint extensions: path normalization, window merging, per-file{offset, limit, mtimeMs, size}regions (the staleness guard's input), and the preserved-tail scan (plain module — underlib/so the extension loader's*.tsauto-discovery skips it)extensions/lib/pdf-convert.ts— PDF detection, conversion (pymupdf4llm → pdftotext), temp-file helpers, and response builders shared by fetch-url and pdf-read-guard (plain module — underlib/so the extension loader's*.tsauto-discovery skips it)extensions/lib/fleet-model.ts— The fleet constants:FLEET_PROVIDER = "opencode-go",FLEET_MODEL = "deepseek-v4.1-flash", and the one provider-qualified specFLEET_MODEL_SPEC = `${FLEET_PROVIDER}/${FLEET_MODEL}`. The spec is what both consumers pass: the--modelfor spawns whose agent has nomodel:frontmatter (fallback and resume arms), and the model compaction resolves — a bare id would resolve against whatever provider the session defaults to and put traffic on unpinned routes (owner ruling 2026-10-22). Shared by subagent-async and compaction (OpenRouter-only under 022; the fleet moved to the OpenCode Go plan quota under 036, 2026-10-18; the/fleet-modelcommand and~/.pi/fleet-model.jsonoverride were deleted with the toggle in 022). Long-lived sessions hold extension code in memory, so a change here reaches a running session only on restart or/reload— sessions spawned before the edit still run the old model.extensions/lib/quota.ts— The footer's provider quota mechanism: one descriptor per provider, one fetch path, one warn-once, one segment renderer over normalized parts (label, used percent, reset instant, %-through, width-ladder drop order). Providers: z.ai, MiniMax (two hosts), OpenCode Go, Kimi. Plain module — underlib/so the extension loader's*.tsauto-discovery skips it. Design:docs/design/quota-footer.md; the pre-refactor byte goldens:tests/footer-quota-characterisation.test.ts.extensions/pdf-read-guard.ts— Interceptsreadon.pdffiles; replaces raw binary content with converted Markdown (never raw bytes in context)extensions/subagent-async/index.ts— Non-blocking subagents via RPC mode: spawn, check progress (/subagents), steer, stop. Subagents fork from HEAD (not working tree) — commit first. Includes live log viewer (/watch), external viewer (watch-session). The printed session id is the subagent's real pi session id (spawned with--session-id), so it is searchable in/resumeand joinable from a terminal viapi --session <id>. It is also the subagent's peer-link identity: every subagent heartbeats into the peer mailbox assubagent-<id>, so you canpeer_sendit a message (injected as a user message) and its reply comes back automatically.PI_PEER_NAMEis always set to the session id — an inherited value would make sibling subagents collide on one mailbox identity. Children get the shared global prompt (APPEND_SYSTEM.md) explicitly via an extra--append-system-prompt, because passing the agent body suppresses pi's own append-system-prompt discovery (decisions/subagents/025-global-append-for-subagents.md).extensions/subagent/— (disabled) Original synchronous subagent extension, kept for reference.extensions/footer-session-id.ts— Replaces the footer with one that adds a themed, reversible identifier (e.g.arcane-phoenix-archmage) for the current session on the right side. The phrase is bijective with the first 4 hex chars of the UUID session ID — look up the words in the lists to recover the prefix. The full session UUID itself is also shown greyed-out to the left of the words (the only place the raw id appears in the footer; dropped first on narrow terminals). Each session also gets a per-session hue (derived from the same bits) and a staleness indicator (●◐◌○) that tracks time since the most recent entry — the words themselves fade along the same axis, so freshness reads at a glance. Row 3's right side carries the provider quota/usage segment — z.ai, MiniMax, OpenCode Go and Kimi (kimi-coding): percent, %-through-window where the window length is known, time to reset (decisions/external-tools/006, 008; mechanism and endpoints:docs/design/quota-footer.md). The mechanism itself isextensions/lib/quota.ts— one descriptor per provider ({ provider, endpoint, auth, parse, render }), one fetch path, one renderer; a new provider is a descriptor entry.extensions/session-name.ts— Reverse lookup: resolves a friendly name back to a full session id (session_by_nametool,/session-name <name>command). The words encode only 18 of the UUID's 128 bits, so the name is a fingerprint, not an identity; when several sessions collide on one name and exactly one was active (last activity = file mtime) within the last 7 days it is assumed correct, otherwise the collision is surfaced — a list picker in TUI, the candidates as text elsewhere. Non-UUID ids (subagents/reviewers) all map to the sentinelwandering-???-seekerand resolve back through it.extensions/modes.ts— Toggles the parent session betweenimplement(default — parent does work directly) andorchestrate(parent conducts through subagents; may dispatchorchestratorsubagents for large parallelizable chunks — one nesting level only, harness-capped atPI_SUBAGENT_DEPTH2). Per-project state at<cwd>/.pi/mode.json. Commands:/modetoggles,/mode <implement|orchestrate>sets explicit. Footer shows current mode.extensions/pinned-sessions.ts— A curated, named shortlist of sessions you want to keep track of and relaunch easily, out of the noise of one-off sessions. Global registry at~/.pi/pinned-sessions.json(keyed by session-file path; each entry remembers its project so a pin resumes across projects after a reboot). Commands:/pin [name]pins the current session (name falls back to the session display name, then the first user-message excerpt; when given it also sets the session display name),/unpin [query]unpins the current session or a named match (tab-completes names),/pinnedopens a list picker that resumes the selection. Dead session files are skipped. Startup-summary line included.agents/implement.md— Single implementation tier:implementonopencode-go/deepseek-v4.1-flash:high(all feature work routes here; the fleet rides the OpenCode Go plan quota since 2026-10-18, decision 036). Every seat runs the same model since 2026-10-01 (decision 022 — the z.ai subscription ended): reviewers:high(all three review seats onopenrouter/z-ai/glm-5.3-flash:highsince 2026-10-22 — off OpenCode Go extra-usage billing, same model; Zhipu-lab, decorrelated from the DeepSeek implementer; decisions 037, 041), scouts:low, oracle:maxondeepseek-v4-pro(037), orchestrator tracks its caller's model at:high(031, 047), compaction:low(effort audit::mediumsilently clamped to high, and the only real discount tier islow); the/fleet-modeltoggle is gone. Single review tier since 2026-08-28 (the-deepreviewer agents were deleted).extensions/append-system-local.ts— Appends a second system-prompt fragment from~/.pi/agent/APPEND_SYSTEM.local.md(a real file, not symlinked into this repo, so it never gets committed) directly afterAPPEND_SYSTEM.md. Optional and missing-file-safe — for personal/local nudges you want to experiment with without touching shared config. Cached by mtime, so edits apply live on the next agent run with no restart.extensions/peer-link/— Two user-launched pi sessions coordinate by exchanging user messages. Sessions sharing a mailbox (~/.pi/peer-mail, or$PI_PEER_MAILBOX) send each other messages; an inbound message is injected as a real user message ([peer:<name>] …) that triggers the receiving agent, and the response can auto-flow back once per message (expectReply). Loop-safe: envelopes are consumed on read, replies never re-forward. Identity:$PI_PEER_NAME, else the session display name (--name), else the session filename, else host-pid. Commands:/peers,/peer-send [--reply] <name> <text>,/peer-autoreply [on|off]. LLM tools:peer_list,peer_send(broadcast withto: "*"; unlisted names fail loud instead of queueing into an inbox nobody reads — call peer_list first;requireOnline: truefails loud for offline peers; listed-but-offline peers queue until their next scan). Tests:tests/peer-link.test.ts(unit + SDK integration),tests/peer-link-smoke.ts(two real processes). External peers:external.tslets a Claude Code session join the mailbox through its hooks (claude-<session_id>); mail reaches it only on the user's next prompt, and nothing starts a Claude turn. Design and ToS argument:docs/design/peer-link-external.md; tests:tests/peer-link-external.test.ts. A model request that fails with an HTTP 4XX/5XX mails a peer without the model's involvement: amessage_endhandler writes an envelope to the last correspondent (the peer whose envelope this session delivered most recently, persisted under the mailbox's_state/) while that peer is online or is a registered external (Claude Code) peer, and otherwise to every peer online now; one mail per 30 minutes, carrying the suppressed count when a run of failures was collapsed (design:docs/design/model-error-mail.md, tests:tests/model-error-mail.test.ts). Asession_startthat cannot wire the runtime notifies the human and throws, so pi's runner reports it as an extension error instead of the notify being the only trace; no heartbeat and no inbox scan start.extensions/seat-sync.ts— Atsession_startin a clone's main checkout, fetch andgit merge --ff-only @{u}when the tree is clean and the branch is not ahead; otherwise warn loudly with the evidence (up to 10 dirty paths, the ahead/diverged counts, or the fetch's stderr) and a status line (seat sync: fast-forwarded N commits/seat sync: BLOCKED — N dirty files). Agit worktree(.gitis a file) and any path beneathwt/are never touched. The fetch runs in a tasksession_startdoes not await — pi awaits that handler before the first prompt — and it is the one shared checkout fetch, joined by owner-asks viaextensions/lib/git-fetch.ts, killed after 10 s. Logic:extensions/lib/seat-sync.ts.PI_SEAT_SYNC_DEBUG=1prints the exact git invocations. Design and measured cost:docs/design/seat-sync.md; decision:decisions/sessions/002-seat-checkout-sync.md; tests:tests/seat-sync.test.ts.extensions/owner-asks/— Counts the registry questions awaiting the owner (tfd'sdocs/reviews/, read from the upstream branch viascripts/reviews_status.py --ref), with anewcount since the owner last acked. pi: status bar (index.ts, refreshed on each settle and every 60 s) and/asks(list, then ack). Claude Code:external.ts statuslineas thestatusLine, pluslist/ackfor the/askscommand in~/.claude/commands/asks.md. Other repos show nothing. Tests:tests/owner-asks.test.ts.extensions/bug-triage.ts— Turns a session launched aspi --name bug-triageinto the standing bug-triage peer: a long-running intake queue for bugs inmain(ownership transfer, not delegation — each session still fixes its own WIP regressions). Injectsagents/bug-triage.mdinto the system prompt + a one-shot kick-off; Non-triage sessions run no-op handlers (a name compare). Runs in its own clone (one-clone-per-session convention) — bisects, checks out commits, builds freely; trivial fixes clear the review gate, then merge to the mainline and push toorigin. Reporters hand off viapeer_send("bug-triage", …)(see APPEND_SYSTEM); the peer files agh issue+ failing.casesand pings back only if the finding affects the caller's in-flight work, or hands back if it reproduces only on the caller's branch. Launch:pi --name bug-triagein a second terminal, then/pin bug-triage. Why a peer not a 4th mode: seedecisions/bug-triage/001-bug-triage-peer.md. Test:tests/bug-triage.test.ts.skills/explain— Plain explanation of a concept for a reader new to it: prerequisites mapped and supplied in dependency order, literal terms, analogies only with mapping and breakdown stated. Shared with Claude Code vialn -s ~/Developer/pi-config/skills/explain ~/.claude/skills/explain. Why:decisions/skills/001-explain-skill.mdskills/pdf— On-demand PDF conversion (pymupdf4llm/pdftotext) for when automatic paths failskills/review-brief— Review-brief format: per-feature briefs + batch index, ISO 24495-1 plain-language pass, chunked review units, fresh-reader comprehension gateskills/work-order-template— Work-order schema for delegating implementation tasks to subagentsrules/— Global path-scoped rules, symlinked whole-dir into~/.pi/agent/rules/(mirrors the skills/extensions pattern). The whole dir is version-controlled here:writing-rules.md— meta: how to write/format pi rules (fires on.pi/rules/**/*.md)work-order-numbering.md— reserve-then-dispatch for sequential work-order ids across concurrent sessions, push as the collision detector (fires on**/work-orders/**)
themes/catppuccin-macchiato.json— Color theme
fetch-url and kagi-search save large pages / search responses under ~/.pi/tmp/<extension>/ (72h TTL; this is NOT a cache — every call hits the network fresh). model-tiers still uses ~/.pi/cache/<extension>/ for benchmark data (24h TTL). All outside the repo (under HOME), so no .gitignore entries are needed; dirs are created on first write by the extension itself.
~/.pi/tmp/fetch-url/— large fetched pages (HTML→Markdown); 72h TTL~/.pi/tmp/pdf-convert/— PDF conversions saved when too large to inline; 72h TTL~/.pi/tmp/kagi-search/— Kagi API search responses; 72h TTL~/.pi/cache/model-tiers/— OpenRouter benchmarks and model info; 24h TTL~/.pi/peer-mail/— peer-link mailbox: one subdir per peer for envelopes,_peers/for heartbeats,_state/for per-peer non-mail bookkeeping (the last correspondent a model-error mail goes to); created on first use, 0700
subagent(agent, task, cwd?, inheritParentModel?, isolate?, carryUncommitted?, baseRef?, review_policy?, workOrderPath?, silenceTimeoutMs?)— Spawn an async subagent that runs in the background. Subagents fork from HEAD — commit any uncommitted work the subagent needs before delegating.workOrderPathmakes the referenced work order'sreview_policythe single source of truth for the review gate (decision 014).inheritParentModelruns the child on the caller's active model instead of the agent's own — an explicit per-spawn choice (decision 020); theorchestratoragent always inherits the caller's model and keeps its own:levelsuffix regardless of the flag (decision 031).silenceTimeoutMsauto-kills a child that makes no progress (no tool calls and no assistant messages) for that many milliseconds — default 30 min,0/negative disables the auto-kill (decision 015; the session stays resumable viasubagent_resume). Use/subagentsto check progress,/watch <id>for live output.subagent_status(session_id)— Check progress of a running async subagentsubagent_steer(session_id, message)— Inject a steering message into a running subagentsubagent_stop(session_id, final_message?)— Tell a running subagent to wrap up and returnsubagent_kill(session_id)— Hard-kill a stuck subagent by terminating its underlyingpiprocess (SIGTERM, then SIGKILL after 5s). Use whensubagent_stopcannot recover the child (RPC stdin not being read, deadlocked, infinite loop, blocked provider call). Result is delivered as a user message with a[Killed via subagent_kill ...]marker. Destructive — prefersubagent_stopwhen the subagent is responsive.checkpoint(summary{goal,filesTouched,decisions,triedAndFailed,constraints}, nextSteps?, continue?, relevantPaths?)— Archive the current session to.pi/checkpoints/session-<id>-<timestamp>.jsonl(+.meta.json, thinking stripped), override compaction with the supplied summary, and continue withnextStepsas the kickoff prompt (continueunset continues only when no wait is pending;true/falseoverride — seewait_for).summaryis a fixed schema, every field required:goal,filesTouched,decisions,triedAndFailed(each entry names an approach tried, what happened, why it failed — the fresh window retries what it is not told about),constraints; empty means the literal"none"/["none"], never an omitted field or[], and a checkpoint with neither a decision nor a failed approach is rejected (nothing to resume) — every violation comes back in one tool error and lands nothing. One renderer turns the fields into the sectioned text the compaction window, the meta and the fork message all receive, withnextStepsas itsNext:slot. The compaction's stored summary is that text verbatim followed by a machine block of facts only tooling knows: the git ref (branch, short HEAD, clean/dirty, ahead-behind from the upstream), the files read and modified merged across preceding checkpoints, the archive path with itsrghint, the injected file block, and tokens before →after: unknown(pi estimates the after count only once the entry exists; the notify and the meta carry it). The meta carries the structured fields, the authored summary, the context tokens before compaction and pi's estimate after it, and the git binding (git: nulloutside a repo — a checkpoint never fails or waits on git).relevantPathsentries are re-read from disk after compaction: only the line ranges actually read this session are injected, behind a one-line header saying it is a checkpoint-time snapshot of ranges chosen from an earlier read, and every requested path is named in the block — already in the preserved tail, over budget, changed since the read (re-read instead of a stale window), or never read through the read tool (re-read after compaction). Because the block rides the summary,continue: falsekeeps the file context too. Compaction fires at the turn's end, never inside the tool call.wait_for(reason?, kind?, clear?, id?)— Record what the session is waiting for (owner,peer,restart,subagent,other). A pending wait stops the next checkpoint from continuing on its own;wait_for({ clear: true, id? })drops it. Running subagents and unansweredexpectReplysends are waits automatically, derived by their own extensions; this tool records what nothing else can see. Mail does not clear a manual wait; a peer wait ends on any envelope from that peer, not just a reply. Persisted as a full snapshot entry on the session branch, so it survives compaction and a restart, and the first turn after a restart opens withWAITING ON: …. Design:docs/design/checkpoint-waits.md; decision:decisions/checkpoint/003; tests:tests/checkpoint-waits.test.ts.checkpoint_fork(newCwd, summary{…same schema…}, nextSteps?, continue?, relevantPaths?)— Stage a fork intonewCwd(must exist; the switch happens on the next/go): archive the source session (meta binds the source cwd's git ref and recordsnewCwd), create a session file in the target cwd with the rendered summary as its initial context, and injectrelevantPaths(read from the source cwd) into the new session. A pending manual wait travels with it, written as acheckpoint-waitentry on the new session's branch (decisioncheckpoint/003, ruling 4); derived waits do not, since each belongs to a process and a source. Nothing is written to the old session.bin/heavy -- <cmd> [args…]— Run a command holding one ofHEAVY_SLOTS(default 1) machine-wide slots under~/.cache/heavy-slots/, waiting while all are held (decisionexternal-tools/010).bin/heavy_slot.pyis the sameslot()as an importable module.
auth.json— API keys (stored in macOS Keychain via~/.pi/agent/auth.json)bin/— Binary tools (rg, fd) — install separatelysessions/— Session history.pi/checkpoints/— Per-project checkpoint archives