Extension Packages
These rules apply to every package under extensions/*.
New extension checklist
Create:
extensions/<name>/package.jsonextensions/<name>/index.tsextensions/<name>/README.mdextensions/<name>/__tests__/harness.test.tsextensions/<name>/mise.toml
Then run mise run //:lock and the extension-local lint, test, and check tasks.
Do not run npm install directly.
Package manifest contract
Every extension package must declare:
- name
@bugabinga/pi-ext-<name> private: truetype: "module"main: "index.ts"license: "MIT"- development dependencies on
@marcfargas/pi-test-harnessand Vitest
Declare what the generated package needs under pi-ext:
entries: extra bundled entrypoints such as worker scripts spawned by pathresources: files or directories copied verbatim, such as skills, prompts, templates, or executable helpersassets: package-relative destination to<dependency>/<file>for browser assets copied out of a bundled dependencyexternals: third-party packages kept external, each with its demonstrated bundling incompatibility; they install into the generated package
Portable third-party JavaScript is bundled by default; Node built-ins, Pi modules, and TypeBox stay external.
Resolve runtime files relative to import.meta.url or import.meta.dirname; generated packages keep every chunk flat in the package root so those lookups work from any chunk.
When a path depends on the file extension, derive it from import.meta.url (.ts in source, .js generated).
Do not declare a per-extension version.
The root package owns the repository version.
Do not declare package scripts.
Extension automation belongs in mise.toml.
Declare only packages imported by the extension, including development and test code.
Put Pi packages under peerDependencies with range "*".
Never depend on another @bugabinga/pi-ext-* package.
scripts/extensions.mjs enforces this contract.
Mandatory harness test
Every extension must include a pi-test-harness test that loads index.ts through the real Pi extension runtime.
Mocks around helper functions do not replace this test.
Run it through mise run //extensions/<name>:test.
Mandatory local tasks
Every extension must expose lint, test, and check in its local mise.toml.
Root tools and environment are inherited, so do not redeclare them locally.
Use this baseline, replacing angel with the package directory name:
[tasks.lint]
description = "Lint angel extension"
depends = ["//:install"]
run = [
"biome ci --config-path ../.. .",
"rumdl check --config ../../.rumdl.toml .",
"taplo format --check --config ../../.taplo.toml mise.toml",
]
[tasks.test]
description = "Test angel extension"
depends = ["//:install"]
run = [
{ task = "//:test", args = ["extensions/angel"] },
]
[tasks.check]
description = "Check angel extension"
depends = ["//:install"]
run = [
{ task = "//:build", args = ["angel"] },
]
Keep optional package automation, such as bench, e2e, or dev, in the same local mise.toml.
Do not replace structured task references with command strings that spawn mise run.
Independence
Extensions must remain standalone packages.
Never import from another @bugabinga/pi-ext-* package or another extension directory.
Optional integration must use capabilities or events and degrade cleanly.
Important behavior needs a native fallback; decorative integration may no-op.
Model calls
Prefer a parent-linked child AgentSession over ctx.modelRegistry.complete() for nested model work so Pi session tracking records it.
When a direct model call is necessary, use ctx.modelRegistry, never Pi AI compatibility dispatchers, so extension-registered providers work.
Imports
| Package | Provides | Placement |
|---|---|---|
@earendil-works/pi-coding-agent |
Extension API, context, and coding-agent types | peer dependency when imported |
@earendil-works/pi-ai |
model and message APIs | peer dependency when imported |
@earendil-works/pi-tui |
terminal UI components and rendering helpers | peer dependency when imported |
typebox |
tool parameter schemas | peer dependency when imported |
@bugabinga/pi-ext-* |
sibling extension implementation | never |
Diagnostics
Runtime extension code must never write diagnostic text through console.* or process stdout/stderr; direct terminal writes corrupt Pi's TUI and machine-readable modes.
Every extension must implement the debug contract with a committed generated src/pi-ext-debug.ts, handwritten src/debug.ts, lifecycle events, and one extension-local harness check.
Generate the local writer from scripts/codegen/targets.json with mise run //:codegen; change its template rather than editing generated copies.
In src/debug.ts, call defineDebug with literal event names, span bases, and approved numeric, boolean, or finite-enum metadata fields.
Import the needed dbg, span, and closeDebug exports from the handwritten file in runtime code; do not import direct logger exports from the generated file.
Static checks require the schema binding and a runtime import, and reject runtime imports of the generated module.
Never pass credentials, prompts, commands, raw errors, paths, or arbitrary objects to debug code; use safe counts, finite classifications, or presence flags instead.
Keep logging off by default; enable it only through user settings or the extension-specific environment override.
Each extension uses only its own local writer and remains usable outside the root checkout.
Use ctx.ui.notify for actionable failures, throw tool failures, and use opt-in file diagnostics for debugging.
Do not emit info notifications during /reload: Pi replaces them with its reload-success status.
Preserve actionable warning and error notifications.
Terminal-control sequences, standalone benchmarks, and worker protocols are exempt.
Settings
Configuration must implement the settings contract through a committed generated src/pi-ext-settings.ts; add the extension to the pi-ext-settings codegen target and run mise run //:codegen.
Never read getGlobalSettings, getProjectSettings, registerFlag, or getFlag directly; static checks reject them outside generated files.
Keys live under the extension slug; new env variables are PI_<SLUG>_<NAME>; flags are for startup toggles only.
Pass real project trust, and never let an invalid value fall through to a lower source.
Secrets stay in env or Pi's credential store, never in settings.
Add settings only for behavior users actually need to change.
Startup
session_start must be fast.
Treat every Windows process spawn as expensive.
Prefer native Node APIs and defer unavoidable process spawns until first use.
Keep entry modules thin and lazy-load command or tool implementations.
Gate optional domain work with cheap relevance checks before imports.
Do not await network access, Git, filesystem scans, or subprocesses during startup.
Publish minimal UI immediately, then defer slow work with a background task and generation or stale-result guard.
Abort asynchronous startup work on reload and ignore stale results.
Check executable presence through native PATH access, not version subprocesses.
Never block other extensions for telemetry, recovery, decoration, or cache warming.
Preserve registration, validation, rendering, and first-use behavior when deferring work.
Measure startup performance with the complete extension set on Windows and WSL.
Tool widgets
Tool widgets must match Pi built-ins: compact by default and expanded through app.tools.expand.
Normal tool rows must rely on Pi-owned keyboard and mouse expansion.
Every custom renderCall and renderResult must honor native expanded state, including progress, errors, and malformed-detail fallbacks.
Collapsed results may show status, counts, paths, identifiers, and tiny bounded summaries only.
Full content, previews, snippets, verbose logs, structured bodies, and per-result lists require expanded state.
When content is hidden, show keyHint("app.tools.expand", "to expand").
Do not use the legacy expandTools action.
Focused overlays and non-tool widgets may handle their own expansion action.
Cross-platform behavior
All extensions must work on Windows, macOS, and Linux.
Do not add operating-system-scoped extensions.
Prefer native Node APIs over shell helpers such as which, bash, sed, grep, or /tmp.
Check platform-tool availability when the extension loads.
Report missing required tools with warning severity and missing optional tools with info severity.
Test platform-tool behavior through the real extension or tool path with controlled fake executables on PATH, not only mocked helpers.
Context impact
For new extensions or major edits, measure before/after context impact following scripts/context-startup.md; report provider-token deltas, clearly distinguish heuristic estimates, and disclose measurement blockers.
Commands
Commands with subcommands must provide argument autocomplete.
Documentation
Keep READMEs terse. Do not duplicate source-of-truth configuration, API details, or implementation documentation. Link to canonical source files, commands, settings, or upstream documentation. A package README should explain purpose, installation or loading, commands and tools, settings, and optional integrations.
Generated packages
Pi loads dist/<name>, never the source workspace; the root manifest is intentionally empty.
Build with mise run //:build-ext <name> or //:build-all, then /reload in Pi.
Verify relocation with mise run //extensions/<name>:verify-package; verify the whole collection with mise run //:verify-packages --collection.
Print the user settings package list with mise run //:settings-entries.
Publishing
Use the root //:publish task.
Publishing is Git-only; there is no npm registry flow.