Skip to main content

pi-extension-authoring

WHAT: Conventions for adding or modifying a pi extension under `config/pi/extensions/` - the .ts + .md pair and where pure helpers / unit tests live. WHEN: the user asks to add a new pi extension, extract extension logic into a shared helper, or modify an existing extension's behavior. DO-NOT: put business logic in the .ts file (extract to `lib/node/pi/`); skip the companion .md doc; forget the `extensions/README.md` row; or make behavior tier-specific (small-model vs big-model branches).

インストールへ移動

ソース情報

リポジトリ
weikinhuang/dotfiles
ソースの最終更新活動
2026年6月11日 21:57
検出された SKILL.md の言語
英語
スター
21
フォーク
3

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
pi-extension-authoring
description
WHAT: Conventions for adding or modifying a pi extension under `config/pi/extensions/` - the .ts + .md pair and where pure helpers / unit tests live. WHEN: the user asks to add a new pi extension, extract extension logic into a shared helper, or modify an existing extension's behavior. DO-NOT: put business logic in the .ts file (extract to `lib/node/pi/`); skip the companion .md doc; forget the `extensions/README.md` row; or make behavior tier-specific (small-model vs big-model branches).
# Pi Extension Authoring Pi extensions live under `config/pi/extensions/`. Each extension is a pair: a `.ts` file (the extension itself) and a sibling `.md` file (the deep reference doc). Pure logic extracts into `lib/node/pi/` so it's unit-testable under `tests/lib/node/pi/`. This skill captures the repo-specific conventions on top of pi's public extension API. ## When this applies - Adding a new pi extension (new `.ts` + `.md` pair). - Extracting logic from an extension into a shared helper. - Modifying an extension's detection / nudge / guardrail behavior. - Wiring (or unwiring) an extension in `settings-baseline.json`. Skip this skill for config-only changes to `settings-baseline.json` that don't touch extension code. ## File trio Every extension is three things: | File | Purpose | | -------------------------------------------------------- | -------------------------------------------------------------------------- | | `config/pi/extensions/<name>.ts` | The extension itself. Hooks, tool registrations, minimal glue. | | `config/pi/extensions/<name>.md` | Deep reference: detection rules, config shape, env vars, hot-reload notes. | | `lib/node/pi/<helper>.ts` | Pure helper(s) the extension calls into. Testable without pi's runtime. | | `tests/lib/node/pi/<helper>.spec.ts` (added with helper) | Vitest spec covering every branch of the helper. | Wire the extension into `config/pi/settings-baseline.json` under the `extensions` array. Import paths are relative to the baseline file. ## The layering rule Keep extensions thin. Every non-trivial behavior belongs in a pure function under `lib/node/pi/`. - **Extension `.ts`**: pi hooks (`session_start`, `agent_end`, tool registrations), plumbing, env var reads, and calls out to the helper. Aim for under 100 lines. - **Helper `lib/node/pi/*.ts`**: the actual logic. Pure, deterministic, no pi API calls. One exported function per concern. These get vitest specs. - **Helper spec `tests/lib/node/pi/*.spec.ts`**: covers every input/output pair the helper produces. Run with `npm test`. This layering is what lets the test suite run without spinning up pi. If you find yourself mocking pi inside a vitest spec, the logic probably wants to move down into a helper. ## Robustness, not tier switches The robustness machinery in every extension carries the load across model tiers. Do NOT write `if (model.isSmall)` / `if (tier === 'weak')` branches. - Detection patterns (regex / heuristics) stay the same regardless of which model is running. - Nudge text stays the same - don't "dumb down" for small models; a clear instruction is a clear instruction. - When a feature genuinely needs a cheap model (e.g. a critic subagent's grader), use `modelOverride` on the subagent call site, not a tier switch inside the extension. - See the companion memory `research-extensions-robustness-principle`. ## Tiny-model usage is opt-in and non-load-bearing If the extension uses a local tiny model (e.g. a small quantized local model) for a subtask: - Route through `runOneShotAgent` (or a `tiny-helper` subagent) - never embed the model id directly. - Gate via a setting that defaults off. When the tiny model is disabled or unavailable, the extension falls back to deterministic behavior - never errors out. - The tiny model must never touch user-visible research content or verification verdicts. It's for plumbing (heuristic classification, phrase extraction) only. - See the companion memory `research-tiny-model-non-load-bearing-rule`. ## Deep-doc (`.md`) shape The `.md` file is the reference, not a tutorial. Mirror the shape of existing docs like `verify-before-claim.md`: 1. **One-sentence purpose** - what failure mode the extension addresses. 2. **Composition table** (when multiple extensions listen on the same hook) - how this extension distinguishes its signal from siblings. 3. **Detection** - the exact patterns / walks / heuristics. Link into the helper file. 4. **Rule / config shape** - with a JSONC example when the extension supports per-project overrides. 5. **Environment variables** - `PI_<NAME>_DISABLED`, `PI_<NAME>_VERBOSE`, `PI_<NAME>_TRACE=<path>`. Standard trio; include what applies. 6. **Hot reload** - which files trigger `/reload`, which require a session restart. Update `config/pi/extensions/README.md`'s index table in the same commit. ## Smoke-testing against a local small model After shipping a nudge / detection / guardrail, smoke-test against a local small / weak model to confirm it behaves sensibly when tool-call precision is weak: ```bash pi -p "<scenario prompt>" --model <provider/local-small-model> --no-session ``` Use whatever local small/weak model your setup runs; omit `--model` to smoke against the current model instead. Scenarios to exercise: - The detection positive case - does the extension fire? - An explicit negative - confirm the check exempts legitimate work. - The idempotency path - the sentinel-marker check means the same user message must NOT re-trigger the extension on a retry. For visual / critic extensions that call a critic subagent, confirm the critic can attach images (`read <png>` auto-attaches on recognized extensions - `png`, `jpg`, `gif`, `webp`). Render SVGs to PNG with whatever rasterizer your machine has (`rsvg-convert` or `magick`). Record your own setup's specifics - the exact local model id, any proxy caveat, the installed SVG rasterizer - in a personal note or memory rather than here, so this doc stays portable for anyone cloning the repo. ### Multi-turn headless A single `pi -p` is one turn. To drive a real multi-turn conversation headless (e.g. a guardrail that should fire on turn 2, or summary / affinity state that accrues across turns) there are two ways: **RPC mode - preferred when an agent is driving the test.** One long-lived `pi --mode rpc` process takes JSONL `prompt` commands on stdin and streams events back as JSON lines on stdout. From inside pi, start it as a `bg_bash` job with `interactiveStdin: true`, write one command per turn, and watch for the `agent_end` event that marks each turn done: ```text pi --mode rpc --model <provider/local-small-model> --no-session # write to stdin, one JSON object per line: {"id":"t1","type":"prompt","message":"turn 1"} {"id":"t2","type":"prompt","message":"turn 2"} # same process, remembers turn 1 ``` Context lives in the process for its lifetime, so `--no-session` is fine. It uses stdin / stdout pipes (no unix socket), so the sandbox caveat below does not apply. Wait for `agent_end` before sending the next prompt; to enqueue while the agent is still streaming, add `"streamingBehavior":"steer"` (delivered after the current tool calls) or `"followUp"` (delivered when the agent stops). Shut down with SIGTERM. Full protocol: `docs/rpc.md` in the pi clone. **Session reuse - good for shell scripts / CI.** Respawn `pi -p` against a persisted session: ```bash DIR=$(mktemp -d) pi --session-dir "$DIR" --session-id smoke -p "turn 1" --model <provider/local-small-model> pi --session-dir "$DIR" --session-id smoke -p "turn 2" # resumes the same session, sees turn 1 rm -rf "$DIR" ``` `--session-id <id>` opens the session when one with that exact id already exists in the project, otherwise creates it; it cannot be combined with `--no-session`. A throwaway `--session-dir` keeps the test isolated and easy to clean up. (`-c` / `--continue` also continues a conversation, but it picks the most-recent session non-deterministically - prefer `--session-id` for scripted tests.) ### Driving the TUI with tmux Interactive surfaces (statusline, header / footer widgets, avatar, keybindings) never render under `-p`. Script the TUI in a detached tmux pane and assert on the captured screen: ```bash SOCK=./.pi-tui-sock # socket path under cwd or /tmp tmux -S "$SOCK" new-session -d -x 120 -y 40 -s t "pi --model <provider/local-small-model>" sleep 8 # let the TUI boot tmux -S "$SOCK" capture-pane -p -t t # read the rendered screen tmux -S "$SOCK" send-keys -t t "your prompt" Enter # type + submit sleep <model-latency> tmux -S "$SOCK" capture-pane -p -t t # assert on the response / widget tmux -S "$SOCK" send-keys -t t C-p # exercise keybindings, slash commands, etc. tmux -S "$SOCK" kill-server # clean up ``` Sandbox caveat (Linux): the `sandbox` extension's seccomp filter blocks tmux's unix socket, and the `unixSockets.allow` path list is ignored on Linux (seccomp can't match by path). When you are about to run a tmux TUI test, ask the user to disable the sandbox for the session (`/sandbox-disable`, or relaunch with `PI_SANDBOX_DISABLED=1`) - do **not** enable `unixSockets.allowAll`, which would relax socket isolation for the whole session. ## Wiring into settings-baseline.json Add an entry under `extensions`: ```jsonc { "extensions": [ // …existing… "./extensions/<name>.ts", ], } ``` `settings-baseline.json` mirrors `~/.pi/agent/settings.json`; keep runtime-only keys out of the baseline (e.g. `lastChangelogVersion`). Run `/reload` in a live pi session to pick up extension changes without restarting. ## Anti-patterns - **Logic in the `.ts` extension file.** Tests can't reach it without spinning up pi. Extract to `lib/node/pi/`. - **Tier-specific branches.** Small-model vs big-model code paths. Use robust detection + gentle nudges instead. - **Tiny model on the hot path.** If the extension errors when the local model is unreachable, it's load-bearing - refactor to a deterministic fallback. - **Skipping the deep `.md` doc.** The `.md` is the contract for future readers. The extension index in `extensions/README.md` links to it. - **No `PI_<NAME>_DISABLED` escape hatch.** Every extension should be silenceable by env var. - **Re-triggering on the extension's own nudge.** Use a sentinel marker (e.g. `⚠ [pi-<name>]`) on the injected message and short-circuit when it's present on the most recent user message. - **Leaving the test gap - no `lib/node/pi/*.spec.ts` for the helper.** Small regressions in detection patterns destroy the guardrail's value; tests catch those. ## Checklist before finishing 1. `.ts` extension under `config/pi/extensions/` - thin, no business logic. 2. `.md` deep doc with detection + config + env vars + hot-reload sections. 3. Pure helpers extracted to `lib/node/pi/`. 4. Vitest spec under `tests/lib/node/pi/`. `npm test` passes. 5. Entry added to `extensions` array in `settings-baseline.json`. 6. Row added to `config/pi/extensions/README.md` index table. 7. `PI_<NAME>_DISABLED` env var supported; smoke-tested against a local small model. 8. `./dev/lint-shell.sh` passes for any shell files touched. 9. If a companion skill teaches WHEN to use this tool, add it under `config/pi/skills/<name>/SKILL.md` and cross-link from the `.md` and `README-skills.md`.
GitHubで見る