extensions
Pi extension packages owned by this repository. A package README that declares itself the sole behavioral owner is normative; otherwise supported behavior stays at the package’s local decision seams. This inventory and its authoring procedures are non-normative.
The default package set loads through one absolute local-path entry in pi/agent/settings.json — pi reads the TypeScript directly (jiti), so there is no build step and no copying: this tree is the live source. Tests live in each package’s test/ directory; tests that run several packages together live in test/pi/ (write a test).
The aggregate manifest owns load order; each package manifest owns its entrypoints. Follow a package link for its behavior, configuration, and evidence.
| Package | What it owns |
|---|---|
| profiles | Named model-provider profiles: selection by flag, parent, session record, folder, or /profile; provider visibility and the default model |
| herdr-report | Herdr state, session, resume command, and release for a frontend-driven RPC session |
| user-turns | User-turn tracking, answer summaries/revision, optional root TUI/RPC completion, and the open-turn service; loads before tui-display (decision) |
| tui-display | Transcript-view draw rule, status block and status row, answer summary line, shared work store |
| tui-aside | Fork the current session into a Herdr pane |
| herdr | HerdR session titles |
| worktree | Worktree guards, lifecycle tool, and the session’s work root |
| bash | Bash-call default timeout and RTK command rewrite |
| observability | Request logging and output-rate samples |
| tui-footer | Session footer fields, width behavior, and session cost |
| relay | Owned peer messaging and the shared filesystem transport for peer and native async-task mail; loads before async-tasks |
| async-tasks | Async tasks, their results, and the service that lists them: subagents, named headless child agents, and shell tasks, including persistent jobs and output |
| goal | Durable owned Goal lifecycle, budgets, child attribution, and rollback preflight |
| ast-grep | The ast_grep tool: working-tree outline, source read, structural search and guarded rewrites |
| codebase-memory | Native CBM graph tools with work-root admission and explicit indexing |
| stall-guard | Bounded recovery from silent or premature model stops |
| compaction | Default asynchronous compaction through public checkpoints, with a configured target, native safety fallback and usage accounting |
| browser | Isolated native browser MCP, task-session guard, and explicit task close |
| supervisor | Durable project supervision, child attempts, and evidence verification |
| providers | Native provider registration: provider connections, scoped catalogs, and transport ownership |
| caffeinate | The macOS idle-sleep assertion held while a run is in flight, and its release when the session settles |
shared is not an extension package: it holds the contracts and stateless helpers that packages share. Pi does not load it, and it is not in the manifest.
For occasional TypeSafe Jev classifications, use native codemode with models.getModelOfType('classifier', 'typesafe', 'jev-latest') and models.classify(). Check result.stopReason before using answers; no local classifier extension is loaded.
Model-facing tool guidance
Section titled “Model-facing tool guidance”Keep each tool’s description, promptSnippet, and promptGuidelines together in an exported object in the package’s guidance.ts (or the local tool owner’s guidance.ts), then spread that object into registerTool. This gives reviewers one place to inspect the prose that teaches the model when and how to use the tool. Keep parameter descriptions with the parameter schema because they define individual arguments and should change with their validation.
These fields reach the model through different channels:
descriptionaccompanies the provider’s callable tool definition.promptSnippetadds the tool to Pi’s Available tools system-prompt section; keep its normalized text to one concise entry.promptGuidelinesadds selection and usage rules to Pi’s system-prompt Guidelines section while the tool is active.- Parameter descriptions accompany the provider’s parameter schema.
Use the description for capabilities and hard limitations, the snippet for quick recognition, and guidelines for actionable use/avoid rules. Do not load prose from Markdown or text files at runtime; TypeScript exports remain typechecked and loader-independent. Preserve exact wording during structural moves and exercise the registered guidance in package tests when behavior depends on it.
Every request carries these definitions. Central tool-surface verification owns measurement and eval selection. Surface tests cap tool count, total characters, and guidelines per tool. Added text must fit the caps or raise them as a deliberate decision.
A package’s model-behavior evals live in its eval/ directory: the tasks it owns, and their offline grader tests. The shared runner in evals/tool-surface/ runs them with the full Pi setup (see evals/tool-surface.md). Change a tool and its tasks in the same change.
Authoring loop
Section titled “Authoring loop”Use engineer’s proportional workflow and central verification for check selection and repository gates. Local TypeScript extension edits take effect on /reload.
Adding a package
Section titled “Adding a package”-
Create
packages/<name>/and move the entry files in. -
Add
packages/<name>/package.json:{"name": "pi-<name>","version": "0.0.0","private": true,"type": "module","keywords": ["pi-package"],"pi": { "extensions": ["./<entry>.ts"] },"peerDependencies": { "@earendil-works/pi-coding-agent": "*" }}pi.extensionslists every entrypoint in load order; shared non-entry modules (for examplepackages/worktree/worktree-tool.ts) stay out of the manifest.peerDependencieslists the host packages the code imports —@earendil-works/pi-ai,@earendil-works/pi-coding-agent,@earendil-works/pi-tui,typebox— each pinned"*". Type the pi surface withimport typefrom@earendil-works/pi-coding-agent, narrowed to the members the file reads (Pick<ExtensionAPI | ExtensionContext, …>).Pickkeeps Pi’s per-eventonoverloads, so a handler typed for the wrong event fails the typecheck.tui-display/is the exception: its adapter patches private Pi internals and restates the surface it patches, pinned by its own host probes intest/display-host.test.ts. -
Append
"./packages/<name>"to the rootpi.extensionsmanifest in intended load order, and add its README link to the inventory above. The manifest remains load-order authority. Keep the nativeknip.tsandfallow.tomlentry lists in step with it. -
Add a minor Changeset for its initial
0.1.0version and changelog. -
Use central loader smoke to check the package without ambient extensions or network access.
-
No settings change is needed: the existing entry already points at this root.
packages/shared has no entrypoints, so steps 3 and 4 do not apply to it.
Toggling one entrypoint
Section titled “Toggling one entrypoint”Turn the settings string entry into an object with filter paths relative to this package root:
{ "source": "~/code/my-pi/extensions", "extensions": ["-packages/bash/rtk.ts"]}-path force-excludes one exact entrypoint; +path force-includes one. Verified: with the filter above, pi list marks the package (filtered) and rtk.ts alone stops loading while bash-timeout.ts stays live.
Dependencies
Section titled “Dependencies”Runtime dependencies include pinned koffi in relay for descriptor-relative filesystem operations, and providers, which pins @router-for-me/pi-cliproxyapi-provider@1.4.23 for its shipped discovery and stream helpers. Add another dependency through the root Bun workspace, for example bun add <dependency> --filter pi-<name>. Pi auto-installs dependencies only for npm- and git-sourced packages, never for this local path. Host packages (pi-ai, pi-coding-agent, pi-tui, typebox) always belong in peerDependencies with "*", never bundled; the root devDependencies pin the versions the extensions are qualified against — the host release Pi runs.
Cross-package imports
Section titled “Cross-package imports”Packages import code they share from shared by relative path and do not import each other (decision); imports between packages that predate the decision are moving there. Pi loads each entrypoint with its own module cache, so an imported module is instantiated once per entry: a module imported across packages must hold no state another entry observes, and a module that needs such state must publish it through a Symbol.for registry on globalThis.
A package that resolves paths or spawns processes for the session reads the directory through workRootOf(ctx) from shared/contracts/work-root.ts, not ctx.cwd: after worktree enter, the session’s tools run in the work root while ctx.cwd stays the launch cwd. Project-scoped configuration still reads ctx.cwd. See the work root.
Extension-triggered runs
Section titled “Extension-triggered runs”Pi 1.3.0 runs before_agent_start for an idle pi.sendMessage(…, { triggerTurn: true }) message, an extension-triggered run: a bridge resume, a stall retry, or a wake from an async task’s result. The event has messageType: "custom" and a starting message, rather than a user prompt and images; handlers intended only for user prompts must check messageType === "user". The shared run setup checks compaction, delivers pending nextTurn messages and handler messages, and preserves the run’s system-prompt sections through tool follow-ups. It does not dispatch input handlers or expand prompt templates.
A pi.sendUserMessage input is different: it runs the prompt lifecycle. Owned Relay uses native custom-message wakes, not user prompts. Pi saves a run’s prompt to the branch only after agent_start, so a handler that classifies the run must read its trigger from before_agent_start, not the newest branch entry.
The session counts as busy during run setup, including before_agent_start. Messages sent then or during model/tool work queue into that run rather than starting another pre-start dispatch. Durable agent context can remain in the transcript’s messages, as the subagents roster does; request-local prompt sections are also preserved for idle extension-triggered runs.
Version history
Section titled “Version history”The root Changesets configuration versions these private packages independently. Add a Changeset for every user-visible capability or behavior change; brain, skill, test, documentation, and CI-only changes do not require one. A change in shared carries a Changeset for each package whose behavior it changes, or an empty one when it changes none; never name pi-shared. Keep the body to one or two sentences, at most 240 characters; put rationale and feature inventories in the package README and pull request. After CI passes on a push to main, its version job applies the pending Changesets, formats the result, checks it with bun run check:ci, and pushes chore: version extension packages straight to main. It skips when main has moved on, because the newer push’s run versions the same Changesets. Private versions do not create tags or publish.
Publishing (deferred)
Section titled “Publishing (deferred)”Nothing here is published. Public packages first need stable scoped names, truthful Pi peer ranges, licenses and provenance, explicit published files, compiled exports, and isolated installation smoke tests. Publication should use one root workflow rather than one workflow per package.