Skip to content

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.

PackageWhat it owns
profilesNamed model-provider profiles: selection by flag, parent, session record, folder, or /profile; provider visibility and the default model
herdr-reportHerdr state, session, resume command, and release for a frontend-driven RPC session
user-turnsUser-turn tracking, answer summaries/revision, optional root TUI/RPC completion, and the open-turn service; loads before tui-display (decision)
tui-displayTranscript-view draw rule, status block and status row, answer summary line, shared work store
tui-asideFork the current session into a Herdr pane
herdrHerdR session titles
worktreeWorktree guards, lifecycle tool, and the session’s work root
bashBash-call default timeout and RTK command rewrite
observabilityRequest logging and output-rate samples
tui-footerSession footer fields, width behavior, and session cost
relayOwned peer messaging and the shared filesystem transport for peer and native async-task mail; loads before async-tasks
async-tasksAsync tasks, their results, and the service that lists them: subagents, named headless child agents, and shell tasks, including persistent jobs and output
goalDurable owned Goal lifecycle, budgets, child attribution, and rollback preflight
ast-grepThe ast_grep tool: working-tree outline, source read, structural search and guarded rewrites
codebase-memoryNative CBM graph tools with work-root admission and explicit indexing
stall-guardBounded recovery from silent or premature model stops
compactionDefault asynchronous compaction through public checkpoints, with a configured target, native safety fallback and usage accounting
browserIsolated native browser MCP, task-session guard, and explicit task close
supervisorDurable project supervision, child attempts, and evidence verification
providersNative provider registration: provider connections, scoped catalogs, and transport ownership
caffeinateThe 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.

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:

  • description accompanies the provider’s callable tool definition.
  • promptSnippet adds the tool to Pi’s Available tools system-prompt section; keep its normalized text to one concise entry.
  • promptGuidelines adds 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.

Use engineer’s proportional workflow and central verification for check selection and repository gates. Local TypeScript extension edits take effect on /reload.

  1. Create packages/<name>/ and move the entry files in.

  2. 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.extensions lists every entrypoint in load order; shared non-entry modules (for example packages/worktree/worktree-tool.ts) stay out of the manifest. peerDependencies lists 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 with import type from @earendil-works/pi-coding-agent, narrowed to the members the file reads (Pick<ExtensionAPI | ExtensionContext, …>). Pick keeps Pi’s per-event on overloads, 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 in test/display-host.test.ts.

  3. Append "./packages/<name>" to the root pi.extensions manifest in intended load order, and add its README link to the inventory above. The manifest remains load-order authority. Keep the native knip.ts and fallow.toml entry lists in step with it.

  4. Add a minor Changeset for its initial 0.1.0 version and changelog.

  5. Use central loader smoke to check the package without ambient extensions or network access.

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

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.

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.

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.

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.

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.

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.