# Changelog

All notable changes to this package. The extensions themselves are snapshot copies from the author's pi environment; their individual histories live in that repository.

## Unreleased

Snapshot sync of two upstream commits (`5c90729`, `f3b4e4a`) plus four uncommitted files from the same snapshot. No version bump: 2.3.0 has not been published to npm, so this entry rides along until the next release. The extension count stays at 30 and the suite moves 1282 → 1334 tests — and the failure count drops 12 → 11, because the `dangerous`-mode test this sync's own work fixed used to be one of them.

### Changed

- **`bash-command-collapse/sandbox.ts` — the seatbelt profile's base is now `(allow default)`, with exactly one capability taken back: delete.** The old base was `(deny default)` with what was *permitted* enumerated, which has a consequence that is easy to miss: **the thing being bounded was not deletion but every seatbelt operation nobody had enumerated yet.** Measured — `screencapture` died with `Trace/BPT trap: 5` inside the sandbox while working outside it, and bisecting one rule at a time showed the only missing piece was `iokit-open`: the gap was the enumeration, not the permission decision. A 17-cell matrix run over both bases came out cell-for-cell identical (`file-write-unlink` is a separate mach operation, so `allow default` does not open a hole in it). Known and deliberately unfixed: `ps`, `top` and `launchctl list` are still blocked, a seatbelt limitation rather than something the profile can grant. The cost is stated in the docs: the sandbox no longer backstops *unknown* operations, and apart from deletion it does not care what a command does.
- **The authorization escalation chain works again on pi 0.99.x.** pi 0.99.1 changed built-in bash from `throw` on a non-zero exit to `return { isError: true }`, and the whole "extract the blocked paths → dialog → widen the profile and rerun" chain lived in a `catch` — so a sandbox `EPERM` took the **normal return** path, `catch` never ran, and the entire escalation was silently dead in interactive sessions. Only `/sandbox-boundary allow`, `PI_SANDBOX=off` and `shift+tab` dangerous were left as exits. The new `runWrapped` helper accepts **both delivery paths** (`isError` returns *and* real throws such as abort/timeout), and a failure that does not look like a sandbox denial is handed back untouched — as the original `result`, so `details` / `structuredContent` survive and neither the preview nor codemode's `structuredContent` loses anything.
- **`looksLikeDeletionCommand`, a verb gate against false `[沙箱]` annotations.** `maskedDenialPaths` decides "a delete was swallowed" from two signals: denial *text* in the output, and the target path still existing on disk. A measured run broke that: `echo "rm: /path: Operation not permitted"` over a path that really exists satisfies both while deleting nothing, and the result got an annotated `[沙箱] 命令整体成功，但以下删除被沙箱拦下（文件仍在）`. The gate runs **before** the byte match: split on `;` `&&` `||` `|` `&` and newlines, skip leading `VAR=value` prefixes and wrapper words (`sudo`, `env`, `nohup`, `time`, `xargs`, …), then read the segment's program basename — `rm`, `rmdir`, `unlink`, `mv`, `ln`, `shred`, `trash`, plus `sed` / `perl` / `ruby` with `-i` (in-place edit rewrites through `rename`, an unlink of the source). `git` is resolved to its subcommand, so `git clean` and `git worktree remove|prune` count while `git commit -m "clean up"` does not. **The direction is deliberate: annotate too little rather than too much** — a delete hidden inside a script (`node x.js` calling `unlink`) is not annotated, which costs one line of guidance and is still covered by the non-zero-exit escalation, whereas a gate that annotates `cat` and `grep` output would emit noise on every log containing those words.
- **`background-tasks` — the turn-ended dock note is now English and drops its first half**: `└ 本轮已结束，该任务仍在运行` became `└ Task is still running` (user decision 2026-10-01). The dock's own position already says the turn ended, so repeating it only spends width, and `running` in the same line is English too — one language inside the line. The line still states only what the extension can prove; an 8-minute `npm test` continuing after the turn is normal, so it never claims the task is orphaned.
- **`user-message-bar` — the bar glyph is now `▏` (U+258F, left one-eighth block) instead of `▎` (U+258E, left one-quarter block).** `BAR_GLYPH` and the `glyph` option's documented default change with it; the geometry is untouched (the glyph still occupies the one column of left padding `Box` already reserves, and the extra indent still comes back out of the trailing padding), so nothing about width or wrap positions moves.
- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README (the `user-message-bar` row's glyph, the English turn-ended note, and the sandbox section's new `(allow default)` base, `isError` escalation fix and verb gate).
- **English documentation** updated by hand: [README](README.md) (the `user-message-bar` row's glyph, test count), [docs/extensions.md](docs/extensions.md) (the full `allow default` rationale and the verb gate in the `sandbox.ts` section, the turn-note wording and its reason, the `▏` glyph in three places, test counts), [docs/development.md](docs/development.md) (test counts, the new per-pi-version table, and the note that the `dangerous`-mode failure is gone), [docs/installation.md](docs/installation.md) (the `▏ ` bar), [docs/extensions-architecture.html](docs/extensions-architecture.html) (the boundary caption and the footer note).

### Notes

- **Measured on this sync** (inside a pi session, i.e. with the outer seatbelt boundary active, which is why the nested-sandbox probe fails and 23 cases skip): **1334 tests** — 1300 pass / 11 fail / 23 skipped against this machine's default pi **0.99.2**; **1334 / 1304 / 7 / 23** against pi **0.99.1**; **1323 tests, 1299 pass / 1 fail / 23 skipped** against **0.85.1** and **0.87.1** (the 12 missing tests are `codemode-tree/index.test.ts`, which throws at load without `createCodemodeExtension`; its 11 pure-logic `render.test.ts` cases are unaffected). From a shell that is not itself inside seatbelt — a plain terminal, or a background task, which bypasses the boundary — the probe succeeds and all 23 execute: **1334 tests, 1323 pass, 11 fail, 0 skipped**, the same eleven failures.
- **The eleven failures are pi-version couplings, not sync regressions.** Every failing file is byte-identical to the snapshot except `bash-command-collapse/render.test.ts`'s standing delta (the repo-root probe below), so the snapshot cannot be showing a different set — the snapshot has no `package.json`, so its own suite cannot be run to confirm it directly. The seven are `theme.fgColors` color assertions (`bash-command-collapse/render.test.ts` 5, `read-path-collapse/render.test.ts` 2 — pi 0.99.1 made the field private) and the four are preview-window assertions in `bash-command-collapse/render.test.ts` (pi 0.99.2 renders the truncation hint as ASCII `...` where the tests assert `…`, places the `└ ` one line later than expected, and leaves a blank line before the appended `Command exited with code N` status that the test reads as a break in the tree's fence). Fixing them belongs upstream in `clients/pi/`.
- **The failure count went 12 → 11 and nothing new appeared.** The one that disappeared is the `dangerous`-mode case: its `bypass` control group expected the wrapped command to throw, which 0.99.1 stopped doing — the same `isError` change this sync addresses, so the test now passes. Measured on the pre-sync tree (default pi 0.99.2): **1282 tests, 1248 pass, 12 fail, 22 skipped**, against **1334 tests, 1300 pass, 11 fail, 23 skipped** now. The +52 reconciles file by file: `bash-command-collapse/sandbox.test.ts` 59 → 110 (the two profile cases plus three table-driven `looksLikeDeletionCommand` blocks expanding into 49 cases) and `bash-command-collapse/render.test.ts` 49 → 50 (the echo false-positive regression). The skipped count moved 22 → 23 in that same `render.test.ts` — the new real-sandbox case is one of the skip-marked ones.
- **All 30 entries load with `errors: []`** through pi's own loader (`discoverAndLoadExtensions`) against **0.99.1** and **0.99.2**; against 0.85.1 / 0.87.1 it is **29 of 30**, the one error being `codemode-tree/index.ts` (`createCodemodeExtension is not a function` — that export arrived in 0.99.1), which leaves the other 29 untouched.
- `extensions/` is byte-identical to the snapshot except for `bash-command-collapse/render.test.ts` (delta 5: the repo-root probe walks up to the first `.git` instead of hard-coding four `..` segments, which only works in the standalone repo — `rsync` overwrites it on every sync, so it was re-applied here); `themes/*.json` are byte-identical; `config/AGENTS.md` and `config/AGENTS.core.md` were re-copied verbatim. **`config/settings.json` needed no hand-merge this time**: the snapshot's copy is unchanged since the last sync, so `diff` shows exactly the five removed machine-local model selections and nothing else.
- **No version bump**, so `package.json` still says `2.3.0` and this entry is not a release section. The `1282 → 1334` count above is measured against the suite as the 2.3.0 section left it, so the two entries reconcile.

## 2.3.0 — 2026-10-01

Snapshot sync of nine upstream commits. The headline is a new **`codemode-tree/`** extension that gives pi's built-in `codemode` tool block the same tree rendering the bash and read blocks already had, and the **startup header's title line now carries the current model and thinking level**. The extension count moves 29 → 30 and the suite 1256 → 1282 tests.

### Added

- **`extensions/codemode-tree/`** — the `codemode` tool block rendered as the same tree as the bash block: `• codemode` at column 0 → syntax-highlighted script on `│ ` continuations (pi's 10-visual-line preview budget and its `… (N more lines, ctrl+o to expand)` hint kept) → result tree with `└ ` appearing **once**, on the first substantive result line. Columns align with the bash block exactly (dot 0, `codemode` / `│` / `└` 2, body 4), so every child component renders at `width - 4` — including the command side, whose two deliberately empty indent columns keep the wrap width from jumping when the result arrives. The dot is **three-state** (`stateDotSlot`): white (`text`) while running — deliberately different from bash's `dim` — green (`toolDiffAdded`, byte-identical to bash's success dot) on success, red (`toolDiffRemoved`) on failure; with the background gone the dot is the only outcome lamp, and the render cache key includes the state so the running white dot is not pinned after the result arrives. No background, no boundary blank lines (`renderShell: "self"`). **How it gets codemode's execution logic**: `codemode` is a built-in *extension* (`builtin:codemode`) with no definition-only export, so the extension runs pi's own `createCodemodeExtension()` against a `Proxy` that intercepts only `registerTool` and forwards every other `pi.*` access to the real API, then spreads the captured definition and overrides only the two renderers and `renderShell`. The captured `parameters` is the **same object reference** as pi's own (measured on a real CLI), which keeps the MCP extension's schema-reference-equality check (`isCodemodeTool()`) working; description and parameter schema verified byte-identical (5674 bytes). **Truncation hints come in two nouns** — code/output previews say `… (N more lines, …)` / `... (N earlier lines, …)` but the nested-call list says `... (N earlier calls, …)`; recognising only `lines` hung the `└ ` on the hint (only visible with more than 8 nested calls, which is why the regression assertion uses the built-in renderer's real output string). `PI_CODEMODE_TREE=off` disables it — but see the settings coupling below. 23 new tests: `render.test.ts` (11, pure logic) and `index.test.ts` (12, through pi's real loader and `ToolExecutionComponent`, including "the success dot is byte-identical to bash's" and full semantic inheritance of name / exposure / defaultActive / execute / prepareLoadout / parameters / renderShell). Colour assertions deliberately assert relations ("same as bash", "the three differ") instead of hardcoded values.

### Changed

- **`config/settings.json`** — three new portable keys, hand-reconciled as always: `extensions: ["-builtin:codemode"]` (disables pi's built-in codemode extension so `codemode-tree` owns the tool without pi printing the "built-in extension `codemode` was not loaded" warning in two unsuppressible places), `defaultTools: ["+codemode"]` (the captured definition registers inactive — `defaultActive: false`, `exposure: "model-only"` — so this line is what activates it), and `followUpMode: "all"` (**a deliberate non-default**: queued follow-up messages deliver as one turn instead of one turn each, so several queued background-task notifications become one model turn — measured 2026-09-30: nine notifications, 38 s, nine turns under the default). The five machine-local model selections remain removed per the standing delta; `diff` against the snapshot shows exactly those and nothing else.
- **`startup-logo/`** — the title line is now `pi v0.99.2 (deepseek-flash-qd with max effort)`: the model id from `ctx.model?.id` and the thinking level from `ctx.thinkingLevel`, read live so `/model` and `shift+tab` are reflected on the next frame, each in a `try/catch` because a stale ctx throws on read (missing model → no parenthesis, missing level → `(model)`). **Every header line is indented one column** (`MARK_INDENT`) so nothing sits flush against the terminal edge, and the title, hint and onboarding lines all go through `truncateToWidth` — the model id is external input of unbounded length and pi-tui throws on an over-wide line (`Rendered line N exceeds terminal width`), so the budget counts the indent and "no frame over-wide" does not depend on terminal width. 3 new tests (the long-model-id / narrow-terminal width sweep, plus two `formatTitleLine` cases).
- **`config/AGENTS.md` / `config/AGENTS.core.md`** — the one self-contradicting wait rule is fixed: "Do not block on `sleep` … poll, or split the work" became "Never wait by blocking: no `sleep`-poll loops … `run_in_background` is the answer — start it, end the turn; its terminal notification wakes you" (attributed in the snapshot to a session where the model slept 38 times, 56.6 minutes total, polling a background task's log). The core file gained the matching line so `core-rules`' mid-session re-injection cannot push the old wording back.
- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README (the `codemode-tree` row, the `followUpMode` and `-builtin:codemode` rationale, pi-web-access 0.34.0 and the pnpm `minimumReleaseAge` upgrade pitfalls, the corrected `AGENTS.md` character budget).
- **English documentation** updated by hand: [README](README.md) (extension count 29 → 30, the `codemode-tree` row and switch, the startup-logo row, test count, the loader-check paragraph now split by pi version), [docs/extensions.md](docs/extensions.md) (counts, the full `codemode-tree/` section, the startup-logo model/title paragraph, the `PI_CODEMODE_TREE` switch row, the settings-coupling interaction bullet), [docs/configuration.md](docs/configuration.md) (the `extensions` / `defaultTools` / `followUpMode` rows), [docs/development.md](docs/development.md) (test counts, the per-pi-version measurement table, the codemode-tree minimum-version note), [docs/installation.md](docs/installation.md) (the split loader-check result).

### Notes

- **Measured on this sync** (inside a pi session, with the outer seatbelt boundary active — which is why the nested-sandbox probe fails and 22 cases skip): **1282 tests** — 1248 pass / 12 fail / 22 skipped against this machine's default pi **0.99.2**; **1252 pass / 8 fail / 22 skipped** against pi **0.99.1**; **1271 tests, 1248 pass / 1 fail / 22 skipped** against pi **0.85.1** and **0.87.1** (the 11 missing tests are `codemode-tree/index.test.ts`, which throws at load without `createCodemodeExtension`; its 11 pure-logic `render.test.ts` cases are unaffected).
- **The failures are pi-version conditions, not sync regressions — the snapshot repository shows the identical sets.** The eight on 0.99.1 are the standing set recorded in 2.2.0 (seven `theme.fgColors` color assertions, one `dangerous`-mode bypass control). pi **0.99.2** adds four more, all in `bash-command-collapse/render.test.ts`: two assert the truncation hint's `…` where 0.99.2 now renders ASCII `...` (the extension's own `isTruncationHint` already accepts both, so the tree is still shaped correctly), one expects the preview window's `└ ` one line later than 0.99.2 places it, and one reads a blank line pi now leaves between the `(no output)` placeholder and the appended `Command exited with code N` status as a break in the fence. Fixing them belongs upstream in `clients/pi/`.
- **`codemode-tree` needs pi 0.99.1** (`createCodemodeExtension` first shipped there). All 30 entries load with `errors: []` through pi's own loader on 0.99.1 and 0.99.2; on 0.85.1 / 0.87.1 it is 29 of 30, that one entry erroring and the rest untouched. The documented minimum stays 0.85.1 for the other 29.
- **The 22 skips** are the real-`sandbox-exec` cases: they skip exactly when the test process's own shell is already inside seatbelt, so a run nested inside a pi session declares them skipped rather than faking a pass. From a shell that is not itself sandboxed the probe succeeds and all 22 execute — the numbers above are the nested run.
- `extensions/` is byte-identical to the snapshot except for `bash-command-collapse/render.test.ts`, which `rsync` overwrites on every sync and which was re-applied here; `themes/*.json` are byte-identical; `config/AGENTS.md` and `config/AGENTS.core.md` were re-copied verbatim; `config/settings.json` shows exactly the five removed machine-local selections and nothing else.

## 2.2.0 — 2026-09-30

Snapshot sync of eighteen upstream commits, the largest of which **removes the packaged MCP extension** (pi 0.99.1 now ships `builtin:mcp`, and the two fight over the `/mcp` registration), **isolates every background task in its own git worktree**, gives the **`plan-mode`, `memory`, `background-tasks` and `ask-user-question` tool blocks the same tree rendering** the bash block already had, and **empties the startup resource list completely**. The extension count moves 30 → 29 and the suite 1269 → 1256 tests.

This release also folds in the previously unreleased **background-task dock** entry (recorded below), which rode along because 2.1.6 was already published to npm.

### Removed

- **`extensions/mcp/`** — the twelve-file, zero-dependency MCP implementation (three transports, a hand-written JSON-RPC layer, `headersCommand` for dynamic auth headers, `/mcp`) is gone. pi 0.99.1 made MCP a built-in extension and **both register `/mcp`**; pi resolves the collision first-registered-wins, so every start printed `Extension …/extensions/mcp/index.ts registers command /mcp, so built-in extension mcp was not loaded`. Using the built-in instead buys OAuth, a `/mcp` management UI, a `pi mcp` CLI and exposure/`codemode` integration. Three differences matter for a copied config: `timeout` is in **seconds**, project config is **only** `.pi/mcp.json`, and legacy SSE plus `headersCommand` are gone. The implementation and its 132 tests stay in the git history (`git show bf3ab71` in the snapshot repository). MCP now requires pi **0.99.1** — on anything older there are simply no MCP tools. All `/mcp*` rows, the reference section, the tool-name interaction bullet and the state-on-disk row moved out of the docs into a short "use pi's built-in" note.

### Added

- **`background-tasks` git-worktree isolation** (`worktree.ts`, pure logic) — every `run_in_background` call now runs in a **fresh worktree of the current repository, detached at HEAD**, so concurrent tasks never share a working tree. The motivating measurement: in an 8-task session one task's A/B script rewrote a config file while another task claiming to measure the *baseline* read the treated code, silently invalidating the data — and throttling cannot fix several writers sharing one tree, only isolation can. Cleanup is Claude Code's three-state contract: **untouched → removed entirely**, **changed or committed → kept and its path reported to the model** (plus a `pi/bg_<id>-<timestamp>` ref when a commit would otherwise be garbage-collected), **removal failed → kept, with the git error** (fail-safe). Gitignored dependency directories are symlinked from the main repository (`DEFAULT_LINK_DIRS = ["node_modules"]`), because a fresh worktree has none and the task would not run; the worktree itself lives in the system temp directory, inside the seatbelt-deletable boundary. Creation and removal are serialized in-process (both write `.git/worktrees/`), the tasks still run in parallel, and anything that is not a git repository **silently degrades** to the old behaviour with a note to the model. The per-call `worktree: false` argument and `PI_BACKGROUND_TASKS_WORKTREE=off` both opt out. **Cleanup runs before the completion notification**, because the notification triggers a follow-up turn and sending it first would make "is the worktree still there" a race.
- **Turn-ended dock note** — after `agent_settled`, a task still running and past `TURN_NOTE_THRESHOLD_MS` (5 s, `PI_BACKGROUND_TASKS_TURN_NOTE_MS`) gets a second dock line: `└ 本轮已结束，该任务仍在运行`. It states only the half the extension can confirm — an 8-minute test continuing after the turn is normal, so it never claims the task is orphaned. `statusline/line.ts` splits the value on newlines into separate footer rows (no `trim()`, so the `└`'s indent survives) and truncates each row on its own.
- **`background-tasks/render.ts`** — pure-logic display layer for the three tool blocks and the terminal notification. Tool blocks carry **a dot and no marker**; only the terminal notification keeps a `✔` (a checkmark asserts "the task finished", and a successful `run_in_background` means it has only just started). Outcome comes from `details.ok`, not `isError`, because all three tools return normally with `ok: false`.
- **`memory/render.ts`** — the same treatment for the four memory tools: `renderShell: "self"` plus `• name` / `│ …` / `└ …`, green / grey / red dot as the outcome lamp, no `✔` / `✘` at all, and pi's 10-line preview truncation re-implemented (`… (N more lines, ctrl+o to expand)`, counted in wrapped visual lines) since the self shell drops it.
- **`plan-mode/consent.ts`** — both plan dialogs paint their **body** in `fg` while the title and highlighted option keep pi's `accent`. pi's `ExtensionSelectorComponent` wraps the whole title (the only place a body can go) in `accent`, and an inner explicit color overrides an outer one, so the body is wrapped per line — one wrap around the block would lose the color at the first in-segment newline. Nothing is imported from pi, so the wording is unit-testable.
- **`ask-user-question/render.test.ts`** — the tool block now declares `renderShell: "self"` too (no background in any of the three states, no boundary blank lines) with `paddingX = 1` putting back the one column of left margin the default `Box(1, 1)` used to draw.

### Changed

- **`plan-mode` tool blocks render as trees** — `enter_plan_mode` / `exit_plan_mode` draw `• enter_plan_mode ✔` with the body on a 2-column indented tree. Four states are decided in `render.ts` by reading `details.consented` / `details.accepted`, because "entered / approved" and "refused / rejected" are both normal returns to pi. The `└` follows through to the **last** line here, which deliberately diverges from the bash block's single `└` on the first output line — both are chosen shapes, not accidents.
- **`startup-logo` prunes the whole startup list, not four sections** — `[Extensions]` went first, and the follow-up commit took `[Skills]` too, so `[Context]` / `[Skills]` / `[Prompts]` / `[Extensions]` / `[Themes]` all disappear (only diagnostic sections such as `[Skill conflicts]` remain) and the extra blank line pi inserts before `[Context]` is removed with them rather than left as white space. **This breaks the old verification habit**: the startup list was the one screen that showed whether the extensions loaded, and it is no longer printed at all — use `pi config` or the loader check in [docs/development.md](docs/development.md) instead.
- **`statusline/line.ts` renders a multi-line dock value** (the turn-ended note) as separate footer rows.
- **Themes: `toolPendingBg` is no longer blank.** All three point it at the same variable as `toolSuccessBg`, so a card does not change color when it finishes. The tree-rendered blocks above paint no background in any state, so this only affects tools still on pi's default shell. `pi-coder-catppuccin` keeps its unused `vars.pendingPanel` as a ready value.
- **`config/settings.json`** — gains the portable parts of the snapshot's latest changes: `git:github.com/jayli/superpowers` in `packages` (superpowers is now a **pi package** — extension plus 15 skills pulled to `~/.pi/agent/git/` — replacing the older manual copy into `~/.agents/skills/`), `subagents.defaultModel: "inherit"`, `subagents.modelScope` (with the machine-local model id stripped, leaving `allow: ["inherit"]`), and `lastChangelogVersion: "0.99.1"`. The removed machine-local selections are now **five**, not four; the fifth is the `litellm-any/deepseek-flash-qd` entry inside `modelScope.allow`.
- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README (the MCP section is rewritten for the built-in, the superpowers section describes the package dependency, and the new worktree/render rationale is in place).
- **English documentation** updated by hand: [README](README.md) (extension count, test count, the `mcp/` row removed, companion packages 2 → 3 with superpowers explained, the startup-list and requirements paragraphs), [docs/extensions.md](docs/extensions.md) (counts, the `/mcp` rows and the whole `mcp/` section replaced by a built-in note, the full worktree/turn-note/render sections for `background-tasks`, `memory`, `plan-mode` and `ask-user-question`, two new switches, a new state-on-disk row for temp-dir worktrees), [docs/development.md](docs/development.md) (test counts, the sync deltas renumbered 1–5 with `subagent/config.json` and the re-applied `render.test.ts` delta, the pi 0.99.1 test caveat), [docs/configuration.md](docs/configuration.md) (the removed-keys table, the `packages` / `subagents.defaultModel` / `subagents.modelScope` rows, the `mcp.json` and `subagent/config.json` sections), [docs/installation.md](docs/installation.md) (the companion packages and the MCP paragraph), [docs/themes.md](docs/themes.md) (pending/success share a background).

### Notes

- **Measured on this sync**: 1256 tests, 1226 pass, 8 fail, 22 skipped against the pi **0.99.1** entry the test files resolve from the `pi` shim, and **1256 / 1234 / 0 / 22** against a **0.87.1** entry.
- **The eight failures are a pi-version condition, not a sync regression — the snapshot repository shows the identical eight.** Seven are color assertions in `bash-command-collapse/render.test.ts` (5) and `read-path-collapse/render.test.ts` (2) that mutate `theme.fgColors` to prove the dot color is read from the theme at render time; **pi 0.99.1 made that field private** (the singleton now carries `fgAnsi` / `resolvedColors`), so the assertions cannot reach it. The eighth is the `dangerous`-mode case, whose `bypass` control expects the wrapped command to throw and under 0.99.1 gets a normal return carrying the nested-sandbox message instead. Point the loader at a 0.87.1 entry and all eight pass. Both are test-side couplings to pi internals: `bash-command-collapse.ts`'s own `bashOutput` override guards on `typeof fgColors?.set === "function"` and falls through to the plain render, so on 0.99.1 that one cosmetic token is inert rather than broken. Fixing them belongs upstream in `clients/pi/`.
- **The 22 skips** are the real-`sandbox-exec` cases: nested `sandbox-exec` cannot run inside a pi session, so they declare themselves skipped.
- **All 29 entries load through pi's own loader with `errors: []`** against pi 0.85.1, 0.87.1 and 0.99.1 (`discoverAndLoadExtensions`, the check that catches a `ParseError` `node --test` accepts). That is why the documented minimum stays 0.85.1 even though MCP now needs 0.99.1.
- `extensions/` is byte-identical to the snapshot except for `bash-command-collapse/render.test.ts`, which `rsync` overwrites on every sync and which was re-applied here; `themes/*.json` are byte-identical; `config/AGENTS.md` and `config/AGENTS.core.md` were re-copied and are byte-identical to what was already shipped; `config/settings.json` shows exactly the five removed machine-local selections and nothing else.
- The snapshot also gained `config/subagent/config.json` (a companion package's own tuning). It is deliberately not shipped; [docs/configuration.md](docs/configuration.md) explains why.

### Previously unreleased — the background-task dock

Snapshot sync of one earlier upstream commit (`df2e14c`): the **background-task dock** — a reserved statusline key that renders running (and just-finished) background tasks on the footer's bottom line. The extension count stayed at 30 and the suite moved 1238 → 1269 tests.

#### Added

- **`background-tasks` statusline dock** — while a task runs, or for a 10 s linger window after it reaches a terminal state, the footer gains one last line: `⚙ bg_1 running 12s · npm run test --silent…`. The new pure module `extensions/background-tasks/status.ts` owns the wording and coloring (icon `dim`, id `accent`, status word by outcome — running → `warning`, exit 0 → `success`, non-zero and killed → `error` — elapsed `muted`, command `dim`); `index.ts` only publishes and keeps time with a 1 s `unref`'d interval that stops itself and clears the key when there is nothing to show. Exactly one line is ever rendered (most recently started running task wins; otherwise the newest terminal task inside its linger window, the rest folded into `(+N)`). **`(+N)` sits before the command on purpose**: over-width rows truncate only the trailing command, and the count is the one thing that matters with several tasks. During a prompt (`ui_prompt_start` → `ui_prompt_end`) the dock publishes **not even once** — the tick is stopped *and* the event-driven publish is skipped — because pi's main-screen render pins the viewport to the bottom, so a single repaint would drag the user's scrolled-back scrollback down (the same reason `working-indicator` freezes its repaints); `ui_prompt_end` recomputes from current state, so nothing is lost. New switches: `PI_BACKGROUND_TASKS_DOCK=off` (dock line only — tools and notifications unaffected) and `PI_BACKGROUND_TASKS_DOCK_LINGER_MS` (linger window, default 10 000).
- **`statusline/line.ts` reserved-key lift** — `composeFooterLines` pulls the `background-tasks` key out of the concatenated second row and renders it as the footer's last line (`formatExtensionStatuses` skips it), so the dock neither spends the 5-entry budget nor shares a row with a long cwd. The key name is imported from `background-tasks/status.ts`'s `STATUS_KEY` (the same cross-directory import trade-off as `plan-mode`'s): two drifting literals would silently demote the dock back into the second row with no error. The dock line carries its own ANSI and is rendered as-is; width settles in `truncateToWidth`, so id / status / elapsed are never truncated. With no background tasks the footer output is byte-identical to before.
- **31 new test cases** (1238 → 1269): `background-tasks/status.test.ts` (16 — the four outcome shapes, task selection, linger boundary, coarse command truncation, color slots, a `(+N)` truncation regression), eight dock cases in `background-tasks/index.test.ts` (20 → 28 — publish on start, tick advancing elapsed, terminal linger then auto-clear and timer stop, prompt freeze/recovery, **no event-driven publish during a prompt either**, shutdown clears and stops, `DOCK=off` never calls `setStatus`), and seven dock cases in `statusline/line.test.ts` (32 → 39 — the reserved key lifted to its own last line, second-row budget untouched, byte-identical footer when absent).

#### Changed

- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README (the `background-tasks` row gains the dock paragraph, the interactions list gains the reserved-key bullet).
- **English documentation** updated by hand: [README](README.md) (`background-tasks` and `statusline` rows, the switch highlight, test count), [docs/extensions.md](docs/extensions.md) (dock paragraph and test counts in the `background-tasks/` section, two new switches in both tables, the reserved-key interaction bullet, the `statusline/` second-row paragraph), [docs/development.md](docs/development.md) (test counts, the new `line.ts` → `status.ts` cross-directory import in the ship-together list).

#### Notes

- Measured on that sync: 1269 tests, 1268 pass, 1 fail — the then-known-flaky real-spawn MCP handshake in `mcp/client.test.ts` under full parallelism. That test file is gone in 2.2.0 along with the extension.
- `extensions/` is byte-identical to the snapshot except for `bash-command-collapse/render.test.ts` (the standing deliberate delta: the repo-root probe walks up to the first `.git` instead of hard-coding four levels, which only works in the standalone repo).
- The dock needs no minimum-version bump: `ctx.ui.setStatus`, the `ui_prompt_start` / `ui_prompt_end` events and `theme.fg` all predate pi 0.85.1.

## 2.1.6 — 2026-09-27

Snapshot sync of three upstream commits (`587dfe6`, `9ae68c7`, `c4fc934`): a new **`background-tasks`** extension supplying the background-execution primitive pi 0.87.1 lacks, the **watchdog review hint** in `working-indicator`, the **removal of the retired `destructive-guard` extension**, and the statusline's **model icon change** from `⚡️` to `🅼`. The extension count stays at 30 (+1 `background-tasks`, −1 `destructive-guard`) and the suite moves 1304 → 1238 tests. This release folds in the previous "Unreleased" entry (the `background-tasks` sync), which was recorded as unreleased because 2.1.5 was already published to npm.

### Added

- **`extensions/background-tasks/`** — a minimal `run_in_background`. pi's kernel has no background-execution primitive (`run_in_background` appears nowhere in its dist and `ExecOptions` carries only `signal` / `timeout` / `cwd`), so a long task could only block the foreground `bash` tool until its timeout. Three tools mirror Claude Code's trio — `run_in_background` (= `Bash` with `run_in_background: true`), `background_output` (= `BashOutput`, incremental by default with `offset` as an absolute character position), `background_kill` (= `KillShell`, SIGTERM to the whole process group then SIGKILL after a 2 s grace period) — plus `/background` (= `/bashes`: list, `<id>` details with log tail, `kill <id>`). **Completion wakes the model**: a terminal state injects a `<background-task-notification>` and triggers a follow-up turn (immediately when idle, `deliverAs: "followUp"` while streaming), which is why the tool descriptions forbid sleeping or polling. **Tasks live and die with the pi session**: `session_shutdown` calls `killAll()`, and `detached: true` exists only so the command leads its own process group for `kill(-pid)` — the child is not `unref`'d, so nothing survives pi and no cross-restart recovery is needed. Two costs are recorded in the header: a SIGKILLed pi never runs shutdown, and `/reload` ends running tasks. Session *replacement* (`/clear`, `/new`, `/resume`) also fires `session_shutdown` while the extension instance survives, so `session_start` resets the `disposed` flag (otherwise tasks in the new session could never wake the model) and a registry identity check stops a late `exit` from a task the old session killed from injecting into the new one. **Background commands bypass the seatbelt delete boundary** the foreground `bash` tool runs inside — the extension spawns directly, semantically the user running `cmd &` — and both the tool description and `/background`'s output print that warning. Output is written twice: a 256 KB in-memory ring buffer (oldest chunks dropped, reported honestly as `droppedBefore`) and a complete log at `<agentDir>/bg-tasks/<sessionId>/<id>.log`, created synchronously because the tool result hands that path to the model immediately; stdout and stderr merge into one stream (`2>&1`), and a single read is capped at 30 000 characters, returning the tail when exceeded. `registry.ts` is pure logic (spawn, clock, caps and grace period all injectable) and `index.ts` only wires it up. Deliberately absent: recovery across pi restarts, an automatic timeout kill (CC's background bash has none either), agent-type tasks, a footer task dock, and split stdout/stderr. `PI_BACKGROUND_TASKS=off` disables it, `PI_BACKGROUND_TASKS_DIR` moves the log root for test isolation. 36 assertions: `registry.test.ts` (16) drives the full state machine with fake child processes whose pids sit above the system limit, so the group-kill path can never touch a real process; `index.test.ts` (20) loads the real extension through pi's loader and **really spawns** — completion notification, non-duplicating incremental reads, group kill reaching grandchildren, no orphans after shutdown, `/background` details not advancing the model's read offset, `disposed` reset after a session replacement, and a stale registry's late terminal state not injecting into the new session.
- **`working-indicator` watchdog review hint** — `pi-subagents`' watchdog runs an independent reviewer model inside `agent_end` on every turn that changed the repository (measured 7–17 s), and pi awaits every `agent_end` handler before clearing the spinner, so that stretch was a spinner with no explanation. The extension now arms a one-shot timer on `agent_end` (`PI_WORKING_INDICATOR_WATCHDOG_DELAY_MS`, default 2 s) and, if the turn has still not settled when it fires, switches the message to a fixed `Subagent watchdog reviewing` (user-specified wording, no elapsed time or token count). `agent_settled`, `agent_before_settle`, `session_before_compact`, `agent_start` and `session_shutdown` all reset it — the middle two exist so that automatic compaction and `verify-loop`'s `/goal` evaluation are not mislabelled as a watchdog review. A normal turn settles in milliseconds, so the 2 s threshold does not misfire. `PI_WORKING_INDICATOR_WATCHDOG=off` disables it. Five new cases in `index.test.ts` (18 → 23), including the no-misfire case for fast settles.
- **`statusline/line.test.ts`** — two new `MODEL_ICON` cases (30 → 32): the code point is pinned to U+1F17C, and the not-an-RGI-emoji property is asserted, because that is what keeps the glyph one column wide in both pi-tui and the terminal.

### Removed

- **`extensions/destructive-guard/`** — retired from the live environment on 2026-09-24 (the seatbelt capability boundary replaced the lexical blacklist) and now removed from the package too; its 109 assertions leave the suite. The full implementation stays in the git history. Docs updated accordingly: the reference section and the `/destructive-guard` / `PI_DESTRUCTIVE_GUARD` rows are gone from [docs/extensions.md](docs/extensions.md), the extension table row and command list from [README](README.md), and the load-check line from [docs/installation.md](docs/installation.md).

### Changed

- **`statusline` model icon** — `⚡️` → `🅼` (U+1F17C, NEGATIVE SQUARED LATIN CAPITAL LETTER M), painted `dim` like the separators. The difference is measured, not cosmetic: U+1F17C is not an RGI emoji, so pi-tui's `graphemeWidth` falls through to `eastAsianWidth` → Ambiguous → **one column** (Ghostty's default width method agrees), the whole row is one column narrower, no VS16 is needed, and — unlike an emoji — the glyph actually takes a foreground color. The source writes the escape `\u{1f17c}` and the tests pin the code point, because `Ⓜ` (U+24C2) and `🅜` (U+1F15C) are near-identical glyphs. Font availability was scanned with fontTools across 166 JetBrains / Maple files: none cover it, so it renders through system fallback (`Lyth Mono Term` at the same advance as `M`, `LXGW WenKai`, `YuGothic`), the same trade-off as the branch icon.
- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README; it gains the `background-tasks` row with the full design rationale (the CC tool mapping, the wake-on-completion contract, the session-lifetime rules and their two recorded costs, the boundary bypass, the dual output path), the `working-indicator` watchdog row and the destructive-guard removal note in the install list.
- **English documentation** updated by hand: [README](README.md) (extension count 30, test count, a new `background-tasks` row, `/background` in the command list, `PI_BACKGROUND_TASKS` in the switch highlights, `working-indicator` row, destructive-guard row and command removed), [docs/extensions.md](docs/extensions.md) (counts, the `/background` command row, a new `background-tasks/` section, two new switches, an interaction row recording that it sits **outside** the delete boundary, a state-on-disk row for the log root, the statusline icon paragraph, the working-indicator watchdog paragraph and two new switches, the destructive-guard section and rows removed, the `tool_call` hook interaction row rewritten), [docs/development.md](docs/development.md) (test count, the 30-entry loader check), [docs/installation.md](docs/installation.md) (`/background` in the load check, the destructive-guard lines removed).

### Notes

- The suite moves from **1304 to 1238 tests** (measured: 1238 tests, 1216 pass, 22 skipped, 0 fail, ~45 s wall time with `--test-concurrency=4`). The delta is +36 (the new `background-tasks` cases) −109 (the removed `destructive-guard`, matching the "109 assertions" its docs recorded) +7 new (5 watchdog, 2 icon).
- Running the suite through this package's own `run_in_background` tool reports **0 skipped** instead of 22: background commands bypass the seatbelt boundary, so nested `sandbox-exec` becomes available and those cases actually execute (and pass). That is the boundary bypass documented above, observed from inside the suite.
- All 30 entries load through pi's own loader with `errors: []` (verified against pi 0.87.1's real library entry), which is the check that catches a `ParseError` `node --test` accepts; the changed extensions' loader-based suites pass (55/55 for `working-indicator` + `statusline/line`).
- The `background-tasks` extension needs no minimum-version bump: `registerMessageRenderer`, `ctx.isIdle()`, `sendMessage`'s `deliverAs: "followUp"` and `getAgentDir` all predate pi 0.85.1, the version [README](README.md) and [docs/installation.md](docs/installation.md) already state.
- `extensions/` is byte-identical to the snapshot except for `bash-command-collapse/render.test.ts` (the fourth deliberate delta, unchanged by this sync); `themes/*.json` are byte-identical, and the two `*.png` files under `themes/` continue to live in [`assets/`](assets) here.
- `config/AGENTS.md`, `config/AGENTS.core.md` and `assets/pi-coder-palettes.html` were re-copied and are byte-identical to what was already shipped; `config/settings.json` still shows exactly the **four** removed model selections and nothing else.

## 2.1.5 — 2026-09-26

Snapshot sync of six upstream commits: a new **`memory`** extension (Claude Code style auto-memory), plan mode's **three-state permission mode** (`dangerous` / `bypass` / `plan`), a **`brainstorming` ↔ plan mode mutual-exclusion gate**, a fix for **delete refusals masked by an exit-code-0 command**, `tool-diff` switching to `createXToolDefinition` so the `edit` / `write` prompt metadata survives, and a static spinner glyph. The extension count moves 29 → 30 and the suite 1222 → 1304 tests.

### Added

- **`extensions/memory/`** — Claude Code style auto-memory, filling the highest-weighted gap in issue #13 (memory / cross-session learning). Storage follows CC: `~/.pi/agent/memory/<project-slug>/` holds a `MEMORY.md` index plus one file per memory with CC-compatible frontmatter (`name` / `description` / `metadata.type` — `user` / `feedback` / `project` / `reference` — and `modified`), the slug derived from the git root of `cwd`. **The index is derived mechanically, never hand-written by the model** (option C): `memory_write` rebuilds it by scanning every body file's frontmatter, idempotently (identical content is not rewritten), which removes the failure mode both CC and Qoder fight — a memory saved but never indexed, so it is written and never recalled. Injection goes through `before_agent_start` mutating `systemPromptOptions.sections.memory` (discipline text + index; an empty or disabled store injects nothing), so it lands in the system message, replays with the transcript and survives compaction; because the index only changes on writes its bytes are naturally stable and none of the KV-cache snapshot machinery a log-based memory needs. The discipline text carries CC's three gates (applicable / durable / legible), the tense rule (save past-tense observations — measurements, decisions with rejected options, user corrections — never present-tense repo-state claims, which rot), the read-side verification duty (a memory naming a file / function / flag must be re-checked before acting on it) and no secrets. Four tools — `memory_write`, `memory_read`, `memory_forget`, `memory_search` (zero-dependency keyword search, frontmatter hits weighted 3, body hits 1) — all wrapped in `withFileMutationQueue` because tool calls run in parallel; `/memory` is the only user surface (status line, open folder, show index, per-project `.disabled` toggle). `PI_MEMORY=off` disables it, `PI_MEMORY_DIR` moves the root for test isolation. Deliberately not in v1: background dream consolidation (a mount point is left), a USER/PROJECT dual scope, semantic search, and a mechanical write gate. 26 assertions: `store.test.ts` (10) and `context.test.ts` (4) are pure logic, `index.test.ts` (12) loads the real extension through pi's loader and includes a regression assertion for the issue #13 bug where the injected `promptSnippet` was stripped.
- **`extensions/plan-mode/brainstorm.ts`** — the pure judgement behind the mutual-exclusion gate (13 cases).
- **`extensions/bash-command-collapse/sandbox-mode.ts`** — the `globalThis` singleton (`dangerous` / `bypass` / `plan`, default `bypass`) that carries plan mode's permission state to the two delete-interception layers, plus `sandbox-mode.test.ts` (5 cases).
- **`extensions/tool-diff/prompt-metadata.test.ts`** — 6 assertions that the `edit` / `write` prompt metadata survives re-registration.

### Changed

- **`extensions/plan-mode/`** — two states became **three permission states** (user decision 2026-09-27), walked by `shift+tab` in a fixed cycle `dangerous → bypass → plan → dangerous`: `dangerous` (`☢`, red) switches the seatbelt delete boundary off entirely, `bypass` (`⏵`, green — the red moved to `dangerous`) keeps it on and is the default that startup, `/resume` and any unrecognized historical value converge to, `plan` (`⏸`, orange) is read-only exploration. **`dangerous` is reachable only by `shift+tab`** — there is no `/dangerous` command, `/plan` never lands there (it toggles `plan` and returns to `bypass` on the way out), and the model path can only enter `plan`, so turning protection off is always a key the user pressed. `plan` has three exits with different landing states: `shift+tab` → `dangerous` (the cycle's next state, not the way it came, otherwise one press from `bypass` looks like nothing happened), `/plan` → `bypass` (a command should not quietly drop the user into the sandbox-off state), and plan-document-written → `returnPhase` (recorded by `enterPlan`; the user approved the plan, so implementation runs under the posture they had chosen), with the notify saying explicitly that the boundary is off when it returns to `dangerous`. There is still **no execute phase** — the third state is a permission mode, not the progress phase that was deleted on 2026-09-24, and the two have nothing to do with each other.
- **`extensions/plan-mode/index.ts`** — the model's `enter_plan_mode` now checks the **`brainstorming` mutual-exclusion gate** (user decision 2026-09-26) *before* the consent dialog: if this run (everything after the last `role:"user"` message) contains an assistant `read` `toolCall` whose path contains `/brainstorming/` (a fragment match, so a relocated skill library still counts), it neither enters plan mode nor shows a dialog and returns an either-or explanation instead — follow the skill, do not call this tool again, and press `shift+tab` / `/plan` if plan mode is wanted. Only the model path is gated. Any error in the check **fails open** (the dialog shows as usual), because the costs are asymmetric: a false "not loaded" merely restores the old behaviour while a false "loaded" would silently take plan mode away. Two boundaries are recorded in the module header: reading `SKILL.md` with bash `cat` does not count as loading (only the `read` tool does, the same lexical standard `verify-loop` uses), and the exemption covers the current run only.
- **`extensions/bash-command-collapse.ts`** + **`sandbox.ts`** — a delete masked by an overall-successful command is now reported. In `rm <outside> ; <ok>` the kernel refuses the `rm` with `EPERM` but the command exits 0, so pi does not throw and the catch branch (the dialog) never runs — the refusal was swallowed silently. The new `maskedDenialPaths` detects that shape and `annotateMaskedDenial` appends one `[沙箱]` line naming the blocked paths and the authorization exit. It does not prompt and does not rerun (user decision 2026-09-26: annotate only) — the command still counts as succeeded. The false-positive guard is in the same function: a `grep "Operation not permitted" some.log` whose output merely contains those words is not treated as a blocked delete. The seatbelt wrapper now reads `getSandboxMode()` at **execution time** and skips wrapping entirely under `dangerous`; that read is ANDed with the registration-time env gate (`PI_SANDBOX` + platform), so any one of them saying off turns the boundary off, and with plan mode absent or disabled the singleton stays `bypass` and behaviour is unchanged (fail-safe).
- **`extensions/sandbox-boundary/index.ts`** — the `apply_patch` gate reads the same singleton, so `dangerous` switches off both halves of the boundary.
- **`extensions/tool-diff.ts`** — switched from `createEditTool` / `createWriteTool` to **`createEditToolDefinition` / `createWriteToolDefinition`**. The former are `wrapToolDefinition(createXToolDefinition(…))`, and `wrapToolDefinition` keeps only eight fields (`name`, `label`, `description`, `parameters`, `constrainedSampling`, `prepareArguments`, `executionMode`, `execute`), so `promptSnippet` and `promptGuidelines` were silently dropped: the `edit` / `write` rows vanished from the system prompt's `<tools>` section (`visibleTools` filters on `!!toolSnippets[name]`) and the five guidance lines in `<rules>` went with them. Nothing errored — the tools still worked, the model just could not see them. pi's own `examples/extensions/built-in-tool-renderer.ts` uses `createEditTool()` and carries this bug, so the header now says explicitly not to copy that pattern. The previous comment claiming both tools had no prompt metadata was wrong and is corrected.
- **`extensions/simple-task/widget.ts`** — the spinner is a static `▣` instead of the eleven-frame `✳✴✵…` sequence.
- **`config/AGENTS.md`** and **`config/AGENTS.core.md`** — resynced. `## Uncertainty` gains a **"brainstorming and plan mode are either-or"** clause (once `brainstorming`'s `SKILL.md` has been read this run, follow that skill's design → approval flow and do not call `enter_plan_mode`, which the extension blocks silently anyway; the exemption covers the current run only, so a new prompt that did not load the skill gets the normal gate), `## Skills` replaces "brainstorm before plan mode" with the same either-or rule and notes that `brainstorming` **replaces** the plan gate for that run rather than widening or narrowing it, and the `## Plan mode` bullet is rewritten to match.
- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README: the `memory` extension's full design rationale, the three-state permission mode (cycle diagram, the per-mode permission table, the three exits and their landing states, how the sandbox switch travels through the `globalThis` singleton), the `brainstorming` mutual-exclusion gate with its fail-open asymmetry argument, the masked-denial annotation, and the `createXToolDefinition` fix.
- **English documentation** updated for every change above: [README](README.md) (count 29 → 30, the `plan-mode` row rewritten for three states, a new `memory` row, `/memory` in the command list, `PI_MEMORY` in the switch highlights), [docs/extensions.md](docs/extensions.md) (counts, `/memory` and the corrected `/plan` + `/plan-status` rows, the `tool-diff` `createXToolDefinition` warning, the plan-mode section rewritten around the three states and the brainstorming gate, a new `sandbox-mode.ts` section, the masked-denial paragraph, a new `memory/` section, two new switches, the interaction and state-on-disk rows), [docs/development.md](docs/development.md) (counts, the cross-directory import checklist, and the new fourth sync delta below), [docs/installation.md](docs/installation.md) (the three-state indicator in the load check, `/memory` in the command list).

### Fixed

- **`extensions/bash-command-collapse/render.test.ts`** — the new dangerous-mode case resolved the repository root with four hard-coded `..` segments, which is correct in the snapshot (`clients/pi/extensions/bash-command-collapse/` → the repo root) but resolves to `$HOME` here, where `extensions/` sits two levels shallower, so its probe file landed in the home directory and the test failed with `EPERM` on cleanup. It now walks up to the first directory containing `.git`, falling back to `process.cwd()`, which is correct in both layouts. This is the **fourth deliberate delta** from the snapshot and is recorded as such in [docs/development.md](docs/development.md#keeping-this-package-in-sync); `diff -r` on `extensions/` is now expected to show this one file.

### Notes

- The suite grows from **1222 to 1304 tests** (measured: 1304 tests, 1282 pass, 22 skipped, 0 fail, 39.0 s wall time with `--test-concurrency=4`). The delta reconciles exactly: +32 in the changed files (`render.test.ts` 46 → 49, `sandbox.test.ts` 55 → 59, `plan-mode/index.test.ts` 39 → 51, `plan-mode/plan.test.ts` 105 → 114, `plan-mode/render.test.ts` 8 → 10, `sandbox-boundary/index.test.ts` 22 → 24) and +50 in the new ones (`memory` 26, `brainstorm.test.ts` 13, `sandbox-mode.test.ts` 5, `prompt-metadata.test.ts` 6).
- The skips move 20 → 22 and remain the real-sandbox cases in `bash-command-collapse/render.test.ts` (nested `sandbox-exec` cannot run inside a pi session). The new dangerous-mode case deliberately uses `{ skip }` rather than `{ skip: sandboxSkip }` — `dangerous` means *not* wrapping in seatbelt, so it needs no nested sandbox and runs even inside a pi session.
- `extensions/` is byte-identical to the snapshot except for the one test file above (`diff -rq` reports exactly that); `themes/*.json` are byte-identical, and the two `*.png` files under `themes/` continue to live in [`assets/`](assets) here (`ayu1.png` / `ayu2.png` md5-verified identical to the snapshot's copies).
- `config/settings.json` is unchanged by this sync and still shows exactly the **four** removed model selections (`defaultProvider`, `defaultModel`, `modelThinkingLevels`, `subagents.watchdog.main.model`) and nothing else.
- `plan-mode` now imports `../bash-command-collapse/sandbox-mode.ts`, the fourth cross-directory import after `recap` → `simple-task/gap.ts`, `verify-loop` → `recap/subagents.ts` and `sandbox-boundary` → `bash-command-collapse/sandbox.ts` + `allowlist.ts`. `sandbox-mode.ts` is live code with two consumers, so `bash-command-collapse/` cannot be deleted without breaking both `sandbox-boundary` and `plan-mode`.
- The `core-rules` header comment needed no edit: it describes the injected core as ~7 KB and the full `AGENTS.md` as 27 KB, and the resynced files measure 6998 and 28025 bytes, so both figures still hold.

## 2.1.4 — 2026-09-26

Snapshot sync: the delegation playbook gains its **orchestration trigger**. The previous sync restored "once authorized, actively look for parallel opportunities" but never said *which mechanism* to parallelize with, and the measured result was zero calls — `subagent` had been called 6 times (`toolCall`-exact, across the 63 session files the snapshot records), 3 of them real `{agent, task}` dispatches and 3 `list` / `guide` management, with `workflowScript` / `workflowScriptPath` invoked **0** times. Capability was never missing (pi-subagents 0.71.0 ships `runs.run` / `runs.all` / `runs.lanes` / `runs.steer` / `outputSchema` / typed gates / worktree isolation / three budgets, and the tool description itself names `workflowScript`); the trigger was. `## Delegation` now carries a "pick the orchestration mechanism once authorized" clause, the distilled core follows, and `core-rules`' header comment tracks the new sizes. No extension changed behavior: the suite stays at 1222 tests.

### Changed

- **`config/AGENTS.md`** — `## Delegation` gains one clause, **`Pick the orchestration mechanism once authorized`**: one bounded child → a direct `subagent({ agent, task })`; anything needing stable keyed children, sequencing, fanout, steering, retry or aggregation → **one** top-level `subagent` call carrying `workflowScript` (or `workflowScriptPath`) with every child launched inside that script, never a second top-level orchestration and never N ad-hoc direct calls for a shape one script expresses. It prefers a packaged prompt shortcut when the shape fits (`/prompt-workflow parallel-review | review-loop | parallel-research | parallel-cleanup | gather-context-and-clarify | council`) and requires composite workflows to be bounded with `timeoutMs` / `toolBudget` / `usageBudget`. The clause states in its own text that it describes **how** to orchestrate once delegation is authorized and is not a new authorization source. The delegation **gate is untouched** — the authorization sources are still the user's current request, an applicable project instruction, or a skill.
- **`config/AGENTS.core.md`** — the distilled core carries the same clause in condensed form, so the trigger does not decay in a long session (`core-rules` detects it by hash and re-injects with a replacement notice on the next session).
- **`extensions/core-rules/index.ts`** — header comment resynced to the new sizes: the full `AGENTS.md` is now ~27 KB (was ~26 KB) and the injected distilled core ~7 KB (was ~6 KB). The comment is the extension's own documentation of what it injects; the injected content is read from `AGENTS.core.md` at runtime and is unchanged code.
- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README. It gains a new section, *编排触发机制：workflowScript 零调用的病根*, with the `toolCall`-exact measurement and a mechanism-selection table taken from pi-subagents' `docs/workflows.md`; it also **corrects two figures in the previous section** — the "9 calls" count there included `guide` / `status` / `list` management actions and non-`toolCall` mentions (the precise count is 6), and "never dispatched a real subagent" was wrong, since the 3 direct `{agent, task}` calls are real dispatches; what the logs actually show is that nothing ever reached the **orchestration** layer. It also records that `/council` is not a registered slash command (driven by the `council-mode` skill instead), which is why the clause names `/prompt-workflow council` as the real entry point.

### Notes

- The suite is unchanged at **1222 tests** (measured: 1222 tests, 1202 pass, 20 skipped, 0 fail, 37.4 s wall time with `--test-concurrency=4`). The 20 skips remain the real-sandbox cases in `bash-command-collapse/render.test.ts`. This sync touches one comment line and three prose files, so the count moving would itself be a signal that something unintended was copied.
- The zero-call measurement above was re-derived from the session logs for this entry rather than restated: counting `role:"assistant"` `toolCall` parts named `subagent` across the **63** top-level session files that predate the upstream commit, there are exactly **6** calls (3 `{agent, task}`, 2 `list`, 1 `guide`) and `workflowScript` appears **0** times — the snapshot README's figures check out exactly. (The authoring session is excluded from that set because it is the one that opened later and contains the call described below.)
- The clause's **first measured effect** is visible in the same logs, and it did not wait: the session that authored the clause wrote it into the live `~/.pi/agent/AGENTS.md` at 10:39:53 and 10:40:23 (CST), and at **11:04:53** that same session made the first `workflowScript` call in the entire log set — a `runs.run` fan-out to a `scout` child returning a structured recon result. That is 24 minutes after the clause went live, and it is the only `workflowScript` call on record. The timestamps are recorded as an observation, not as proof of causation; what is certain is that the trigger clause is followed by the behavior it was written for, where the previous 6 calls had gone straight to `{agent, task}` or management actions.
- `extensions/` is byte-identical to the snapshot (`diff -r` clean), the single content change in this sync being the `core-rules` header comment above; `themes/*.json` are byte-identical too, and the two `*.png` files under `themes/` continue to live in [`assets/`](assets) here (`ayu1.png` md5-verified identical to the snapshot's copy).
- `config/settings.json` is still the one intentional divergence: a sync's `diff` on it is expected to show exactly the **four** removed model selections (`defaultProvider`, `defaultModel`, `modelThinkingLevels`, `subagents.watchdog.main.model`) and nothing else.
- Version 2.1.3 was already published to npm before this sync, so this one takes its own patch version rather than folding the change into the 2.1.3 entry.

## 2.1.3 — 2026-09-26

Snapshot sync: a new **verify-loop** extension turns the "no completion claims without fresh verification evidence" discipline into code — a gate that forces one more turn when files changed but nothing ran afterwards, and a `/goal` evaluator that judges every turn against a user-set condition. Skill discovery gets no extension at all: the trigger rules moved into the global `AGENTS.md` (native discovery plus prompt reinforcement), and the short-lived `skill-router` experiment upstream was added and deleted before this sync, leaving only its rules behind. The delegation playbook regains the "actively look for parallel opportunities" bullets that a 19→5 condensation dropped, and the shipped `settings.json` turns on `pi-subagents`' watchdog reviewer. The extension count moves 28 → 29 and the suite 1131 → 1222 tests.

### Added

- **`extensions/verify-loop/`** — the verification gate plus the `/goal` evaluator, mirroring two Claude Code mechanisms on pi's `agent_before_settle` boundary ("the final actionable boundary: it can append entries and request one continuation"). **The gate** (CC's `type: "command"` Stop hook): on every settle with `outcome === "completed"` it scans the run since the last user message — file changes (`edit` / `write` / `apply_patch` / `multiedit`, non-document paths) with **no bash command after them** append a `display: true` injection message and force one continuation. The block count is read from the model-visible projection (counting this extension's own injected messages), not from memory: `agent_start` re-fires on every boundary continuation, so an in-memory counter would be zeroed mid-chain and defeat the cap; the projection count is branch-correct, survives resume and needs no mutable state. Cap 2 (`PI_VERIFY_LOOP_CAP`). The verification criterion is **any bash call** — the first live smoke test measured a false positive on `node --input-type=module -e "import('./probe.js')…"` (genuine evidence, no test-runner shape), and a lexical gate cannot judge relevance; `PI_VERIFY_PATTERN=strict` restores the test/build/lint-only pattern or accepts a custom regexp. **`/goal`** (CC's session-level prompt evaluator): `/goal <condition>` (≤ 4000 characters) persists via `appendEntry` and starts a turn immediately; every later settle first defers while a subagent is running (reusing `recap/subagents.ts`'s RPC, CC's "background work defers evaluation"), then makes one tool-less model call (condition + tail-truncated `serializeConversation(convertToLlm(projection))`, 120k characters default) and parses a three-verdict JSON (`met` / `not_met` / `impossible`): not met injects the reason and continues, met or impossible records an entry and clears. Fail-open on failure / timeout / unparseable answers, no-progress detection (2 continuations with zero tool calls stops the loop and keeps the goal), an 8-continuation cap (`PI_GOAL_CAP`), and resume restores an active goal while resetting the turn count. The evaluator model is `PI_VERIFY_EVALUATOR_MODEL` (default `litellm-any/qwen3.8-flash`, falling back to the session model). The one deliberate divergence from CC: no hooks configuration layer exists in pi, so the gate is **on (`block`) by default** with a narrow trigger; `PI_VERIFY_LOOP=off|notify|block` switches it. An active goal shows `◎ /goal active` on the statusline's second row. 91 assertions: `gate.test.ts` (24), `goal.test.ts` (23) and `evaluator.test.ts` (23) are pure logic, `index.test.ts` (21) loads the real extension through pi's loader with a fake subagent bus and model registry.

### Changed

- **`extensions/core-rules/index.ts`** — header comment resynced: the injected body is described as ~6 KB (was ~2 KB) and the list of injected rules now includes the skill-trigger rules.
- **`extensions/plan-mode/index.ts`** — the `enter_plan_mode` tool description gains one bullet: if no brainstorming happened yet, read the `brainstorming` skill's `SKILL.md` (path from the system prompt's `<available_skills>` list) and follow it — clarify point by point, offer 2–3 options with trade-offs — with the plan-mode adaptations (no `docs/superpowers/specs/` writes, no commits, the design document comes out of `exit_plan_mode` into `.pi/plans/`, one-question-at-a-time is `ask_user_question`).
- **`config/AGENTS.md`** — resynced: `## Delegation` rewritten to the Codex-aligned shape (the gate now accepts project instructions and skills as authorization sources, not only the user's own words, and is followed by a full delegation playbook — plan before delegating, bounded sidecar tasks only, disjoint write sets, children edit files directly and report paths, never redo delegated work, wait only when truly blocked), and a new `## Skills` section (native discovery via `<available_skills>`, mandatory trigger check before any response even at 1% likelihood, announce the skill in use, process skills before implementation skills, brainstorm before plan mode, action → tool mapping). A follow-up sync (upstream `623f2c5`) restores two playbook bullets the 19→5 condensation had dropped — once authorized, actively split the same round into disjoint slices with one child per slice and run independent questions out together, and delegate verification only when it can run in parallel with ongoing implementation and catch a concrete risk before final integration. The gate itself is untouched: this is playbook, not authorization (the measured cost of the omission — `workflowScript` saw zero calls across 62 session logs — is recorded in the handbook).
- **`config/AGENTS.core.md`** — resynced with the same moves (the expanded delegation gate plus playbook, including the parallel-opportunity clause, and the condensed `## Skills` block). The distilled core the `core-rules` extension reads grew ~4.4 KB → ~6 KB. As before, it is not shipped as documentation only — install it next to `config/AGENTS.md` or the extension silently does nothing.
- **`config/settings.json`** — gains `subagents.watchdog: { "enabled": true }`: the `pi-subagents` second-model reviewer, on by default upstream since 2026-09-26. On every turn that changed the repository it feeds that turn's diff plus the user's scope to an independent reviewer model (missed constraints, correctness risks, test gaps, unsafe changes, drift); clean turns are silent, `high` findings are pushed back into the model's context, `low` / `medium` are shown to the user only, three identical warnings in a row stop it as a deadlock, and `/subagents-watchdog on|off|status` drives it inside a session. The snapshot's `main.model` (`litellm-any/deepseek-flash-qd`) is removed with the other gateway-specific model selections — without it the reviewer inherits the current session model, the documented fallback. Its Test Gap category overlaps the verify-loop gate on purpose: the gate is deterministic and free, the watchdog is a model judgement that costs a call.
- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README; it now documents the verify-loop design in full (the projection-counting rationale, the measured false positive, the CC mapping table), the delegation gate's Codex alignment (including the `spawn_agent` prompt extracted from the Codex binary), the skill-router removal's resulting shape, the measured consequence of dropping the parallel-initiative bullets and their Codex originals, and the watchdog's switches, model choice and relationship to the verify-loop gate.
- **English documentation** updated for every change above: [README](README.md) (extension table, commands, switch highlights, counts), [docs/extensions.md](docs/extensions.md) (a new `verify-loop` section, the `/goal` command, eight new switches, the core size, the `recap` ↔ `verify-loop` import and storage rows), [docs/installation.md](docs/installation.md) (the `/goal` check), [docs/configuration.md](docs/configuration.md) (core size, the `subagents.watchdog` key row and its removed model selection) and [docs/development.md](docs/development.md) (counts, the cross-directory import checklist, the settings.json diff expectation).

### Notes

- The suite grows from **1131 to 1222 tests** (measured: 1222 tests, 1202 pass, 20 skipped, 0 fail, 37.5 s wall time with `--test-concurrency=4`). The 20 skips remain the real-sandbox cases in `bash-command-collapse/render.test.ts`.
- `verify-loop` imports `recap/subagents.ts` across directories, so the two must be installed together — the third cross-directory import after `recap` → `simple-task/gap.ts` and `sandbox-boundary` → `bash-command-collapse/sandbox.ts` + `allowlist.ts`.
- Upstream added and deleted a `skill-router` extension between this sync and the last one; its net effect is the skill-trigger rules now living in `AGENTS.md` / `AGENTS.core.md`, which this sync carries. Nothing named `skill-router` ships here.
- `config/settings.json` now differs from the snapshot in **four** places instead of three: the watchdog's `main.model` joins the removed model selections, while `watchdog.enabled` itself is kept. A sync's `diff` on that file is expected to show exactly those four.

## 2.1.2 — 2026-09-24

Snapshot sync: the delete protection moves from a **lexical blacklist to an OS capability boundary**. A new `sandbox-boundary` extension closes the non-shell half, `bash` commands run inside a seatbelt profile, a second new extension (`core-rules`) keeps the distilled global rules in context, and plan mode is realigned with Claude Code — an approved plan becomes a document, the task-mirror machinery is gone, and the model's own `enter_plan_mode` now asks for **consent** with all routing criteria moved into its tool description. `destructive-guard` is **retired from the live environment** (the repository copy and its tests stay as the reference implementation of the blacklist route). The extension count moves 26 → 28 and the suite 953 → 1131 tests.

### Added

- **`extensions/sandbox-boundary/`** — the non-shell half of one delete boundary. `bash` is already wrapped in a seatbelt profile, but `write` / `edit` are direct `fs` calls in the extension process that no shell profile can reach; the only capability the sandbox takes away is `file-write-unlink` outside the boundary, so this is the only thing this extension guards. `write` / `edit` / `multiedit` always pass — they create or overwrite content and make no inode disappear (the reversibility of an overwrite belongs to git and the `AGENTS.md` discipline). `apply_patch` has its `*** Delete File:` lines checked, and it shares `classifyOutsidePaths` and the same allowlist singleton with the bash side, so a directory remembered on one side takes effect on the other immediately. Unlike bash it runs on the `tool_call` hook, so it knows every target **before** execution and has no "run the command again" cost. A never-delete path rejects the **whole** patch, with no option to approve the rest. 22 assertions.
- **`extensions/bash-command-collapse/sandbox.ts`** — a deny-default seatbelt profile: reads and network are unrestricted, writes are globally allowed, and `file-write-unlink` is denied first and then allowed for the delete roots (project directory, the temp roots `/tmp` / `/private/tmp` / `/var/folders` / `/private/var/folders` / `/var/tmp` / `/private/var/tmp`, the regenerable caches in `SAFE_CACHE_HOME_DIRS`, and `PI_SANDBOX_EXTRA_WRITE`). It is a **whitelist**, not a list of dangerous paths, so it needs no maintenance as the danger list changes. The same file holds the boundary model, `isPathInWriteBoundary`, `classifyOutsidePaths` (five verdicts, the new one being **never-delete**), the dangerous / never-delete / safe-cache tables and the three-tier scope functions. Pure logic, no pi imports. 55 assertions.
- **`extensions/bash-command-collapse/allowlist.ts`** — the persistent allowlist at `~/.pi/agent/sandbox-allowlist.json` (`PI_SANDBOX_ALLOWLIST` moves it), a `globalThis` singleton shared by both sides. Three defences: `remember()` only accepts roots that pass `isSafeAllowlistRoot` (never a dangerous root, a never-delete path, or an ancestor of a dangerous root), the file is filtered again on load, and writes are atomic (temp file + rename). A corrupt, unreadable or unknown-version file degrades to empty and never throws — the failure direction of a gate whose job is to prompt less must be "ask once more". 21 assertions.
- **`extensions/core-rules/`** — pushes the distilled global rules back to the end of the context. `~/.pi/agent/AGENTS.md` is a user-level file that pi renders into `<project_context>` at the **front** of the system prompt, buried inside a 124 KB blob, so its recency decays as the conversation grows. This extension injects `~/.pi/agent/AGENTS.core.md` (~2.5 KB) as a `before_agent_start` message — which pi appends after the user message and persists in the session — at the three moments Codex uses: session start, the first turn after a compaction, and when the content actually changed. All three come from one decision (`decision.ts`): scan the model-visible projection for its own message — absent means start or compacted away, present with a different hash means changed, same hash skips. Nothing is sent when nothing changed. A missing rules file skips silently. `PI_CORE_RULES=off` disables it. 11 assertions.

### Changed

- **`extensions/plan-mode/`** — realigned with Claude Code: **two phases** (`bypass` → `plan`), no `execute`. `exit_plan_mode` now takes Claude Code's shape — `plan` (the full markdown for the user, rendered in the approval dialog), a **required `slug`** (lowercase English, 3–5 hyphenated words, e.g. `m5-entity-runtime`; the file becomes `.pi/plans/<date>-<slug>.md`) and an optional `summary`. Approval is a three-way choice — write the plan document and implement it, write the document only, or reject — and the plan file is written with `write` while a `tool_call` hook pins that tool to the single approved path; a `tool_result` hook sees the write succeed and closes the phase itself. The whole progress machinery is **deleted**: the `plan: n. ` mirror, `[DONE:n]`, the steps widget and the per-turn reinjection are gone, and progress is handed back to the model (it builds a task list with `task_set` if it judges one is warranted). The old `normal` phase is renamed `bypass`, with an old on-disk value normalized through a whitelist so a stale plan does not come back to life. The approval dialog truncates to one screen (`truncatePlanForDialog` measures with pi-tui's own `wrapTextWithAnsi`, so the height matches the real render, CJK included) because pi pins the viewport to the bottom on every repaint and the confirm dialog cannot scroll. The model's entry path also gained two Claude Code mechanisms: `enter_plan_mode`'s tool description now carries **all** the routing criteria (7 positive conditions, 4 exemptions, GOOD/BAD examples — the global `AGENTS.md` keeps a single pointer instead of a second copy), and calling it opens a **consent dialog** (`进 plan mode（只读探索）` / `直接实施`; Esc counts as refusal) before entering, so a misjudgement costs the user one keystroke instead of a forced planning round. The dialog is model-path only — `shift+tab` / `/plan` / `--plan` are already the user's own decision — and headless runs (`pi -p`) skip it. `PI_PLAN_MODE_CONSENT=off` drops it.
- **`extensions/read-path-collapse.ts`** — fixed a `resolvePath` argument-order bug: pi's `resolvePath(input, baseDir)` (`utils/paths.js`) has the **opposite** argument order to node's `resolve(base, target)`, and this file aliases node's `resolve` under pi's name to mirror its source shape, so `resolvePath(filePath, cwd)` silently ran node semantics — an absolute path returned `cwd`, the "inside cwd" test passed, and the collapsed label degenerated to `"."` (reading `~/.pi/agent/AGENTS.md` rendered as `Read resource .:67-80` at width ≤ 66). Files inside cwd happened to be correct, which is why only a path **outside** cwd exposes this class of bug; those two lines now go through `resolveLikePi(input, baseDir)`, and a regression assertion pins the outside-cwd label. 12 assertions.
- **`extensions/simple-task/`** — decoupled from plan mode. The mirror contract, the id-space separation and the header suppression for mirrored entries are gone; the widget is a plain task list again, and `recap → simple-task/gap.ts` is now the only cross-directory import in the package. All three tools additionally register with `renderShell: "self"`, so their blocks carry **no background and no boundary blank lines** — the same shell as the bash and read blocks; `renderCall` / `renderResult` return `new Text(…, 1, 0)`, whose `paddingX = 1` puts back the single column of left margin the default `Box(1, 1)` used to draw (one leading space per line, not flush-left) while `paddingY = 0` keeps the boundary blank lines away. 3 new end-to-end shape assertions in `render.test.ts` (with a control case asserting other tools keep their background).
- **`extensions/destructive-guard/`** — kept as the blacklist reference implementation, and it grew the three rules the second incident exposed anyway: `outside-workdir` (a delete target inside `$HOME`, outside the working directory and not under a temp root now needs confirmation — this is the `AGENTS.md` assertion that had never reached code), `self-protection` (the guard's own directory, `~/.pi/agent/AGENTS.md` and the `extensions` / `sessions` / `rewind` subtrees are **blocked**), and `vcs-history-loss` (`git reset --hard` / `checkout --` / `restore` / `stash drop|clear` / `branch -D` need confirmation; branch switching and soft resets do not). Gate three was restored: before running a script (`node x.mjs`, `./x.sh`, an interpreter heredoc) the **file is read and judged**, which is the only place that one-line incident could have been caught. The confirm dialog was rewritten in plain language and `splitWords` gained `scanSubstitution`, so `$(dirname "$LOG")` is one word instead of a path shredded by spaces. 109 assertions, with 23 everyday commands as false-positive regressions.
- **`extensions/bash-command-collapse.ts`** — `execute` wraps the command in `sandbox-exec`. A delete outside the boundary is refused by the kernel (`EPERM`), the extension asks once, and only then reruns the command **inside** the sandbox with the approved range added to the profile, so the rest of the command stays supervised; the same command is not asked twice in a session, non-interactive environments fail closed. Authorization is **three-tier** by target path: **never-delete** (identity / credentials / hand-written config — `~/.zshrc`, `~/.gitconfig`, `~/.env`, `~/.bash_history`, `~/.envrc`, `~/.tool-versions` and 43 other home-level files, plus the `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.kube`, `~/.docker`, `~/.azure`, `~/.gcloud`, `~/.terraform.d`, `~/.helm`, `~/.minikube`, `~/.password-store` subtrees and the two nested entries `~/.config/gh` and `~/.config/gcloud`) gets **no dialog and no way to allow it**, because the profile emits its `deny` line after the `allow` lines and seatbelt lets the later rule win — the allowlist, a session exemption and `PI_SANDBOX_EXTRA_WRITE` all lose to it; **dangerous** (system roots, `bin`, application install directories, `~/Library`, anything containing a VCS store) is asked every time with only a session-scoped exemption; **ordinary** is asked once and can be remembered permanently. `~/.config`, `~/.pi`, `~/.claude` and `~/.codex` sit in the ordinary tier on purpose: they are **tool-state directories** whose locks, caches and session logs need routine cleanup, and locking the whole subtree made even pi's own stale-lock cleanup fail with kernel `EPERM` (`pi update --extensions` exited 1). The stated cost is that the guard's self-protection — `extensions/`, `AGENTS.md`, `sessions/`, `rewind/` and the allowlist file all live under `~/.pi` — drops from "the kernel refuses unconditionally" to "a dialog plus explicit consent". Because the never-delete list now carries nested entries, `isSafeAllowlistRoot` / `isSafeSessionRoot` check the **ancestor gate against the dangerous tier only**; otherwise `~/.config` would be unrememberable forever as "an ancestor of `~/.config/gh`". Safety is unchanged: the kernel deny lines reclaim never-delete subtrees after the allow lines, and `classifyOutsidePaths` judges `blocked` before the allowlist, so remembering `~/.config` never hands over `~/.config/gh`. The dialog options are now `Deny` / `Allow once` / `Allow for this session`. Blocked paths are extracted from the failure output by **exclusion** rather than by a program-name allowlist (which silently dropped the python3 `PermissionError`, `find:` and `ln:` shapes), and when nothing can be extracted the extension **no longer prompts and no longer reruns outside the sandbox** — it reports the original error plus a `[沙箱]` line pointing at `/sandbox-boundary allow <directory>`. That fallback was the entry point for the heredoc false positive (`cannot create temp file for here document: EPERM` read as "wants to delete `/bin/bash`"), now fixed at the source by putting `/var/tmp` — bash 3.2's compile-time heredoc temp directory — inside the boundary.
- **`extensions/working-indicator/`** — subscribes to `ui_prompt_start` / `ui_prompt_end`: during a dialog the 1 s tick and the bash blink timer stop, `refresh` returns early, and the spinner is frozen to a single frame (`⠿`). A `Loader` with `frames.length <= 1` starts no animation timer, which is what actually stops pi-tui's 80 ms `requestRender` — 12.5× faster than the tick and the main reason the viewport stayed pinned to the bottom.
- **`config/AGENTS.md`** — resynced: a new `## Delegation` section, the plan gate changed from "anything past a single file" to **default admission** (only an obviously trivial one-file fix is exempt), a "surface a material tradeoff" rule, a rewritten `## Task list` (an approved plan is no longer pre-loaded into the list), and two new `## Verification` rules. A later resync rewrote `## Destructive actions` as six **time-ordered** rules (pick the checkable shape → build the target from literals → check it against the deny list → a script prints every resolved target before deleting → answer the blast radius → act small and recoverable), collapsed the three divergent copies of "when to stop and ask" into one **Ask-triggers** list under `## Blast radius`, reconciled "authorization persists across turns" with "approval does not spread" into *scope persists, not per-instance approval*, and the plan gate now points at the `enter_plan_mode` tool description for its criteria. The latest resync adds a `### When the sandbox blocks you` section (what seatbelt denies and what does not, why `rename` counts as an unlink, the three exits, and that tool-state directories are ordinary-tier), trims the deny-list bullets down to the kernel-enforced ones plus the two gates nothing else enforces (repo roots, temp roots), and drops two `## Task list` lines that duplicated the `simple-task` tool descriptions.
- **`config/AGENTS.core.md`** — resynced with the same three moves (Ask-triggers, scope-vs-approval, the plan-gate pointer), plus a `## Persistence` section, a `## Communication` section and the `EPERM`-means-sandbox-boundary bullet with its exits (never-delete paths have none; tool-state directories are ordinary-tier). The distilled core the `core-rules` extension reads grew ~2.5 KB → ~4.4 KB. It is not shipped as documentation only — install it next to `config/AGENTS.md` or the extension silently does nothing.
- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README; the two-phase plan mode, the three-tier authorization, the seatbelt boundary, the retirement of `destructive-guard`, the consent dialog with the tool-description routing criteria, and the `resolveLikePi` signature trap are all described there in full.
- **English documentation** updated for every change above: [README](README.md) (extension table, counts, commands), [docs/extensions.md](docs/extensions.md) (new `core-rules` and `sandbox-boundary` sections, a rewritten `plan-mode` with the consent dialog and routing criteria, the `simple-task` decoupling, five new switches, the `resolveLikePi` trap in the `read-path-collapse` section, the state table), [docs/installation.md](docs/installation.md) (the load checklist and the new config file) and [docs/development.md](docs/development.md) (counts and the helper file list).

### Removed

- **`extensions/simple-task/plan-mirror.ts`**, **`extensions/simple-task/plan-mirror.test.ts`** and **`extensions/plan-mode/mirror.test.ts`** — the plan-mode ⇄ simple-task mirror contract and its integration test. An extension should not own a progress table the model is perfectly able to keep; the contract existed only to keep two owners of one number in sync.
- **`destructive-guard` from the live environment** — retired to `~/.pi/agent/retired-extensions/` upstream (renaming it does not work: pi looks for `index.ts` in every subdirectory). The package still ships it as the reference implementation; if you install it, it runs alongside `sandbox-boundary`, and the two answer different questions (lexical judgement at any time vs. OS-enforced deletion outside the boundary).

### Unchanged

- No theme moved: `themes/*.json`, `assets/ayu1.png` and `assets/ayu2.png` were already byte-identical to the snapshot. `config/settings.json` still differs from the snapshot only in the three removed model-selection keys. `assets/pi-coder-palettes.html` was resynced from the upstream copy (the palette page's structure was simplified and its cross-reference table now lists the full `colors` slot union).

### Notes

- The suite grows from **953 to 1131 tests** (measured: 1131 tests, 1111 pass, 20 skipped, 0 fail, 37.5 s wall time with `--test-concurrency=4`). The 20 skips are the real-sandbox cases in `bash-command-collapse/render.test.ts`: nested `sandbox-exec` is unavailable inside a pi session, so they report themselves as skipped instead of faking a pass.
- `plan-mode` and `simple-task` are **independent again**; `recap` and `simple-task` still must be installed together through `gap.ts`. `sandbox-boundary` imports `bash-command-collapse/sandbox.ts` and `allowlist.ts`, so those two must be installed together.
- The delete boundary is macOS-only: without `sandbox-exec` (or with `PI_SANDBOX=off`) both the bash profile and the `apply_patch` gate are disabled, and `destructive-guard` — if you install it — is the only remaining delete protection.

## 2.1.1 — 2026-09-23

Snapshot sync: a new **destructive-guard** extension gates deletes before any tool runs, plan mode stops drawing its own progress table and mirrors its steps into `simple-task` instead, and the global `AGENTS.md` gains the destructive-action and blast-radius discipline. The extension count moves 25 → 26 and the suite 839 → 953 tests.

### Added

- **`extensions/destructive-guard/`** — a `tool_call` hook that inspects arguments **before** execution and rejects dangerous deletes. Two gates: delete-shaped commands (`rm` / `unlink` / `shred` / `truncate`, `find … -delete`, `find … -exec rm`, `git clean -fdx`, `rsync --delete`, PowerShell `Remove-Item`) have their targets judged in three tiers — block (fewer than two path components, a protected root, an ancestor of one), confirm (system trees, VCS store roots, fallback-carried targets, computed targets) and ok; and the **content** of `write` / `edit` / `multiedit` / `apply_patch` is scanned for the same shapes, closing the "write the script now, run it later" hole a command check cannot see. `block` refuses outright, `confirm` asks once in the TUI and fails closed without one. `PI_DESTRUCTIVE_GUARD` offers `on` / `block` / `notify` / `off`, and `/destructive-guard` prints the mode plus this session's checked / blocked / confirmed / allowed / notified counts. It is the direct answer to the `rm -rf /` incident the upstream handbook describes. 81 assertions across three test files; `targets.ts` and `writes.ts` are pure logic with no pi imports.
- **`extensions/simple-task/plan-mirror.ts`** — the single contract between plan-mode and simple-task: event names (`plan-mode:sync-tasks`, `simple-task:state`), the `plan: n. ` mirror prefix, and the rebuild rules. Both extensions take the contract from this one file; plan-mode statically imports it, so the two must be installed together. Covered by `plan-mirror.test.ts` (17 cases).
- **`extensions/plan-mode/mirror.test.ts`** — an integration test that loads both extensions onto one event bus and runs the whole chain: `enter_plan_mode` → `exit_plan_mode` → `plan: 1. …` appears in the task list → `task_update` → the statusline reads 1/2 (7 cases).

### Changed

- **`extensions/plan-mode/`** — on plan approval the steps are **mirrored into simple-task** (id = step number) and plan-mode drops its own `plan-steps` widget; the statusline's `▶ n/N` reads the mirrored state back, and `[DONE:n]` in prose survives as an equivalent alias for `task_update`. The reason is measured: in one real execution round the model called `task_update` 17 times and never wrote a single `[DONE:n]`, so a marker-only counter stayed at `▶ 0/10` forever. Three follow-up fixes: state restore reads `getBranch()` rather than `getEntries()`, so a plan discarded on another branch no longer comes back to life; the mirror is re-pushed based on whether it still exists, and cleared on re-planning; and the execute-phase injection now teaches the two-step `task_update` (`pending → in_progress → done`) instead of colliding with the global "never straight to done" rule. The suite grows 165 → 181 assertions.
- **`extensions/simple-task/`** — serves the mirror: it answers `plan-mode:sync-tasks` with full-state broadcasts, self-heals before serving (so a reopened session does not lose hand-built tasks), keeps mirror entries and hand-built tasks in separate id spaces (a colliding hand-built task is bumped above `nextAvailableId`), and omits the `● N tasks (…)` widget header when the list contains mirrored entries — the same totals are already in the statusline.
- **`config/AGENTS.md`** — resynced from the snapshot: the `## Uncertainty` section (look before assuming, decide by reversibility, plan-mode entry), a rewritten `## Destructive actions` (never derive a delete target, never let a fallback reach a delete, a deny-list assertion, temp roots are for creating in), and a `## Blast radius` class table.
- **`docs/handbook.zh.md`** — resynced verbatim from the snapshot README; it now documents the mirror contract's four load-bearing rules and the destructive-guard install line.
- **English documentation** updated for every change above: [README](README.md) (extension table, commands, switches, counts), [docs/extensions.md](docs/extensions.md) (a new `destructive-guard` section, the plan-mode mirror rules, the simple-task mirror role, two new interaction bullets, the switch table), [docs/installation.md](docs/installation.md) (the `/destructive-guard` check) and [docs/development.md](docs/development.md) (counts).

### Unchanged

- No other extension, theme or config file moved: the snapshot diff was exactly the files listed above. `config/settings.json` still differs from the snapshot only in the three removed model-selection keys.

### Notes

- The suite grows from **839 to 953 tests**: 81 destructive-guard, 16 plan-mode (the 7-case mirror integration test plus 9 new index wiring cases) and 17 simple-task plan-mirror cases, ~38 s wall time.
- `plan-mode` and `simple-task` must now be installed together (plan-mode statically imports the contract module); `recap` and `simple-task` already shared that requirement through `gap.ts`.

## 2.1.0 — 2026-09-22

Snapshot sync: a Claude Code style **plan mode** arrives as a new extension, the bash and read blocks lose their backgrounds and gain a `• ` status dot, `pi-coder-summer-night` is replaced by `pi-coder-1337`, `/recap` becomes idempotent, and the two older themes drop the `vars` entries nothing references any more. The extension count moves 24 → 25 and the suite 651 → 839 tests. The sync also removed the stale `themes/pi-coder-summer-night.json` that a plain `cp -R` had been leaving behind since 2.0.0.

### Added

- **`extensions/plan-mode/`** — Claude Code style plan mode: `normal` → `plan` → `execute`. `shift+tab` or `/plan` enters planning, `--plan` starts in it, and the model can call `enter_plan_mode` itself; `exit_plan_mode` submits the plan for approval, `[DONE:n]` advances the steps, and the phase plus steps are stored in the session log (`appendEntry("plan-mode")`) — nothing is written to the working tree. Two independent gates keep planning read-only: `edit` / `write` / `powershell` are removed from the active tool set and **restored from a snapshot** on exit (a hardcoded whitelist would silently drop the twenty-odd extension-registered tools), and a `tool_call` hook rejects write-shaped `bash` commands such as `rm`, redirection, `git commit` and `npm install`. It is a guardrail for a cooperative model, not a sandbox. `shift+tab` is intercepted with `ctx.ui.onTerminalInput` before the editor sees it, because a conflicting `registerShortcut` is skipped by pi's runner, and `app.thinking.cycle` is rebound to `ctrl+shift+t` when that key has no binding of its own. `PI_PLAN_MODE=off` disables the extension, `PI_PLAN_MODE_AUTO=off` only disables the model's tool. 165 assertions across five test files.
- **`extensions/read-path-collapse/render.test.ts`** — 11 end-to-end assertions for the read block, rendered through pi's own loader: the `• ` dot column, its per-state color, the two-column indent, the absence of a background and of boundary blank lines, and a control case proving other tools keep pi's default shell.

### Changed

- **`extensions/bash-command-collapse.ts`** — the block has **no background** and **no boundary blank lines** any more (`renderShell: "self"` plus a `Box` that deliberately carries no `bgFn`; `paddingY: 0`). State moved to a `• ` dot at the head of the **first** line: `dim` while running, `toolDiffAdded` on success, `toolDiffRemoved` on failure — the same coloring the short-lived `▎` bar used. The dot column and the result indentation share one constant, so `Run`, `│` and `└` all sit in column 2 and every body column starts at 4; the left margin is drawn by the extension and both sides subtract it from their width budget. Only bash loses its background — every other tool keeps pi's shell. The render suite grows 20 → 25 cases.
- **`extensions/read-path-collapse.ts`** — the same shell treatment as bash: `renderShell: "self"`, no background in any of the three states, no boundary blank lines, a `• ` dot at column 0 (dim while reading, `toolDiffAdded` / `toolDiffRemoved` after), `Read` at column 2 and the result body indented to the same column with pi's leading blank line stripped. Paths are painted `text` rather than pi's `accent`, so the label and the path do not merge on themes where both are the same color.
- **`extensions/statusline/line.ts`** — a `STATUS_PRIORITY` table puts `plan-mode`'s mode indicator at the **head** of the second row; the remaining entries keep registration order and the 5-item cap. The second row truncates instead of wrapping, so an indicator that trails the ever-growing cwd path can be pushed out of sight, and registration order alone depends on directory names.
- **`extensions/simple-task/index.ts`** — the packed `✔ n/N` status it used to write into the statusline's second row is gone; the widget above the editor already carries the same information, and that slot now belongs to the mode indicator. The same edit drops the unused `GLYPHS` import.
- **`extensions/recap/`** — `/recap` is **idempotent**: when a summary for the current exchange is on screen, running it again returns immediately instead of re-running the model. The fingerprint (last user+assistant pair plus model) is computed in one place, `latestExchange()`, and shared with the automatic path's de-duplication; a failed repeat used to replace the summary with a "could not generate" notice.
- **`extensions/user-message-bar/`** — the bar's default color is the theme's `accent` (fallbacks `selectedBg` → `toolDiffAdded` → `text`) rather than the added-line green.
- **`extensions/theme-command.ts`** — a `Spacer(1)` between the theme list and the color swatches.
- **`themes/pi-coder-1337.json`** — new, and now the theme `config/settings.json` selects. It ports Codex CLI's built-in syntax theme `1337`, re-derived from the theme blob embedded in the local codex executable and reconciled scope by scope against upstream (`1337.tmTheme`): 48 named scopes matched, 37 byte-identical. Six values (`toolDiffAdded` / `toolDiffRemoved` / `toolDiffAddedBg` / `toolDiffRemovedBg` / `thinkingXhigh` / `thinkingMax`) are locked to the removed summer-night palette instead; the cost is that the two diff line backgrounds are 1.02 / 1.01:1 against the new `#202020` success card, i.e. nearly invisible inside a dark diff block. `error` shares one `vars.removedRed` with `toolDiffRemoved` rather than taking 1337's `markup.deleted`. 35 `vars`, 59 colors, no integer or literal values outside `vars`.
- **`themes/pi-coder-summer-night.json`** — **removed.** The file was deleted upstream, and the previous syncs had been copying over it without deleting it, so 2.0.0–2.0.5 shipped a theme that no longer existed in the snapshot.
- **`themes/pi-coder-ayu.json`** and **`themes/pi-coder-catppuccin.json`** — `vars` is trimmed to the entries `colors` / `export` still reference: catppuccin loses six (`rosewater`, `flamingo`, `pink`, `lavender`, `surface2`, `base`; 37 → 31) and ayu three (`editorLine`, `yellow`, `toolPendingBg`; 30 → 27). The blanked `toolPendingBg` values left their variables referenced by nothing — harmless on load, since an *unresolved* reference is what throws `Variable reference not found` and drops the whole theme to `dark`, but misleading to read. `pi-coder-1337` was already at 35 with no spare. catppuccin's `pendingPanel` (`#0b151f`) is the one spare kept on purpose, as the ready value should the pending background ever return.
- **`assets/pi-coder-palettes.html`** and **[docs/handbook.zh.md](docs/handbook.zh.md)** resynced (both byte-identical to the upstream copy again; the palette page's swatch lists and counts follow the `vars` trim, and the handbook carries the Chinese rationale for the plan mode, the dot, the 1337 port, the recap gate and the trimming rule, plus a gateway note about images returned by tools).
- **English documentation** updated for every change above: [README](README.md) (extension table, theme section, commands, switches, counts), [docs/extensions.md](docs/extensions.md) (the two block sections, a new `plan-mode` section, the statusline priority rule, `simple-task`, `recap`, the switch table), [docs/themes.md](docs/themes.md) (the 1337 port in full, catppuccin's grey deviation, the `vars` trimming rule, the custom-token table), [docs/configuration.md](docs/configuration.md), [docs/installation.md](docs/installation.md) and [docs/development.md](docs/development.md) (counts, the helper-only directory list, and a sync procedure that uses `rsync --delete`).

### Unchanged

- No other extension, theme or config file moved: the snapshot diff was exactly the files listed above. `config/settings.json` still differs from the snapshot only in the three removed model-selection keys.

### Notes

- The suite grows from **651 to 839 tests** (165 plan-mode assertions, 11 read render assertions, 5 bash render assertions, 1 statusline case and 1 recap case), ~36 s wall time.
- `/recap`'s idempotence, the mode indicator's fixed slot and the two blocks' shared left edge are all recorded upstream in the handbook, because each of them is a decision that looks like it could be simplified and cannot.

## 2.0.5 — 2026-09-21

Snapshot sync: the bash tool call was rebuilt as a `Run ` command block on a single tree, a failed command is now recognized and painted red, and `user-message-bar` moved its default color from the added-line green to the skin's accent. The command shape and its 22 render assertions arrived together in a directory of their own, so the README's switch table no longer lists the long-gone `PI_BASH_TREE`, and the two count rows (`24 extensions`, test counts) were corrected.

### Added

- **`extensions/bash-command-collapse/render.test.ts`** — 22 end-to-end assertions for the bash block, rendered through pi's own loader and `ToolExecutionComponent` (`bash-command-collapse/` has no `index.ts`, so pi never loads it as an extension — the top-level file imports nothing from it, the directory exists for these tests). They pin the command shape (`Run ` prefix, 2 visual lines, trailing `…`, `… +N lines`), the continuation-column alignment, the single `└ `, the failure colouring in both states, the preview keeping the status line, and the visible width of every line against the terminal width.

### Changed

- **`bash-command-collapse.ts`** — the command block is now `Run ` + **at most 2 visual lines**, the second row's overflow replaced by a trailing `…` and a `… +N lines` marker when whole source lines are left over (the old shape was 3 visual lines plus `… (123 tokens hidden)`). Continuation rows and the marker align their body to the `n` of `Run `: two spaces while the command is starting, `│ ` once it has finished. Only the word `Run` is bold — wrapping the whole prefix would bold the trailing spacing cell too. Results hang off the same tree and `└ ` appears **once**, on the first real output line: the truncation hint keeps `│ `, everything below the corner (further output, warnings, `Took Xs`, `(no output)`) is indented to the body column without a bar, because the tree has already landed there. `PI_BASH_TREE` is retired — the prefix is always a tree — so `PI_BASH_STREAM`, `PI_BASH_PREVIEW`, `PI_BASH_HIGHLIGHT`, `PI_BASH_MIN_TIME_MS` and `PI_BASH_SPINNER` are the remaining switches.
- **`bash-command-collapse.ts`** — a failed command is painted with the `error` slot instead of `success`, in the collapsed and the expanded view alike. pi appends the status as ordinary output (`appendStatus` writes `\n\n` + `Command exited with code N` / `timed out after N seconds` / `aborted`, and replaces the body with `(no output)` when there was none), so the blank line in front of it used to render as a gap with no prefix — right where the tree should continue. The status is now peeled off together with that blank line, the blank line is dropped, the status becomes a row the preview always keeps (otherwise preview trimming dropped it and left only `│ … (N earlier lines)`), and blank lines **above** the `└ ` get the `│ ` bar back so the fence does not break on them. Blank lines **below** the corner stay blank. Two conditions gate the colouring — `isError` from `context` and the status line's shape — because shape alone would repaint `echo "Command exited with code 2"`.
- **`user-message-bar/`** — the bar's default colour is now the theme's `accent` (fallbacks `selectedBg` → `toolDiffAdded` → `text`) rather than `toolDiffAdded`, so the bar reads as the skin's emphasis colour; `PI_USER_MESSAGE_BAR_COLOR=toolDiffAdded` restores the previous green on the two themes where the two slots differ.
- **`theme-command.ts`** — the picker puts a `Spacer(1)` between the theme list and the colour swatches. Both are multi-line blocks and read as one region when they touch.
- **`themes/pi-coder-summer-night.json`** — `syntaxComment` points at `dimText` (`#6b7089`) instead of the computed `commentBright` (`#6e7dc0`), so code comments match the `Think:` row and the settings hints; the cost is 4.36:1 → 3.50:1 against `night`, which is this palette's lowest tier. `commentBright` stays in the file unreferenced. `successCard` and `errorCard` are both `#161616` now, so the finished and failed tool cards no longer differ in temperature — only their foregrounds do; the shared value sits between the two it replaces (`#11171d` / `#191319`) at 1.133:1 against the terminal background and 1.059:1 against `night`.
- **`assets/pi-coder-palettes.html`** and **[docs/handbook.zh.md](docs/handbook.zh.md)** resynced (the palette page is byte-identical to the upstream copy again, the handbook now carries the Chinese rationale for the four changes above).
- **English documentation** updated for the same four changes: [docs/extensions.md](docs/extensions.md) (the bash block section, both `user-message-bar` switches, the retired switch row, the directory count), [docs/themes.md](docs/themes.md), [docs/installation.md](docs/installation.md) (the post-install checklist), [docs/development.md](docs/development.md) and [README](README.md).

### Unchanged

- The suite grows from **624 to 651 tests** (22 bash render assertions, five `user-message-bar` cases), ~36 s wall time. No other extension, theme or config file moved: the snapshot diff was exactly the files listed above.

## 2.0.4 — 2026-09-20

Snapshot sync: the statusline branch icon moved to a code point no font on this machine covers, all three themes dropped their pending-card background, and `pi-coder-catppuccin` joined the other two on the neutral grey thinking border. The `tool-pending-bar` extension that briefly marked pending cards landed upstream and was reverted before this sync, so it is not part of the snapshot.

### Changed

- **`statusline/`** — the git-branch icon is now `ᗌ` (U+15CC, CANADIAN SYLLABICS CARRIER RE), replacing `⑂` (U+2442, OCR FORK) and, before that, the Powerline / Nerd Font glyph U+E0A0 and `⎇` (U+2387). U+15CC is present in **none** of the three fonts in the author's Ghostty stack (`Lyth Mono Term`, `JetBrainsMonoNL Nerd Font Mono`, `Maple Mono SC NF`) and is drawn through system fallback — macOS covers it with `Euphemia UCAS` (advance ≈ 0.98 of a cell) and `Noto Sans CanAborig`, so it does not degrade to a missing-glyph box; the fractional advance does not affect alignment because the terminal places glyphs on fixed cells. On an environment without such a fallback, the previous `⑂` (present in `Lyth Mono Term`) is the one to go back to. Width is unchanged: one column, East Asian Width Neutral, so the truncation budget does not move.
- **`themes/pi-coder-summer-night.json`** — `toolPendingBg` is now the empty string, so a tool card that is still running keeps the terminal's default background. A pending card carries no badge or tint either, so nothing changes at the moment it finishes; the card's colors switch only on success or failure. `vars.pendingCard` (`#101017`) is left in the file unreferenced.
- **`themes/pi-coder-catppuccin.json`** — same `toolPendingBg` change (it moved `pendingPanel` → `""` on top of the earlier hex conversion of upstream's 256-color index), and `thinkingXhigh` / `thinkingMax` leave the palette's `blue` for a neutral grey added as `vars.thinkingGrey` (`#626262`) — the value `pi-coder-ayu` and `pi-coder-summer-night` already use, so all three themes now paint the two highest thinking levels the same grey. `vars.pendingPanel` (`#0b151f`) is left in the file unreferenced.
- **`themes/pi-coder-ayu.json`** — same `toolPendingBg` change; `vars.toolPendingBg` (`#171717`) is left in the file unreferenced. The file is otherwise byte-identical to 2.0.2.
- Documentation resynced: [docs/themes.md](docs/themes.md) (the blank `toolPendingBg` in all three themes, catppuccin's `thinkingGrey` deviation, the `vars` counts), [docs/extensions.md](docs/extensions.md) (the branch icon and its font coverage) and [docs/handbook.zh.md](docs/handbook.zh.md) (the same three theme passages, now on the upstream no-background reading).

### Unchanged

The test suite stays at **624 tests**, and no color value moved apart from the three `toolPendingBg` entries and catppuccin's new `thinkingGrey`: the `statusline` change is comments plus one exported constant, and `line.test.ts` pins the new code point.

## 2.0.2 — 2026-09-20

Snapshot sync: a new extension, a resynced palette page, a rebuilt `pi-coder-summer-night` palette whose added-line color is now green, a re-tuned pending-card background in `pi-coder-ayu`, a shorter `recap` idle delay, a session-replacement crash fix in `user-message-bar` and a `statusline/` branch icon that no longer needs a Nerd Font.

### Added

- **`user-message-bar/`** — a `▏` at the head of every line of a user message box, including the blank padding lines above and below the text. It replaces the single column of left padding `Box` already reserves, so the background, the line width and the wrap positions are unchanged — a bar drawn next to the padding would make pi-tui's renderer throw `Rendered line N exceeds terminal width` and take the TUI down. The color is the theme's `toolDiffAdded` (with `selectedBg` / `accent` / `text` as fallbacks); `PI_USER_MESSAGE_BAR=off` disables the bar and `PI_USER_MESSAGE_BAR_COLOR=<slot>` picks another slot, converting a background slot such as `selectedBg` to a foreground. The patch on `UserMessageComponent.prototype.render` goes in at module-evaluation time and receives the live theme on `session_start`, so it follows `/theme`; `bar.ts` holds the pi-free logic and `index.test.ts` loads the extension through pi's own loader to compare patched and unpatched frames — that test is what proves the patch landed on the class pi actually renders with.

### Changed

- **`themes/pi-coder-summer-night.json`** — resynced. The Tokyo Night base stays (`night` / `panel` / `select` / `find`), while foregrounds and lines now come from [iceberg.vim](https://github.com/cocopon/iceberg.vim): `fg` `#c6c8d1`, `muted` `#818596`, `dim` and `Think:` `#6b7089`, with red / green / yellow / magenta taken from its terminal palette. `text` points at `fg` instead of the terminal default, `bashOutput` is defined (`#818596`, a variable of its own, equal to `muted`), and no literal color value is left anywhere in the file — `export` included. The variables are renamed to Tokyo Night's names, so a local edit to the previous file's `bg` / `verdigris` / `fernMist` will not apply here. `toolDiffAdded` (added diff lines: their line numbers and `+`, and the default color of `user-message-bar`'s bar) also moved, from `teal` `#89b8c2` to a new variable `addedGreen` `#8bc391` — the conventional green: 7.83:1 on the `addedLine` background (was 7.36:1), and 4.96:1 for body text over the 30% inline tint `tool-diff.ts` lays down. It no longer matches `success`, which stays `teal`. The file is now 40 `vars`.
- **`themes/pi-coder-ayu.json`** — `toolPendingBg` moved from `#1b1c1d` to `#1f1f1f`, so a running tool card no longer shares the background of a user message. Only that token moved: `userMessageBg` and `customMessageBg` keep `#1b1c1d`.
- **`working-indicator/`** — the prompt summary is now requested for any prompt that does not fit (`PI_WORKING_SUMMARY_TRIGGER` default `1.2` → `1`; raise it to tolerate truncation, `2` means giving up half the prompt first), and a failed request — error, 45 s timeout, or a reply with no text — is retried once after `PI_WORKING_SUMMARY_RETRY_MS` (new switch, `3000` ms) instead of being dropped. Two attempts per prompt is the cap; a new prompt, the end of the turn or a session replacement cancels the pending retry.
- **`recap/`** — the automatic summary now appears after **10 seconds** of idling instead of 30 (`IDLE_MS` `30_000` → `10_000`, still hardcoded and still without any switch). The three guards around it are unchanged: a subagent that is still running blocks generation, a finished one is not summarized immediately, and the generation timeout stays at 45 s. Its real-timer end-to-end test is the suite's long pole and now runs in ~30 s (30.5 s in isolation here), which is essentially the whole of the suite's wall-time drop below.
- **`statusline/`** — the git-branch icon is now `⑂` (U+2442, OCR FORK) instead of the Powerline / Nerd Font private-use glyph U+E0A0, so the line no longer needs a patched font; the first version of it used `⎇` (U+2387). The replacement is one column wide with East Asian Width = Neutral, so the truncation budget does not move and a CJK-configured terminal cannot render it two columns wide. Font coverage was measured with fontTools on the author's stack: U+2442 is present in the first font of Ghostty's stack (`Lyth Mono Term`) and in neither Nerd Font fallback, so it is drawn through font fallback.
- **`assets/pi-coder-palettes.html`** — the palette reference resynced: it now reads the skin variables instead of hand-copied hex, the three main-color blocks and the thinking-level ladder are gone (123 lines fewer), the summer-night description is half its former length, and the `addedGreen` swap is reflected in its variable table and counts.
- Documentation resynced: [README](README.md), [docs/extensions.md](docs/extensions.md), [docs/themes.md](docs/themes.md), [docs/development.md](docs/development.md), [docs/installation.md](docs/installation.md) and [docs/handbook.zh.md](docs/handbook.zh.md).
- The suite grows from **596 to 624 tests** (the new extension, the summary retry path and the two `user-message-bar` regressions), and its wall time falls from ~73 s to ~34 s with the `recap` change.

### Fixed

- **`user-message-bar/`** — replacing the session (`/clear`, `/new`, `/resume`, `/fork`, `/reload`) could kill pi with `exit=1`. pi invalidates the old `ctx` while the previous session's user messages are still mounted and rendering, and the style source this extension had captured on `session_start` was read from inside a render tick, where the `This extension ctx is stale …` throw reaches pi's `uncaughtException` with nothing to catch it. The source is now reset on `session_shutdown` — which pi emits before the invalidation — and reading the theme is wrapped in a `try/catch`, so the worst case is a few frames drawn without the bar until the next `session_start`. Two regression tests cover both: rendering under an invalidated `ctx` neither throws nor draws, and rendering after shutdown never touches the old `ctx`. Both, plus a loader assertion that the `session_shutdown` hook is registered at all, fail against the previous revision.

### Not included

- `config/settings.json` and `config/models.json`. The snapshot's `defaultProvider`, `defaultModel` and `modelThinkingLevels` keys stay out for the same reason as the gateway's provider registrations: they are machine-specific.

## 2.0.0 — 2026-09-19

Snapshot sync: the three themes were renamed with a `pi-coder-` prefix, so their names cannot collide with themes from another installed package.

### Changed

- **`themes/`** — `summer-night.json`, `catppuccin.json` and `ayu.json` became `pi-coder-summer-night.json`, `pi-coder-catppuccin.json` and `pi-coder-ayu.json`; each file's `name` field followed, and `config/settings.json` now selects `pi-coder-summer-night`.
- Theme names resolve through the `name` field, not the file name, so **an installed `settings.json` that still says `"theme": "summer-night"` silently falls back to pi's built-in `dark`** until it is updated — `/theme` writes the new value. That is why this is a major release.
- References updated: the comments in `bash-command-collapse.ts`, `read-path-collapse.ts`, `tool-diff.ts`, `working-indicator/index.ts`, `working-indicator/spinner-frames.ts` and `spinner-frames.test.ts`, plus [README](README.md), [docs/themes.md](docs/themes.md), [docs/configuration.md](docs/configuration.md), [docs/development.md](docs/development.md) and [docs/handbook.zh.md](docs/handbook.zh.md). Palette names (`ayu-dark`, Catppuccin Mocha, Ayu) are untouched.

### Unchanged

- No color value moved: the three theme files are byte-identical to 1.1.1 apart from the `name` field, and the extension sources differ only in those comment lines.
- The test suite stays at 596 tests.

## 1.1.1 — 2026-09-19

Snapshot sync: the bash and read display toggles were cut back to a fixed default plus environment variables.

### Changed

- **`bash-command-collapse.ts`** — folding is always on and always keeps 3 visual lines. The `/bash-collapse` command is gone (it switched folding off and also set the line budget), and with it the `enabled` / `maxLines` variables and the cache-key fields they fed. `ctrl+o` still expands the command in full.
- **`bash-command-collapse.ts`** — tree indentation and streaming lost their commands as well (`/bash-tree`, `/bash-stream`); both are now read-only startup switches (`PI_BASH_TREE=off`, `PI_BASH_STREAM=on`), so their mutable state became `const`. `/bash-preview` and `/bash-timeout` are the only commands this extension still registers.
- **`read-path-collapse.ts`** — `/read-collapse` removed; `PI_READ_COLLAPSE=off` is now the only way to keep pi's built-in title row. The startup default is unchanged.
- Documentation resynced to match: the command list in [README](README.md), the command table and both tool sections in [docs/extensions.md](docs/extensions.md), the post-install checklist in [docs/installation.md](docs/installation.md), and the one stale `/bash-stream on` sentence in [docs/handbook.zh.md](docs/handbook.zh.md).

### Unchanged

- Defaults before and after this sync are identical: folding on at 3 lines, tree indentation on, streaming off, `read` path collapse on.
- The test suite stays at 596 tests; the removed command handlers were not covered.

## 1.1.0 — 2026-09-18

Snapshot sync: the environment gained an MCP client and a startup fix for pi's built-in footer, and the `ayu` theme was resynced.

### Added

- **`mcp/`** — MCP servers registered directly as pi tools (`mcp__<server>__<tool>`, Claude Code's naming). Config follows Claude Code's `.mcp.json` shape: global `~/.pi/agent/mcp.json` plus the nearest project `.mcp.json`. Three transports, implemented without `@modelcontextprotocol/sdk`: stdio, streamable HTTP and legacy HTTP+SSE. `${VAR}` / `${VAR:-default}` expansion, and `headersCommand` (aliases `headersHelper` / `http_headers_helper`) for dynamic auth headers. Commands: `/mcp`, `/mcp reload`, `/mcp <server>`. Diagnostics stay in an in-memory ring buffer rather than on stderr.
- **`statusline/footer-suppress.ts`** — pi's built-in footer is patched to render zero lines during the boot window, so it no longer paints its default state line before this statusline is installed. `PI_STATUSLINE_BOOT_SUPPRESS=off` disables it.
- Two `ayu` captures in the README, and a `pi.image` gallery preview in `package.json`.

### Changed

- `themes/ayu.json` resynced: `userMessageText` now points at a new `textColor` var (`#dbdbdd`), and `toolPendingBg` now matches `userMessageBg` (`#1b1c1d`).
- The suite grows from **454 to 596 tests**.

### Not included

- `config/mcp.json` — the snapshot's entries hold absolute paths of local MCP server executables, the same class of machine-specific value as `models.json`'s gateway registrations. `config/models.json` and the three model-selection keys in `config/settings.json` remain out as well.

## 1.0.0 — 2026-09-18

First release. A complete pi coding-agent environment packaged for npm.

### Added

- **22 extensions** under `extensions/`, copied verbatim from the author's `~/.pi/agent/extensions/`:
  - Tool rendering: `bash-command-collapse.ts`, `read-path-collapse.ts`, `tool-diff.ts`, `thinking-collapse.ts`, `fenceless-code-block/`
  - TUI chrome: `statusline/`, `cwd-statusline.ts`, `startup-logo/`, `below-editor-after-statusline.ts`, `prompt-editor.ts`, `working-indicator/`
  - Workflow: `simple-task/`, `recap/`, `rewind/`, `init-command.ts`, `theme-command.ts`, `folder-history.ts`, `clear-command.ts`, `exit-command.ts`
  - Model and tooling: `auto-default-model/`, `ask-user-question/`, `subagent-log-guard/`
- **3 themes** under `themes/`: `summer-night` (default), `catppuccin`, `ayu` — including the two custom diff-background tokens and `bashOutput`.
- **Global config files** under `config/`: `AGENTS.md`, `settings.json`, `web-search.json`, `pi-statusline.json`.
- **454 unit tests** runnable with `npm test`, plus the pure-logic module split that makes them possible.
- English documentation: [installation](docs/installation.md), [configuration](docs/configuration.md), [extensions](docs/extensions.md), [themes](docs/themes.md), [development](docs/development.md).
- The original Chinese handbook, kept verbatim as [docs/handbook.zh.md](docs/handbook.zh.md).

### Not included

- `config/models.json`, and the `defaultProvider` / `defaultModel` / `modelThinkingLevels` keys in `config/settings.json`. Provider registrations point at a local gateway and are machine-specific.
