@gavin-hu/my-pi-agent 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.md +61 -45
  2. package/README.zh-CN.md +54 -42
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +344 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +465 -416
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +554 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +66 -53
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
  14. package/docs/extensions/plan.md +976 -0
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +285 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +166 -119
  19. package/docs/extensions/team.md +226 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +60 -38
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
  25. package/extensions/board/commands.ts +87 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +72 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +312 -0
  32. package/extensions/board/result.ts +99 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +75 -0
  35. package/extensions/board/seat.ts +29 -0
  36. package/extensions/board/snapshot.ts +41 -0
  37. package/extensions/board/tools.ts +279 -0
  38. package/extensions/board/transport/router.ts +172 -0
  39. package/extensions/board/transport/server.ts +149 -0
  40. package/extensions/board/types.ts +54 -0
  41. package/extensions/board/view/actions.ts +306 -0
  42. package/extensions/board/view/client.ts +41 -0
  43. package/extensions/board/view/keys.ts +121 -0
  44. package/extensions/board/view/live.ts +397 -0
  45. package/extensions/board/view/page.ts +158 -0
  46. package/extensions/board/view/render.ts +496 -0
  47. package/extensions/board/view/styles.ts +211 -0
  48. package/extensions/board/view/team.ts +305 -0
  49. package/extensions/board/writes.ts +99 -0
  50. package/extensions/bytefly/notify/types.ts +1 -1
  51. package/extensions/bytefly/panel/commands.ts +40 -7
  52. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  53. package/extensions/bytefly/panel/settings.ts +11 -21
  54. package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
  55. package/extensions/bytefly/picker/discovery.ts +0 -2
  56. package/extensions/bytefly/picker/types.ts +0 -2
  57. package/extensions/bytefly/update/check.ts +39 -0
  58. package/extensions/bytefly/update/format.ts +15 -0
  59. package/extensions/bytefly/update/register.ts +73 -34
  60. package/extensions/bytefly/update/state.ts +6 -3
  61. package/extensions/context/tui.ts +1 -1
  62. package/extensions/file-browse/{config.ts → config/config.ts} +13 -4
  63. package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
  64. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  65. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  66. package/extensions/file-browse/index.ts +4 -4
  67. package/extensions/file-browse/{live.ts → live/live.ts} +54 -82
  68. package/extensions/file-browse/live/rev.ts +42 -0
  69. package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
  70. package/extensions/file-browse/transport/server.ts +110 -0
  71. package/extensions/file-browse/view/chrome.ts +83 -0
  72. package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
  73. package/extensions/file-browse/view/diff-html.ts +94 -0
  74. package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
  75. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  76. package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -1
  77. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
  78. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
  79. package/extensions/file-browse/{styles.ts → view/styles.ts} +31 -5
  80. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  81. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  82. package/extensions/file-read/schema.ts +2 -1
  83. package/extensions/goal/plan-route.ts +8 -2
  84. package/extensions/job/index.ts +15 -7
  85. package/extensions/memory/store.ts +12 -5
  86. package/extensions/plan/blocks.ts +23 -2
  87. package/extensions/plan/commands.ts +77 -21
  88. package/extensions/plan/feedback.ts +409 -63
  89. package/extensions/plan/index.ts +7 -5
  90. package/extensions/plan/plan-index.ts +44 -10
  91. package/extensions/plan/plan-text.ts +51 -24
  92. package/extensions/plan/plans.ts +162 -57
  93. package/extensions/plan/policy.ts +20 -12
  94. package/extensions/plan/review-prompt.ts +93 -0
  95. package/extensions/plan/review.ts +51 -95
  96. package/extensions/plan/runtime.ts +22 -10
  97. package/extensions/plan/sections.ts +112 -0
  98. package/extensions/plan/steps.ts +63 -95
  99. package/extensions/plan/tools/enter.ts +8 -3
  100. package/extensions/plan/tools/exit.ts +13 -19
  101. package/extensions/plan/tools/write.ts +8 -3
  102. package/extensions/plan/tools.ts +1 -1
  103. package/extensions/plan/tui.ts +29 -15
  104. package/extensions/plan/types.ts +29 -3
  105. package/extensions/plan/web-chrome.ts +2 -1
  106. package/extensions/plan/web-client.ts +122 -178
  107. package/extensions/plan/web-collab.ts +207 -0
  108. package/extensions/plan/web-comments.ts +269 -0
  109. package/extensions/plan/web-page.ts +96 -17
  110. package/extensions/plan/web-router.ts +109 -47
  111. package/extensions/plan/web-share-session.ts +191 -0
  112. package/extensions/plan/web-share.ts +169 -0
  113. package/extensions/plan/web-streams.ts +124 -0
  114. package/extensions/plan/web-tokens.ts +63 -0
  115. package/extensions/plan/web.ts +245 -151
  116. package/extensions/question/dialogs.ts +91 -1
  117. package/extensions/question/tools.ts +2 -1
  118. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  119. package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
  120. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  121. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  122. package/extensions/rewind/index.ts +3 -3
  123. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  124. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  125. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  126. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  127. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  128. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  129. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  130. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  131. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  132. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  133. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  134. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  135. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  136. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  137. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  138. package/extensions/session-bus/commands.ts +113 -0
  139. package/extensions/session-bus/format.ts +101 -0
  140. package/extensions/session-bus/index.ts +75 -0
  141. package/extensions/session-bus/join-request.ts +40 -0
  142. package/extensions/session-bus/mail-watch.ts +134 -0
  143. package/extensions/session-bus/notice.ts +86 -0
  144. package/extensions/session-bus/overlap.ts +36 -0
  145. package/extensions/session-bus/protocol.ts +104 -0
  146. package/extensions/session-bus/resolve.ts +67 -0
  147. package/extensions/session-bus/result.ts +208 -0
  148. package/extensions/session-bus/rows.ts +171 -0
  149. package/extensions/session-bus/runtime.ts +466 -0
  150. package/extensions/session-bus/store.ts +365 -0
  151. package/extensions/session-bus/tools.ts +192 -0
  152. package/extensions/session-bus/tui.ts +394 -0
  153. package/extensions/session-bus/types.ts +98 -0
  154. package/extensions/subagent/agents.ts +9 -8
  155. package/extensions/subagent/invocation.ts +4 -2
  156. package/extensions/team/commands.ts +112 -0
  157. package/extensions/team/dispatch.ts +133 -0
  158. package/extensions/team/format.ts +105 -0
  159. package/extensions/team/index.ts +33 -0
  160. package/extensions/team/schema.ts +29 -0
  161. package/extensions/team/tools.ts +246 -0
  162. package/extensions/team/types.ts +48 -0
  163. package/extensions/watch/index.ts +15 -9
  164. package/extensions/web-access/fetch/page.ts +4 -1
  165. package/extensions/web-access/fetch/structured.ts +447 -0
  166. package/extensions/web-access/fetch/tool.ts +2 -1
  167. package/extensions/web-access/search/tool.ts +2 -1
  168. package/extensions/wechat/bridge.ts +76 -92
  169. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  170. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  171. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  172. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  173. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  174. package/extensions/wechat/dialog/channels.ts +136 -0
  175. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +66 -51
  176. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
  177. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  178. package/extensions/wechat/index.ts +10 -10
  179. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  180. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  181. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +119 -56
  182. package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
  183. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
  184. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  185. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  186. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  187. package/extensions/wechat/{notices.ts → text/notices.ts} +15 -7
  188. package/extensions/wechat/tools.ts +29 -12
  189. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  190. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +104 -40
  191. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  192. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  193. package/extensions/wechat/transport/outbound.ts +142 -0
  194. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  195. package/extensions/wechat/types.ts +10 -1
  196. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  197. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  198. package/extensions/worktree/git.ts +5 -0
  199. package/extensions/worktree/guard.ts +3 -5
  200. package/extensions/worktree/registry.ts +11 -5
  201. package/extensions/worktree/status.ts +1 -3
  202. package/lib/README.md +12 -6
  203. package/lib/board/card.ts +290 -0
  204. package/lib/board/links.ts +117 -0
  205. package/lib/board/page-pointer.ts +256 -0
  206. package/lib/board/round-text.ts +72 -0
  207. package/lib/board/round.ts +89 -0
  208. package/lib/board/rules.ts +304 -0
  209. package/lib/board/session.ts +105 -0
  210. package/lib/board/store.ts +310 -0
  211. package/lib/board/view.ts +150 -0
  212. package/lib/errors.ts +12 -4
  213. package/lib/format.ts +13 -0
  214. package/lib/http/guard.ts +112 -0
  215. package/lib/http/index.ts +16 -5
  216. package/lib/http/router.ts +40 -13
  217. package/lib/http/serve.ts +234 -0
  218. package/lib/http/server.ts +29 -13
  219. package/lib/http/sink.ts +110 -0
  220. package/lib/http/token.ts +38 -0
  221. package/lib/interaction.ts +10 -0
  222. package/lib/notice.ts +29 -2
  223. package/lib/plan-entry.ts +7 -2
  224. package/lib/policy.ts +12 -2
  225. package/lib/qr.ts +64 -0
  226. package/lib/repo-key.ts +74 -0
  227. package/lib/report.ts +52 -33
  228. package/lib/requirements.ts +7 -9
  229. package/lib/session-label.ts +64 -0
  230. package/lib/session-roster.ts +130 -0
  231. package/lib/session-tool.ts +184 -0
  232. package/lib/tool-names.ts +25 -2
  233. package/lib/tui/README.md +16 -2
  234. package/lib/tui/dock-screen.ts +85 -17
  235. package/lib/tui/glyphs.ts +2 -0
  236. package/lib/tui/line-pane.ts +15 -4
  237. package/lib/tui/tool.ts +23 -4
  238. package/lib/web/README.md +5 -2
  239. package/lib/web/components.ts +6 -1
  240. package/lib/web/runtime.ts +7 -1
  241. package/lib/web/tokens.ts +25 -2
  242. package/package.json +27 -14
  243. package/skills/init/SKILL.md +80 -45
  244. package/skills/init/references/agents-md-quality.md +26 -2
  245. package/skills/init/references/interview-questions.md +36 -14
  246. package/skills/next/SKILL.md +77 -51
  247. package/skills/project-docs/SKILL.md +133 -0
  248. package/skills/project-docs/references/audit.md +131 -0
  249. package/skills/project-docs/references/changelog.md +105 -0
  250. package/skills/project-docs/references/discovery.md +127 -0
  251. package/skills/project-docs/references/readme.md +80 -0
  252. package/skills/project-docs/references/release.md +84 -0
  253. package/skills/project-docs/references/roadmap.md +93 -0
  254. package/skills/project-docs/scripts/state.ts +316 -0
  255. package/skills/refactor/SKILL.md +100 -125
  256. package/skills/refactor/references/handoff.md +35 -0
  257. package/skills/refactor/references/signals.md +54 -0
  258. package/skills/refactor/scripts/inventory.ts +587 -0
  259. package/extensions/file-browse/README.md +0 -467
  260. package/extensions/file-browse/server.ts +0 -134
  261. package/extensions/plan/README.md +0 -709
  262. package/extensions/plan/config.ts +0 -27
  263. package/extensions/wechat/outbound.ts +0 -100
  264. package/extensions/wechat/qrcode.ts +0 -46
  265. package/skills/changelog/SKILL.md +0 -148
  266. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  267. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  268. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  269. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  270. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  271. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  272. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  273. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  274. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -0,0 +1,976 @@
1
+ # plan — file-backed planning for Pi
2
+
3
+ A read-only exploration mode modelled on Claude Code's `EnterPlanMode` /
4
+ `ExitPlanMode` pair. While plan mode is on, the model investigates and proposes,
5
+ but the only thing it may write is the plan file itself. The plan is saved under
6
+ the agent directory; the user reviews that file, and it is the artifact the model
7
+ executes.
8
+
9
+ ## Capabilities
10
+
11
+ Status records what a reader can rely on: **Shipped** is in a tagged release, **Unreleased** is complete in the current tree but not yet tagged, and **Partial** has one released part and one unreleased part. Rows are ordered by user value, highest first.
12
+
13
+ ### Planning lifecycle
14
+
15
+ | Capability | How | What you get | Status |
16
+ |---|---|---|---|
17
+ | Read-only planning | `enter_plan_mode`, `/plan`, `Ctrl+Alt+P`, `--plan` | The session keeps only its readers, so the model investigates and proposes without touching the tree. | Shipped |
18
+ | Confirmed model entry | `enter_plan_mode` | The model asks before it takes write access away, and the user answers first. | Shipped |
19
+ | Read-only git shell | `bash` with the read-only git allowlist | Plan mode can inspect `git status`, `diff`, `log`, and `show`; pipes, redirects, and non-git commands are refused. | Shipped |
20
+ | Read-only delegation | `subagent` with forced `readOnly: true` | A delegated agent can read and search but cannot write, edit, or run a shell while the plan is being researched. | Shipped |
21
+ | Absolute-path guard | `path-guard.ts` | A path outside the project is refused unless it is a known plan-safe reader, so planning cannot write through an absolute path. | Shipped |
22
+ | One writable artifact | `write_plan` | The plan lands as markdown under the agent directory, one store per repository, shared by its worktrees. | Shipped |
23
+ | Approval gate | `exit_plan_mode` | The plan is read back and presented; nothing is written until the user approves it. | Shipped |
24
+ | Session goal at approval | `goal` at approval | A `planning` goal is promoted to the plan's objective, and a session with none gets it, without a second dialog; an active goal is left alone. | Shipped |
25
+ | Steps as todos | `exit_plan_mode` | Approval seeds the plan's steps into the todo list, with the verification section as the last item. | Shipped |
26
+ | Plan kept current | `set_plan_steps` | Ticking a step rewrites its checkbox in the file and its todo item in one call. | Shipped |
27
+ | Stale-step protection | `expected` in `set_plan_steps` | A step whose number moved, is out of range, or no longer matches its text refuses the whole batch instead of ticking the wrong line. | Shipped |
28
+ | Cancel without a plan | `cancel_plan_mode`, `/plan cancel` | Write access returns, and no todo items or goal are recorded. | Shipped |
29
+ | Plans index | `/plan list` | Every saved plan with its title, modified time, and step progress, newest first, under a header naming the store's size and its oldest plan. | Shipped |
30
+ | Plan-store migration | `<repo>/.pi/plans` to the agent-dir store | A store left by an older version is moved in once, with a notice, so worktrees and main see the same index. | Shipped |
31
+ | Instruction-file bootstrap | `/skill:init` | The packaged `init` skill plans and gates a project's `AGENTS.md`, writing only after approval. | Shipped |
32
+ | Branch-following mode | the `plan-mode` session entry | `/resume` and `/tree` restore the mode and its tool set for the branch they land on. | Shipped |
33
+
34
+ ### Review
35
+
36
+ | Capability | How | What you get | Status |
37
+ |---|---|---|---|
38
+ | Review screen | `exit_plan_mode` | A scrollable, themed plan with approve, refine, keep, browser, and exit in the footer. | Shipped |
39
+ | Browser page | `/plan review`, `w` on the review screen | The plan opens in a local browser as a reading-first page with live section and step progress. | Shipped |
40
+ | Approval in the browser | the `/plan review` page's **Approve & run** | The plan is approved without returning to the terminal, and the steps are seeded. | Shipped |
41
+ | Inline comments | the page's `+ Comment`, **Save feedback** | Comments are saved into the plan file as `> [!REVIEW]` lines and returned to the model. | Shipped |
42
+ | Live page | the page's `events` stream, the plan-file poll | An edit made elsewhere repaints every open page without a reload. | Shipped |
43
+ | Changed sections | the `exit_plan_mode` review screen, the `/plan review` outline | After a refinement, only the sections that changed are marked. | Shipped |
44
+ | Section navigation | the review screen, `n`/`p` | The screen names the section you are in and jumps between headings. | Shipped |
45
+ | Remote review | `exit_plan_mode` on a WeChat turn | The plan file is sent, then one compact prompt to approve, keep planning, or exit. | Shipped |
46
+
47
+ ### Collaboration
48
+
49
+ | Capability | How | What you get | Status |
50
+ |---|---|---|---|
51
+ | Shared review | `/plan share`, **Share** | A reviewer link, with a QR a phone can scan, opens the same plan from another machine on this Wi-Fi. | Shipped |
52
+ | One link per teammate | **New link**, **Revoke**, `/plan share stop` | Each teammate holds a link of their own, and one is revoked without disturbing the rest. | Shipped |
53
+ | Two roles | the owner's `token`, a reviewer's link | Only the owner saves, approves, cancels, and manages links; a reviewer reads and comments. | Shipped |
54
+ | Signed comments | the page's `name` control | A reviewer's name binds to her link on its first post, so her notes are hers and the file records who wrote them. | Shipped |
55
+
56
+ ## Quickstart
57
+
58
+ ```bash
59
+ pi --extension ./extensions/plan # load just this extension
60
+ pi -e . # load the whole @gavin-hu/my-pi-agent package
61
+ pi install ./ # install the package for a user
62
+ ```
63
+
64
+ ```text
65
+ /plan # toggle plan mode
66
+ /plan add rate limiting to the public API # enter plan mode and send the task
67
+ ```
68
+
69
+ The model calls `enter_plan_mode` before a non-trivial task; `/plan` and
70
+ `Ctrl+Alt+P` are the user's own switch.
71
+
72
+ ## Tools
73
+
74
+ How the `write_plan` and `exit_plan_mode` calls and their results look in the transcript — the call line as a headline and the body indented under it:
75
+
76
+ <img src="../../assets/tools/write_plan.png" alt="The write_plan tool's call and result: the call line and an indented body with the saved plan path and byte count" width="100%">
77
+
78
+ <img src="../../assets/tools/exit_plan_mode.png" alt="The exit_plan_mode tool's call and result: the call line and an indented body with the approval line and the approved plan preview" width="100%">
79
+
80
+ All five tools are `executionMode: "sequential"` and closed-world. Two are active
81
+ in normal mode and three are `defaultActive: false`, so the plan-mode tool set
82
+ stays small.
83
+
84
+ | Tool | Active | Purpose |
85
+ |---|---|---|
86
+ | `enter_plan_mode` | normal mode | Ask to enter plan mode; empty schema, confirmed with `ctx.ui.confirm`. |
87
+ | `write_plan` | plan mode | Save the plan: `title` (file-name slug), `content` (full markdown in the documented outline), optional `plan_path` to overwrite a refinement. Returns the absolute path. |
88
+ | `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. |
89
+ | `cancel_plan_mode` | plan mode | Leave plan mode without a plan file or a review. Empty schema. |
90
+ | `set_plan_steps` | normal mode | Set the checkboxes of the given top-level steps of an approved plan in one write, and their todo items with them. |
91
+
92
+ ### `enter_plan_mode`
93
+
94
+ No parameters. It confirms with `ctx.ui.confirm` before switching the tool set,
95
+ because the model is changing the mode on the user's behalf; a declined confirm
96
+ answers "stayed in normal mode" and the session keeps write access. Already
97
+ planning, it says so. With no UI it fails with an actionable message instead of
98
+ deciding for the user.
99
+
100
+ On a remote turn the confirmation is a short consent sentence in the peer's
101
+ language, and the notice names how to leave.
102
+
103
+ ### `write_plan`
104
+
105
+ `title` is the file-name slug, `content` the full markdown, and `plan_path`
106
+ overwrites an existing plan when refining. The result carries the absolute path,
107
+ the relative path, and the byte count — the absolute path is what `exit_plan_mode`
108
+ takes.
109
+
110
+ The file lands in
111
+ `<agent-dir>/plans/<repo-key>/<YYYY-MM-DD-HHmm>-<slug>.md`, keyed by the
112
+ repository's common git directory, so a plan approved in one worktree is the
113
+ same plan from the main checkout and from every other session. Outside a
114
+ repository the store is `<agent-dir>/plans`. Plans are outside the project, so
115
+ they are never untracked files and never appear in a `rewind` snapshot; a store
116
+ an older version left in `<repo>/.pi/plans` is moved in once, with a notice.
117
+
118
+ The description documents a canonical outline — a `# <Title>`, then `## Goal`,
119
+ `## Context`, `## Approach`, `## Visual`, `## Steps`, `## Risks`, and
120
+ `## Verification` — and the model is guided to follow it. Every example in the
121
+ template is the shape wanted rather than a description of it, and each section
122
+ has one job: the goal is one sentence, the context is the facts that force the
123
+ approach, the approach names the alternative it rejected, and verification is a
124
+ fence of the commands to run. `Visual` carries the end state in an untagged
125
+ fence, with one line naming what to look at: the same surface before and after
126
+ when the change alters one that already exists, one frame when the surface is
127
+ new, because a reviewer judges the result before the steps that build it; a
128
+ change with no visible surface drops it. The description calls the shape a guide
129
+ rather than a form: `Context`, `Approach`, `Visual`, and `Risks` are dropped when
130
+ they have nothing to say, while `Steps` and `Verification` are kept, because the
131
+ steps are read from that section (without it, the shallowest list anywhere
132
+ becomes the steps) and Verification becomes the last todo item. Within `Steps`
133
+ only the shallowest list is tracked, so nested bullets stay notes instead of
134
+ becoming todos. This is guidance only: any markdown is accepted, stored, and
135
+ rendered as written, and a refinement is asked to keep its section headings so
136
+ the review can still mark only what changed.
137
+
138
+ A plan with more than 50 top-level steps in its `Steps` section is refused with
139
+ the cap and the fix named, and nothing is written.
140
+
141
+ The file is the source of truth. On approval the model follows the plan file's
142
+ steps, and refinements rewrite the same file (`write_plan` with `plan_path`), so
143
+ the reviewed artifact is the executed artifact.
144
+
145
+ ### `exit_plan_mode`
146
+
147
+ `plan_path` names the file `write_plan` returned; the tool reads it back and
148
+ presents it for approval. A hand-edited or older file whose step list is over the
149
+ cap is refused before the review screen. The user approves, keeps planning, asks
150
+ for a refinement, or exits plan mode without approving; the last keeps the plan
151
+ file for a later review and records no todo items or goal. The review also offers
152
+ **Exit plan mode (discard)**, which leaves plan mode without approving.
153
+
154
+ On approval, plan mode ends, write access returns, and the plan's steps go into
155
+ the todo list:
156
+
157
+ - Steps are the top-level numbered/bulleted items of the plan's `Steps` section,
158
+ and approval records them with the `todo` tool (via `ctx.executeTool`). Steps
159
+ marked `- [x]` (or `[DONE:n]`) seed as completed, nested sub-bullets are notes,
160
+ and an indented line continues its step.
161
+ - The list is a merge, not a replacement: existing items keep their place and
162
+ status, a step whose text is already there is skipped, and the tool's item cap
163
+ defers the rest instead of dropping anything.
164
+ - The plan's `## Verification` section follows as the **last** todo item,
165
+ summarized on one line (`Verify: …`) — a fenced block contributes its
166
+ commands, not its fence lines. It is not a plan step, so it is ticked in the
167
+ todo list rather than with `set_plan_steps`, and it never counts against the
168
+ 50-step cap.
169
+ - A plan without a `Steps` heading keeps the older rule — the shallowest list in
170
+ the document — so prose bullets in a plan that follows the outline never become
171
+ todos, and its verification bullets are already covered.
172
+ - Each seeded item is matched back to its step by text, so ticking a step with
173
+ `set_plan_steps` completes the item too.
174
+
175
+ **Goal-aware.** While planning, the injected context restates a pursued session
176
+ goal as one bullet, so the plan is designed against the objective instead of
177
+ beside it. On approval, a goal the goal workflow left as a draft (`planning`) is
178
+ finalized from the plan's own `## Goal` section — falling back to the title —
179
+ through the `goal` tool, with no extra dialog, because approving the plan *is*
180
+ the decision. A session with no goal at all is given the same objective, also
181
+ without asking. An `active` or `achieved` goal is never rewritten: a final
182
+ objective is the user's. No path spends a model turn.
183
+
184
+ ### `cancel_plan_mode`
185
+
186
+ Leaves plan mode without a plan file or a review. Restores write access, records
187
+ no todo items or goal, and keeps any saved plan. Empty schema.
188
+
189
+ ### `set_plan_steps`
190
+
191
+ Keep an approved plan current: `plan_path`, plus `updates`, each a 1-based `step`
192
+ in the order `exit_plan_mode` listed, a `status` of `pending` or `completed`, and
193
+ `expected` — the step's text, so an update whose step number moved is refused
194
+ instead of ticking the wrong line. It is a single all-or-nothing write: a step out
195
+ of range, or an `expected` text that no longer matches the file, refuses every
196
+ entry. Only the checkboxes change, and a reopened step drops its `[DONE:n]`
197
+ marker.
198
+
199
+ The todo item approval seeded from a step is ticked with it: the file is written
200
+ first and is authoritative, a renamed item is left alone and reported as
201
+ unmatched, and a failed todo write is reported in the result. The tool refuses
202
+ while plan mode is on — refine the plan with `write_plan` instead.
203
+
204
+ ### Transcript
205
+
206
+ The three tool rows reuse the transcript component, and `exit_plan_mode`
207
+ separates the plan body from its header with a blank line; the collapse hint
208
+ binds to `app.tools.expand` (`ctrl+o` by default).
209
+
210
+ ```
211
+ enter_plan_mode requested plan mode
212
+ ✓ Plan mode enabled
213
+ ```
214
+
215
+ ```
216
+ write_plan Add rate limiting
217
+ ✓ Saved plan ~/.pi/agent/plans/a1f9c02/2026-10-09-1809-add-rate-limiting.md
218
+ ```
219
+
220
+ ```
221
+ exit_plan_mode submitted a plan
222
+ ✓ Plan approved
223
+ ~/.pi/agent/plans/a1f9c02/2026-10-09-1809-add-rate-limiting.md
224
+
225
+ # Add rate limiting
226
+ 1. Add a token bucket …
227
+
228
+ … 12 more lines (ctrl+o to expand)
229
+ ```
230
+
231
+ ## Commands and flags
232
+
233
+ | Path | Who starts it | Confirmation |
234
+ |---|---|---|
235
+ | `enter_plan_mode` tool | the model, before a non-trivial task | `ctx.ui.confirm` |
236
+ | `/plan` | the user | immediate |
237
+ | `/plan <prompt>` | the user, with the task | immediate |
238
+ | `/plan list` | the user, browsing the plans directory | immediate |
239
+ | `/plan review [path]` | the user, reviewing a plan in the browser | immediate |
240
+ | `/plan share [stop]` | the user, handing the review to a teammate | immediate |
241
+ | `/plan cancel` | the user, leaving plan mode without approving | immediate |
242
+ | `/skill:init` | the user, creating or updating `AGENTS.md` | enters plan mode; write waits for plan approval |
243
+ | `Ctrl+Alt+P` | the user | immediate |
244
+ | `--plan` | the user, at launch | immediate |
245
+
246
+ `/plan` with no argument toggles plan mode; `/plan <prompt>` enters it (if it is
247
+ not already on) and sends the rest of the line as the task, so the first turn
248
+ already runs read-only:
249
+
250
+ ```bash
251
+ /plan add rate limiting to the public API
252
+ ```
253
+
254
+ `/plan <prompt>` never disables plan mode; only the argument-less `/plan` (or
255
+ `Ctrl+Alt+P`) toggles. Because slash commands go through Pi directly, `/plan` is
256
+ the toggle that works in every terminal (for example Zed's integrated terminal,
257
+ which does not forward `Ctrl+Alt+P`). The footer shows `≡ plan` while plan mode
258
+ is on, and `≡ plan · <plan-file>` once the model has written a plan.
259
+
260
+ `/plan list` walks the plans directory and shows every plan with its title,
261
+ modified time, and step progress, newest first — in the TUI as a docked list
262
+ where `Enter` opens the plan's review, elsewhere as a notice with each plan's
263
+ path. The header carries the store's own footprint for this repository: the bytes
264
+ its plans hold and how old the oldest one is, both derived from the walk that just
265
+ happened. The directory stays the index: no registry file, nothing deletes a plan,
266
+ and one bounded read per plan.
267
+
268
+ `/plan review` is the only subcommand that takes an argument. Bare `/plan review`
269
+ reviews the plan this session wrote, or, in a later session, the newest plan file
270
+ in the plans directory (newest modification time); `/plan review <path>.md` (or a
271
+ path with a separator) reviews that plan. A line that merely starts with the word
272
+ — `review the auth flow` — is still treated as a planning task, so the subcommand
273
+ never swallows one.
274
+
275
+ `/plan share` prints the reviewer link and its QR, and `/plan share stop` revokes
276
+ every link; the page's **Share** button does the same. With no browser review open
277
+ it says so and points at `/plan review`. `/plan cancel` leaves plan mode without
278
+ approving, exactly like `cancel_plan_mode`: write access returns and the plan file
279
+ stays for a later review. Both are exact words — `share the tokens` and `cancel
280
+ that idea` stay tasks — as is `list`.
281
+
282
+ ### `/skill:init`
283
+
284
+ `/skill:init` loads the packaged `init` skill. The skill enters plan mode itself
285
+ when it is not active, then decides what to do; nothing is written before
286
+ approval. Its workflow is:
287
+
288
+ ```text
289
+ /skill:init
290
+ └─> plan mode (read-only)
291
+ └─> recon: effective instruction file, sourced evidence, existing instructions
292
+ └─> interview: built-in bank, one call of at most 4 questions, minus resolved facts
293
+ ├─ answered ───────────────> reconcile with evidence
294
+ └─ unavailable/cancelled ──> evidence + assumptions
295
+ └─> draft: new file, exact edits, or an honest no-op
296
+ └─> quality gate
297
+ └─> write_plan ──> exit_plan_mode
298
+ ├─ not approved ──> refine the same plan
299
+ └─ approved ──────> write/edit the target
300
+ └─ read back + quality checklist
301
+ ```
302
+
303
+ The interview comes from a built-in bank in
304
+ [`skills/init/references/interview-questions.md`](../../skills/init/references/interview-questions.md):
305
+ core questions cover purpose, verification, non-obvious rules, isolation,
306
+ completion, sensitive paths, and dependency or network limits, with state-specific
307
+ entries for an existing file, an empty repository, related instruction files, and
308
+ a shadowing override. The first call uses the bank's deterministic set for the
309
+ state, minus every field the evidence sheet resolved; a second call is allowed
310
+ only for a material ambiguity. If the tool is inactive, there is no UI, or the
311
+ user cancels, the skill records the gaps as assumptions and continues from
312
+ repository evidence.
313
+
314
+ Recon resolves the effective instruction file first: a root `AGENTS.override.md`
315
+ replaces `AGENTS.md` and becomes the target, while an override above the project
316
+ root is reported rather than edited. Recon also builds a sourced evidence sheet —
317
+ commands with the file that defines them, generated and vendored paths from
318
+ `.gitignore`, workspace packages and boundaries, and purpose from the README or
319
+ manifest — and the interview asks only what it could not resolve. The plan uses
320
+ the canonical outline, including `## Steps` and `## Verification`, so approval
321
+ seeds the apply actions rather than the draft's bullets.
322
+
323
+ The quality reference is
324
+ [`skills/init/references/agents-md-quality.md`](../../skills/init/references/agents-md-quality.md).
325
+ An existing instruction file is reviewed and updated only after approval, a root
326
+ override is edited in place or removed explicitly when consolidating, an accurate
327
+ file can produce a no-op plan, and an empty repository gets only known facts. It
328
+ never invents commands.
329
+ The skill needs the plan extension for its review gate. It also ships with the
330
+ package, so load the whole package (`pi -e .`) or install it; loading only
331
+ `./extensions/plan` leaves `/skill:init` unavailable.
332
+
333
+ ## Routes
334
+
335
+ The browser review serves these routes on its loopback origin; `…` is the
336
+ session token, and the owner's token is the one that opened the page.
337
+
338
+ | Route | Result |
339
+ |---|---|
340
+ | `GET /?token=…` | the review page (a JSON data island plus `/theme.js` and `/client.js`) |
341
+ | `GET /theme.js` | the shared pre-paint theme bootstrap |
342
+ | `GET /client.js` | the first-party client script |
343
+ | `GET /events?token=…` | Server-Sent Events stream: a `comment`, `plan`, or `share` frame when something changed, `closed` when the review ends, `revoked` when sharing stops, and a dropped stream to detect a closed tab |
344
+ | `POST /api/comment?token=…` | reviewer only: write their own comments into the plan, replacing only their own; never ends the review |
345
+ | `POST /api/feedback?token=…` | owner only: write the owner's comments and return to the model |
346
+ | `POST /api/approve?token=…` | owner only: save, then approve |
347
+ | `POST /api/cancel?token=…` | owner only: end the session without saving |
348
+ | `POST /api/share?token=…` | owner only: mint the first reviewer link and its QR |
349
+ | `POST /api/share/link?token=…` | owner only: mint one more reviewer link, so each teammate holds their own |
350
+ | `POST /api/share/revoke?token=…` | owner only: revoke one link by `{ id }`, leaving the others answering |
351
+ | `POST /api/share/stop?token=…` | owner only: revoke every reviewer link |
352
+
353
+ ## Behaviour by mode
354
+
355
+ The review screen is TUI-only: `exit_plan_mode` opens `PlanViewComponent`, and `w`
356
+ on it opens the browser review. RPC and other non-interactive runs get no screen.
357
+
358
+ Dialog-capable non-TUI modes (RPC/WeChat) use one editor prompt instead: on a
359
+ remote turn the plan file is sent first, then a compact prompt carrying the
360
+ title, step count, first steps, mode status, and three actions — `1` approve,
361
+ `2` keep planning, `3` exit. If the upload fails, the plan's text is sent
362
+ instead. Any other reply is a refinement, so "select Refine, then type" is gone.
363
+ A WeChat turn takes the same dialog path and is answered over
364
+ WeChat, even though the session is in TUI mode: the choice comes from
365
+ `askHuman(ctx, { custom, dialogs, body })` in `lib/interaction.ts`, not from
366
+ `ctx.mode`. Without any UI the tools fail with an actionable message instead of
367
+ deciding for the user.
368
+
369
+ `/plan list` is a docked list screen in the TUI and a notice with each plan's
370
+ path everywhere else; both name the store's size and its oldest plan. The dock is
371
+ the list's alone while it is open — the goal and todo rails hide, as they do for
372
+ the other list screens — and the screen counts its own frame, so a full list keeps
373
+ its closing rule on a short terminal.
374
+
375
+ Browser review is a local-turn feature: the server binds `127.0.0.1`, so a remote
376
+ (WeChat) turn cannot reach it and `/plan review` there refuses with a notice
377
+ instead of opening a server nobody can use. The browser's **Approve & run** is
378
+ offered only for the review `exit_plan_mode` opens, since only that flow owns
379
+ leaving plan mode; `/plan review` stays comment-only.
380
+
381
+ Aborting the turn closes a browser review. `exit_plan_mode` forwards the call's
382
+ abort signal to the review server, so an aborted turn returns "not approved"
383
+ and stops listening instead of waiting on a page nobody is watching.
384
+
385
+ ## Security
386
+
387
+ Plan mode is a **guard rail, not a sandbox**: extensions run with Pi's OS
388
+ permissions, so it prevents accidental writes while planning, not a hostile
389
+ command. The read-only guarantee is layered:
390
+
391
+ - **Tool set at entry.** Tools that are not read-only are removed from the active
392
+ set or blocked. `write` and `edit` are hidden, raw shell is limited to read-only
393
+ git commands through `bash` (chained with `&&`/`;`), `powershell` is blocked
394
+ outright, and other mutating tools (worktree mutations, MCP tools) are blocked
395
+ until you exit plan mode. `subagent` stays active but is forced read-only. The
396
+ only permitted write is the plan control tool `write_plan`, which can only
397
+ create or overwrite markdown plan files directly inside the plans directory —
398
+ never dotfiles and never through a symlink.
399
+ - **Capability policy.** A tool is allowed only when it is a known structured
400
+ reader or declares the MCP `readOnlyHint`; `write`, `edit`, and unknown
401
+ mutators are denied. Classification defaults to *deny* and is driven by the
402
+ shared [`lib/policy.ts`](../../lib/policy.ts), so unknown tools are safe by
403
+ default and the MCP `readOnlyHint` is the only thing that opens one up.
404
+ - **Path backstop.** A `readOnlyHint` is only a claim, so a tool that takes a
405
+ file path is refused unless it is a known plan-safe reader. `path-guard.ts`
406
+ scans nested arguments and path-shaped values, not just fixed top-level keys.
407
+ - **Control-tool identity.** The `tool_call` guard exempts plan's own tools —
408
+ `enter_plan_mode`, `cancel_plan_mode`, `write_plan`, and `exit_plan_mode` — only
409
+ when the registered tool is this extension's, matched by source path, so a
410
+ same-named tool from another extension cannot borrow the exemption.
411
+ `enter_plan_mode` is exempted so a call already in flight when plan mode came on
412
+ is answered with "Already in plan mode" instead of being refused as not
413
+ read-only; the policy still denies it, so it stays hidden while planning.
414
+ - **Contained writes.** `write_plan` only writes markdown non-dotfiles whose
415
+ real parent resolves inside the plans directory, and every plan write —
416
+ create, refine, `set_plan_steps`, browser feedback — goes through a hidden
417
+ temp file followed by a rename, so an interrupted write cannot leave a
418
+ truncated plan.
419
+ - **Sanitized model text.** The plan file is model-authored, so `exit_plan_mode`
420
+ strips control characters from the plan body before rendering it in the
421
+ transcript and in non-TUI notices; a plan file cannot drive the terminal.
422
+ - **Loopback browser review.** The review server binds `127.0.0.1` and requires a
423
+ random per-session token in the URL, rejects a missing or foreign `Host`
424
+ header, serves only `GET`, `HEAD`, and `POST`, and caps the request body; the
425
+ page renders plan and comment text with DOM text nodes, never `innerHTML`, so a
426
+ plan cannot script the reviewer. The page also refuses to be framed
427
+ (`frame-ancestors 'none'` and `X-Frame-Options: DENY`), so a local page cannot
428
+ clickjack **Approve & run**.
429
+
430
+ ## Read-only git shell
431
+
432
+ Plan mode does not run a general shell. `bash` stays active only so the model
433
+ can run **read-only git commands** — for example `git status`,
434
+ `git diff --stat`, `git log --oneline`, `git show HEAD`, or `git branch -a`.
435
+ Several may be chained with `&&` or `;` in one call, but every segment must
436
+ itself be a read-only git command, so a chain only combines allowed reads.
437
+ Anything else (a non-git command, a mutating git subcommand, a piped or
438
+ redirected command, substitution, or a conditional `||`) is blocked by a
439
+ conservative allowlist in `git-bash.ts`. `powershell` is not allowed at all, so
440
+ plan mode uses Git Bash on native Windows: the same read-only git commands work
441
+ there, and a backslash before an ordinary character is accepted as a Windows
442
+ path separator while a backslash that escapes a metacharacter is still refused.
443
+ Everything else uses the structured readers — `read`, `grep`, `find`, `ls`, and
444
+ `read_doc` for a PDF, Word, OpenDocument, RTF, or Excel file — and the other
445
+ read-only tools already available (`web_search`, `web_fetch`,
446
+ `ask_user_question`, `todo`, `goal`).
447
+
448
+ ```bash
449
+ git status && git diff --stat # allowed: two read-only git commands
450
+ git status; git log --oneline -5 # allowed
451
+ git diff -- extensions\plan # allowed: a Windows path separator
452
+ git status && rm -rf x # blocked: the second segment is not git
453
+ git status | sh # blocked: pipes are never allowed
454
+ git diff > out.patch # blocked: redirection is a write
455
+ ```
456
+
457
+ A refused shell command explains the fix: the working directory is already the
458
+ project root (so `cd` is unnecessary), and files belong to the `read`, `grep`,
459
+ `find`, and `ls` tools.
460
+
461
+ A string parser can never be the boundary, so capability gating replaced the
462
+ earlier segment-based command allowlist. The git shell guard is a narrow,
463
+ deliberate exception — an allowlist over read-only git command shapes, not a
464
+ general shell parser. If you need anything else during a plan, exit plan mode
465
+ first or wait for approval.
466
+
467
+ ## Read-only delegation
468
+
469
+ While planning, `subagent` stays available but every call is **forced
470
+ read-only**. Plan mode sets `readOnly: true` on the call in the `tool_call`
471
+ guard, and `subagent` in turn gives each spawned agent a reader-only `--tools`
472
+ list (`read`, `grep`, `find`, `ls`, `read_doc`, `web_search`, `web_fetch`). The child
473
+ process enforces it, so a delegated `explorer`, `researcher`, or even `worker`
474
+ cannot write, edit, or run a shell — regardless of its prompt or an external
475
+ agent file that overrides it.
476
+
477
+ ```jsonc
478
+ // The model just calls subagent; plan mode supplies the readOnly flag.
479
+ { "agent": "explorer", "task": "Map how the plan extension gates tools" }
480
+ ```
481
+
482
+ - A `subagent` whose registered schema does not declare `readOnly` is refused
483
+ (default-deny), so an old or unrelated tool cannot be run read-write while
484
+ planning.
485
+ - The delegated run keeps its own isolated context window, so broad exploration
486
+ does not fill the planning session.
487
+ - `readOnly` is also a normal `subagent` parameter; outside plan mode a caller
488
+ may set it to run any agent read-only on purpose.
489
+
490
+ ## Review screen
491
+
492
+ <img src="../../assets/screens/plan.png" alt="The terminal plan review: a Plan Review title rule, a notice that one section changed since the last review, the plan body with its headings and ◼/◻ task checkboxes, a scroll gauge naming the current section and the visible line range, and the Esc/a/r/w, scroll, and section-jump hints" width="100%">
493
+
494
+ `exit_plan_mode` opens this screen on the plan file it just wrote. The body is
495
+ rendered with Pi's own `Markdown` component and `getMarkdownTheme()`, so
496
+ headings, lists, inline code, code blocks, links, and quotes carry the active
497
+ theme's colours; task items get the shared `◼` (done) or `◻` (pending) marker,
498
+ like the bytefly screen and the question dialog. A plan taller than the terminal
499
+ scrolls under a gauge that names the section at the top of the viewport and the
500
+ line range it shows, and the footer lists only the keys that apply.
501
+
502
+ - **Actions.** The footer colours the primary actions (`a approve & run` in
503
+ `success`, `r refine` in `accent`). `a` asks `y/N` in the footer, then ends
504
+ plan mode, restores write access, and seeds `todo`; `y` or `Enter` approves and
505
+ `n`, `q`, or `Esc` goes back to reading. `r` keeps the plan mounted and opens
506
+ an inline editor beneath it (`Enter` submits, an empty buffer does nothing,
507
+ `Esc` returns to the plan); `Esc` keeps planning and tells the model the plan
508
+ was not approved; `x` leaves plan mode without approving and keeps the plan
509
+ file; `w` opens the browser review.
510
+ - **Sections.** The screen splits the body at its headings, names the section you
511
+ are in, and jumps between sections with `n`/`p`; the scroll row names the
512
+ current section when the plan has more than one.
513
+ - **What changed since the last review.** After a refinement, the screen names
514
+ how many sections changed, marks the current one in the scroll row, and the
515
+ browser outline dots each changed section, so a re-review does not mean
516
+ re-reading the whole plan. The baseline is one digest per heading section of
517
+ what the previous review showed, kept as a session entry, so a resumed session
518
+ or a `/tree` restore marks only what changed on its own branch.
519
+ - **Scrolling.** `↑`/`↓` or `j`/`k`, `space`/`b` or `PgUp`/`PgDn`, `d`/`u`,
520
+ `g`/`G`, and the mouse wheel. When the plan overflows, a gauge row above the
521
+ hints shows the scroll position, the percent, and the visible line range (the
522
+ range drops first on a narrow terminal); the gauge comes from the design
523
+ system's `gaugeBar` and stays visible while the refine editor is open, since the
524
+ body above is still clipped.
525
+ - **Narrow widths.** Hints are dropped whole as the terminal narrows, and each
526
+ footer leads with its safe exit (`Esc keep plan` in the review, `Esc back to
527
+ plan` in the inline editor) so the exit survives even when only one hint fits.
528
+
529
+ ## Browser review
530
+
531
+ <img src="../../assets/screens/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title, a status chip, and the Approve & run, Save feedback, and Back to Pi actions, a Share action, a side panel with step progress and a section outline, and the plan as one measured column with a comment rail holding the owner's own comment and a teammate's @priya comment" width="100%">
532
+
533
+ Press `w` on the review screen — or run `/plan review [path]` — to open the plan
534
+ in the default browser on `127.0.0.1`. The page is a reading-first review
535
+ surface: a sticky header carrying the plan's H1 (falling back to the file-name
536
+ slug), a live status chip (connection, comment count, and unsaved changes), the
537
+ theme toggle, and the review actions, over a hairline reading-progress bar. A
538
+ sticky side panel shows the plan's `Steps` section progress and comment count above a
539
+ **section outline**, and the document is GitHub's Markdown content column — one centred
540
+ measure of at most 1012px with a comment rail in its left margin. Every block
541
+ reveals a **+ Comment** action on hover; select
542
+ text first to quote it. **Save feedback** writes the comments into the plan
543
+ file and returns them to the model.
544
+
545
+ - **Shared theme.** The page uses the same Bytefly tokens, base rules, and
546
+ syntax-token colours as `file-browse` (`lib/web/`), follows the system light/dark
547
+ preference until the header toggle sets an explicit choice, and remembers that
548
+ choice under the shared `pi-web-theme` key.
549
+ - **One content column, one rail.** The document is GitHub's Markdown content
550
+ column (a centred measure of at most 1012px), so a plan reads the way the same
551
+ file does on GitHub; each block carries its `+ Comment` button and
552
+ comment-count badge
553
+ in a rail in the left margin, so annotating never reflows the text.
554
+ - **Section outline and facts.** Headings become a sticky outline with jump
555
+ links, an active-section highlight, and a per-section comment count; the panel
556
+ above it reports the `Steps` section's progress (`n/m`, the same list approval
557
+ seeds) and total comments. A
558
+ section that changed since the previous review on this branch carries a dot
559
+ (its `title` and accessible label say so); an unchanged plan shows none.
560
+ - **Inline threads.** A comment renders directly under the block it anchors to,
561
+ labelled and indented as a thread, so it reads in place and the plan keeps one
562
+ measure at every width. There is no separate column to reflow.
563
+ - **Nothing typed is lost.** **Save feedback**, **Approve & run**, and **Back to
564
+ Pi** refuse while a composer holds text and put the cursor back in it, a
565
+ deleted comment is offered back in the header for a few seconds, and this tab's
566
+ unsaved work survives a reload: the comments it added and the text still in its
567
+ composer are kept in `sessionStorage` under a digest of the plan text the page
568
+ rendered, then restored on the next load of that same text with the composer
569
+ reopened at the caret and the selection it quoted, and dropped when the plan
570
+ changed underneath them. A composer closed with `Esc` or **Cancel** discards its
571
+ text, so only an abandoned tab gets it back. Nothing is stored on the server, and
572
+ another writer's comment is never part of the draft.
573
+ - **Prints as a document.** Printing uses the light palette whatever the screen
574
+ shows, drops the header, panel, rail, and progress bar, and lets the comments
575
+ stay with the blocks they belong to.
576
+ - **Keyboard first.** `j`/`k` move to the next or previous block, `c` comments
577
+ on the current block, `a` approves (the approval flow only), `s` saves, `Esc`
578
+ closes the composer or the sheet and then the review, and `?` opens a shortcut
579
+ sheet; the header's `?` button opens the same sheet. Adding, editing, or
580
+ deleting a comment rebuilds the document, so the tab stop returns to that
581
+ block's comment control instead of being dropped, and the status line says
582
+ which change landed; a repaint the stream caused never moves focus. Either
583
+ dialog traps `Tab` and hands focus back to whatever opened it.
584
+ - **Decision in place.** A review opened for approval (`w` on the review screen)
585
+ shows **Approve & run** beside Save feedback: it persists the current comments,
586
+ ends plan mode, restores write access, seeds `todo`, and hands control back to
587
+ the model. Because that cannot be undone from the page, the button and `a`
588
+ open a confirmation that says what happens and starts on **Cancel**; only its
589
+ own **Approve & run** posts. **Save feedback** keeps plan mode and returns the
590
+ comments as a refinement; **Back to Pi** closes the review with no decision.
591
+ Each action settles the page on a terminal state — `Approved`, `Saved`, or
592
+ `Closed` — so the tab never falls to Offline. `/plan review` is comment-only:
593
+ it renders no approve action and the server refuses one.
594
+ - **Markdown that reads.** Headings, lists, task items, blockquotes, fenced
595
+ code, GFM tables, `---` rules, and bare-URL autolinks render; a list item's
596
+ continuation lines hang under the item's text, inline code is a soft fill
597
+ rather than a bordered chip, tables honour the alignment row and scroll
598
+ horizontally when narrow.
599
+ - **Highlighted code.** Fenced code is tokenized by the shared
600
+ `lib/highlight.ts`; the client copies only text and `tok-*` spans through
601
+ `DOMParser`, so a code line is never parsed as markup.
602
+ - **The file is the store.** Each comment is written as
603
+ `> [!REVIEW] <comment>` on the line after the block it anchors to, tagged with
604
+ its author when a teammate wrote it (`> [!REVIEW] @alice: <comment>`). Saving is
605
+ idempotent per writer: that writer's previous markers are replaced and everyone
606
+ else's are kept, so two people reviewing the same plan cannot erase each other.
607
+ Stripping is fence-aware: a `> [!REVIEW]` line inside a code block is
608
+ documentation, not feedback. The file is the only copy of a saved comment; the
609
+ ones this tab has not saved yet live in its own `sessionStorage` and nowhere else.
610
+ - **A revision keeps the review.** `write_plan` passes the file it is replacing
611
+ through `carryFeedback`, which re-attaches each comment by the strongest evidence
612
+ that survived: the anchored block's text, per occurrence, so a plan that repeats
613
+ a step keeps the comments apart; then the block whose wording still overlaps it —
614
+ half the longer block's words — so a reworded step keeps its note, and a step
615
+ that shifted down under an inserted one keeps it too; then the gap it left
616
+ between the steps that survived; then the last block that section still has. A
617
+ comment whose section is gone too moves to the plan's last block rather than
618
+ disappearing, and every one of those placements lands beside a block, never after
619
+ a blank line, so the page keeps the thread under its block. Only a plan with no
620
+ block at all leaves a comment in the unattached list.
621
+ - **Anchored to source.** `blocks.ts` splits the plan (fence-aware) into
622
+ source-line ranges; a comment is placed after the whole anchored block, so a
623
+ fenced code block is never split. A save re-checks the file against what the
624
+ page rendered and answers `409`; the page then offers a **Reload** action,
625
+ because only a reload can re-anchor the comments to the new text.
626
+ - **Back to the model.** A save from the review screen returns `refined: true`
627
+ with the comments, so the model stays in plan mode and revises the same file. A
628
+ save that carries no comments asks for nothing, so the review screen comes back
629
+ instead. `/plan review` sends them as a user message (a plan-mode revision or a
630
+ plain update, depending on the mode).
631
+ - **Loopback first, tokenized.** The server binds `127.0.0.1`, carries a random
632
+ per-session token in the URL, rejects a `Host` header it was not bound on,
633
+ refuses a cross-origin POST, serves only `GET`, `HEAD`, and `POST`, and caps the
634
+ request body. Sharing adds a second listener (below); it never widens the first. A save or cancel
635
+ resolves the session only after its HTTP response is delivered, so the browser
636
+ never sees a false "Failed to fetch". The last bound port is remembered per
637
+ plan, so reviewing the same plan again reuses the origin (and the browser's
638
+ saved theme with it). It is closed on the owner's save or cancel, on the owner's
639
+ page going away, or on `session_shutdown`; a reload reconnects inside that grace,
640
+ so refreshing or reloading a conflicted page does not end the review. A
641
+ reviewer's tab opening or closing is not the review's lifetime.
642
+
643
+ The routes it serves are in [Routes](#routes).
644
+
645
+ ## Shared review
646
+
647
+ A review is not only the owner's. **Share** in the review header (or `/plan share`)
648
+ mints a reviewer link, binds a second listener on the machine's first reachable
649
+ IPv4 address, and shows the link with a QR a phone can scan. **New link** in the
650
+ same dialog mints one more, so each teammate holds a link of their own. **Stop
651
+ sharing** revokes every link and closes that listener; closing the review does
652
+ both.
653
+
654
+ - **Two roles, one token each.** The owner's token — the one the browser opened
655
+ with — decides: it may comment, save (which returns to the model), approve,
656
+ cancel, mint links, and revoke them. A reviewer token may read and comment, and
657
+ its one write route is `POST /api/comment`: `/api/feedback`, `/api/approve`,
658
+ `/api/cancel`, and the `/api/share` family answer `403`, and the page renders
659
+ only the name control and **Post comments** for it — and once that link has
660
+ posted, the control shows the bound name instead of offering the dialog.
661
+ Anything the reviewer holds is reachable only while its link is shared.
662
+ - **One link is one person.** The dialog lists every link with the name it has
663
+ bound, and the two things an owner does with one: **Copy**, and **Revoke**, which
664
+ drops that link alone — its page is told `revoked` and its token stops
665
+ answering, while the others keep working. Revoking the last link stops sharing.
666
+ A review holds at most eight links, because each is a live credential.
667
+ - **The owner's page is the review's lifetime.** A reviewer's tab opening or
668
+ closing changes nothing; the review ends when the owner saves, approves, or
669
+ closes it, or when the owner's own page goes away. A reviewer's post writes the
670
+ plan and updates every open page, and leaves the review waiting. A page that is
671
+ still open when the review ends is told why — `closed`, or `revoked` when its
672
+ link was revoked or sharing was stopped — and keeps the plan readable.
673
+ - **Signed comments.** A reviewer names herself once, on the page, and that name
674
+ is bound to her link on its first post: the link stores under it from then on,
675
+ a second tab of the same link that picked another name is refused (`409`) rather
676
+ than silently re-signing, and a name another live link already holds is refused
677
+ when it is first claimed. A bound name is shown, not offered, so a reviewer can
678
+ edit or withdraw her own notes and nobody else's, and her second post replaces
679
+ rather than duplicates the first. The name is a label on a bearer link, not an
680
+ account, which is why the owner still makes the only decision.
681
+ - **One listener, one machine.** Sharing opens exactly the address the dialog
682
+ printed — never `0.0.0.0` — and the `Host` allowlist becomes loopback plus that
683
+ address, however many links answer on it. A machine with no reachable IPv4
684
+ address reports why instead of printing a link that cannot work. There is no
685
+ tunnel, no relay, and no cross-machine sync: this is the same Wi-Fi.
686
+ - **Live.** Every open page follows the stream, so a reviewer's post appears on the
687
+ owner's page without a reload. The review also polls its own plan file once a
688
+ second, so an edit this review did not make — the model rewriting the plan while
689
+ `/plan review` is open in normal mode, or a teammate's editor — repaints every
690
+ reader instead of waiting for the next request, and a save that changed nothing
691
+ but comments stays quiet. A page with a draft open refuses to repaint and says
692
+ what arrived instead. The owner's terminal is told too — `Plan review: @alice
693
+ posted 2 comments.` — because whoever is waiting in the TUI is not looking at
694
+ the page.
695
+ - **What a shared link is.** A bearer credential: anyone who has it reads the plan
696
+ and can comment, under the one name it binds on its first post. The dialog says
697
+ so, `Referrer-Policy: no-referrer` keeps a click from leaking it, and **Revoke**
698
+ (or **Stop sharing**) is the revocation. The plan file is the record — a comment
699
+ arrives as an author-tagged marker, so `/resume` and the terminal show the same
700
+ review the page did.
701
+
702
+ ## Limitations
703
+
704
+ - PowerShell is not available while planning. On native Windows the shell is
705
+ Git Bash (`bash`), which must be installed for any shell command to run.
706
+ - The read-only guarantee is a guard rail against accidental writes, not a
707
+ security boundary (see [Security](#security)).
708
+ - The plans directory is the only index. `/plan list` walks it on every open and
709
+ keeps no registry, so it sees an external edit or a hand-copied plan
710
+ immediately; it lists a file over the 256 KiB plan limit without reading it,
711
+ and does not index the agent directory's plans while a worktree is active.
712
+ - Approval tracks at most 50 steps: `write_plan` refuses a larger plan, and
713
+ `exit_plan_mode` refuses to present one, so no reviewed step is silently
714
+ dropped. A step longer than 200 characters is shortened in the todo list and
715
+ the approval listing — a display rule only, because the plan file always keeps
716
+ the full text.
717
+ - The todo item for a step is matched by its text. An item renamed after
718
+ approval no longer matches, so `set_plan_steps` leaves it alone and says so in
719
+ its result; the plan file is still updated.
720
+ - Browser review needs a local browser: the server is loopback-only, so a remote
721
+ turn cannot drive it.
722
+ - A browser that never connects leaves the review waiting until the user
723
+ cancels, closes the tab, or aborts the turn.
724
+ - Feedback stripping follows fenced code blocks; an unterminated fence makes the
725
+ rest of the file code, so a marker-looking line after it is treated as content
726
+ rather than stored feedback.
727
+ - A comment is one line. Stored feedback is a single blockquote line, so newlines
728
+ and runs of spaces collapse before it is written; a pasted multi-line
729
+ instruction reaches the model as one sentence, which is what keeps the marker
730
+ grammar valid markdown.
731
+ - The plan-file poll runs once a second, so an edit made elsewhere reaches an open
732
+ page within about that long, and a write that leaves the plan's text identical —
733
+ another process appending only a comment — is not announced; the next save still
734
+ refuses a file it did not render (`409`). It is a poll rather than a file
735
+ watcher, so a change on a filesystem that reports nothing at all is still seen
736
+ at the next request, because every page read re-compares the file.
737
+ - A shared review holds at most eight links. Each is a live bearer credential, so
738
+ a ninth is refused with the cap named rather than minted.
739
+
740
+ ## Non-goals
741
+
742
+ - **Not a sandbox.** Extensions share Pi's OS permissions.
743
+ - **No progress tracker of record.** `todo` owns completion state; the plan
744
+ file only mirrors it (`set_plan_steps`), and plan mode does not read completion
745
+ back out.
746
+ - **No auto-execution.** Approval hands control back to the model.
747
+
748
+ ## Pi integration
749
+
750
+ | Integration point | How |
751
+ |---|---|
752
+ | Tools | `enter_plan_mode` (active; `readOnlyHint: true`), `write_plan` (`defaultActive: false`; `readOnlyHint: false`, `destructiveHint: true`), `exit_plan_mode` (`defaultActive: false`; `readOnlyHint: true`), `cancel_plan_mode` (`defaultActive: false`; `readOnlyHint: true`), `set_plan_steps` (active; `readOnlyHint: false`, `idempotentHint: true`). All five are `executionMode: "sequential"` and closed-world, and the read-only policy default-denies `set_plan_steps` while planning. Each returns typed `details` (`EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `CancelPlanModeDetails`, `SetPlanStepsDetails`), and `ExitPlanModeDetails` carries the review's `annotations` when a browser approval or save supplied comments; no `outputSchema`/`structuredContent`. |
753
+ | Commands and flags | `/plan` (toggle, or enter and send a task), `/plan list` (plans index), `/plan review [path]` (browser review), `/plan share [stop]` (reviewer link), `/plan cancel` (leave without approving), `/skill:init` (skill-backed `AGENTS.md` bootstrap), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
754
+ | State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })`, and each review's baseline persisted with `pi.appendEntry("plan-baseline", { path, sections })` as one digest per heading section; both rebuilt from `ctx.sessionManager.getBranch()`, so `/resume` and `/tree` restore the mode and the changed markers of the branch they land on. Plan writes a goal only at approval, through `ctx.executeTool("goal", { objective })` when the `goal` tool is registered: a `planning` goal is promoted to the plan's `## Goal`, a session with none is given the same objective, and an `active`/`achieved` goal is left alone — no path asks first. A review that shows the same content appends nothing. The last plan path lives in runtime memory for the footer chip, and a bare `/plan review` falls back to the newest plan file in the directory. |
755
+ | Entry requests | `pi.events` channel `PLAN_ENTER_REQUEST` from `lib/plan-entry.ts`: the `goal` workflow asks, with the shared consent sentence, and plan enables the mode on the caller's context without a second dialog. Subscribed on `session_start`, released on `session_shutdown`. |
756
+ | Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context, with a pursued session goal read from the branch through `lib/goal-state.ts`; `context` keeps only the newest plan context while enabled and drops stale ones otherwise. |
757
+ | Lifecycle | `session_start`/`session_tree` restore state; `session_shutdown` clears the footer status and closes any browser review server. |
758
+
759
+ ## Design notes
760
+
761
+ - **Consent is one dialog, whichever extension asks.** The goal workflow offers
762
+ plan mode as a phase of the goal, but a command context cannot call
763
+ `enter_plan_mode` (`ExtensionCommandContext` has no `executeTool`). So the
764
+ consent sentence left `policy.ts` for `lib/plan-entry.ts`, and the entry itself
765
+ is a `pi.events` request: the asker shows the question, plan enables silently
766
+ with the asker's context. `enter_plan_mode` keeps its own confirm, using the
767
+ same sentence, so the model-initiated path is unchanged and the two texts
768
+ cannot drift. A request that nobody answers resolves `false` rather than
769
+ hanging, so `goal` loaded alone still works.
770
+ - **One review loop owns the human gate.** `review.ts` asks however the session
771
+ is attached — TUI screen, dialog plus editor, remote file plus digest, or
772
+ browser — and returns one `ReviewDecision`, so `exit_plan_mode` and
773
+ `/plan review` no longer carry two copies of the browser lifecycle and the
774
+ tool's own `for(;;)` with a `continue` per outcome. The loop also owns the
775
+ baseline rule (mark what the screen showed, after it showed it) and forwards
776
+ the call's abort signal, which is what closes a review on an aborted turn.
777
+ - **Two tools, not one.** `EnterPlanMode`/`ExitPlanMode` are a pair: entering is
778
+ consent to explore read-only, exiting is consent to act. A single toggle tool
779
+ cannot express "the model wants to plan but the user declines".
780
+ - **User entry is instant, model entry confirmed.** `/plan`, `Ctrl+Alt+P`, and
781
+ `--plan` are the user's own action and take effect at once; `enter_plan_mode`
782
+ changes the mode on the user's behalf, so it confirms first.
783
+ - **Review the file, decide in a screen.** The plan is a file, so it is the
784
+ artifact already on screen. `ctx.ui.confirm()` is a non-scrollable selector
785
+ that pushes buttons off-screen for a long plan, so the TUI gets a scrollable
786
+ review screen and dialog-capable modes get a `select` plus editor. The screen
787
+ renders the body with Pi's `Markdown` component so the plan reads like any
788
+ other markdown in the app; the plan text is sanitized before it reaches the
789
+ renderer, so only the theme's own ANSI survives.
790
+ - **`write_plan` is the only write.** It is not the builtin `write`, so the
791
+ capability policy and path backstop stay intact; a `plan_path` must name a
792
+ markdown non-dotfile whose real parent resolves inside the plans directory,
793
+ so symlinks and the directory's own `.gitignore` are never writable targets.
794
+ - **Approval seeds `todo` via `ctx.executeTool`.** The event bus carries no
795
+ `ctx`, so a listener could not refresh the todo widget; calling the real tool
796
+ runs its validation and widget sync and needs no change to `todo`. Only the
797
+ top-level list items of the `Steps` section become steps (the whole document
798
+ is the fallback when that heading is absent), and an indented line continues
799
+ its step, so the outline's prose lists never seed the list. The list is
800
+ whole-list replacement, so the current items
801
+ are read back from the branch's last `todo` result (the published state
802
+ channel; no import from that extension) and the steps are merged into them.
803
+ A rejected seed reports the nested tool's own reason in the approval message
804
+ instead of failing silently.
805
+ - **Ticking a step ticks its item, through the same channel.** The seed's text is
806
+ the step's text (`PlanStep.content`, already elided for a long step), so the
807
+ mirror matches on that text and changes only `status`: an item the user
808
+ renamed is left alone and reported as unmatched rather than overwritten, and a
809
+ repeated call writes nothing. The file is written first and stays
810
+ authoritative, so a failed nested write is reported in the result text — once
811
+ one artifact has changed there is no previous state to hand back.
812
+ - **A cap is enforced where it can still be fixed.** Both refusal points —
813
+ `write_plan` and `exit_plan_mode` — sit before the write and before the review,
814
+ because those are the moments the model can still regroup the steps. A note on
815
+ a successful write would arrive after the file existed and the review had
816
+ already listed every step, which is exactly when the overflow becomes lost
817
+ work.
818
+ - **Changed markers compare what was shown, not revisions on disk.** A review
819
+ result is only meaningful next to the text the reviewer already read, so every
820
+ review marks the sections it presented (keyed by resolved path) and
821
+ `plan-text.ts` compares the next plan against those digests. Stored feedback is
822
+ stripped from both sides, so a review comment never reads as an edit, and the
823
+ same helper feeds the terminal summary and the browser outline. The digests are
824
+ a session entry rather than the previous text, so the markers survive a resume
825
+ without copying the plan file into the branch, and the reader and the writer
826
+ share one normalization so a tab cannot read as a change.
827
+ - **The outline guides, it does not enforce.** `write_plan` documents the
828
+ canonical shape in its description and stores whatever markdown it is given;
829
+ no section is required and nothing is rewritten. The description names the two
830
+ headings that do carry behavior — `Steps` is what approval reads as steps, and
831
+ `Verification` the trailing check — so the model is told the cost of dropping
832
+ one instead of having the plan refused. The review's baseline is keyed by
833
+ heading text, so a refinement is asked to keep its headings; a rename reads as
834
+ an edit, which is the one change the markers cannot tell from a rewrite.
835
+ - **Sections are rendered one at a time.** Pi's `Markdown` exposes no
836
+ source-line map, so `tui.ts` splits the body at its headings and renders each
837
+ section separately. That gives exact offsets for the current-section label and
838
+ `n`/`p`; the single-section case is byte-for-byte the old render.
839
+ - **`set_plan_steps` is a second contained write.** It changes only the named
840
+ steps' checkboxes, in an existing markdown plan inside the plans directory
841
+ (`isContainedPlan`), in a single all-or-nothing write — refusing every entry
842
+ if any step is out of range or an entry's `expected` text no longer matches —
843
+ and refuses while plan mode is on, so the read-only guarantee and the path
844
+ backstop are unchanged. A required `expected` is what makes an ordinal safe:
845
+ a plan edited between approval and ticking is caught instead of silently
846
+ marking the step that moved into that number.
847
+ - **Direction: enforce capabilities at the tool boundary.** (1) A shared
848
+ default-deny policy classifies every call as read-only or mutating instead of
849
+ maintaining a mutator list. (2) Read-only delegation is enforced in the child
850
+ process through `--tools`, not by trusting the agent name or prompt. (3) Raw
851
+ shell is removed except the narrow read-only git allowlist in `git-bash.ts`;
852
+ no general shell parser. (4) A recursive path guard blocks hint-carrying tools
853
+ that take a file path unless they are known plan-safe readers.
854
+ - **State is a custom entry.** `{ enabled, removed }` is excluded from model
855
+ context and reconstructed on start and tree navigation, so exiting plan mode
856
+ restores the tools the branch it was left on hid; the `--plan` flag only
857
+ applies when the branch has no persisted entry, so a later disable survives
858
+ `/resume`.
859
+ - **Browser comments live in the plan, not a sidecar.** A review comment is a
860
+ blockquote in the file the model already owns, so there is one artifact to
861
+ read, revise, and approve; `blocks.ts` supplies the source-line anchors the
862
+ existing heading-only split cannot, and the page renders text with DOM nodes
863
+ so review never widens the plan's trust surface. The browser opener is shared
864
+ through `lib/browser.ts` rather than imported from `file-browse`.
865
+ - **Comments render in the document flow, not a second column.** The anchor is
866
+ already a source line, so the browser needs no paired column: a thread is
867
+ appended after its block and the plan keeps one measure at every width. The
868
+ per-block affordance lives in a narrow rail inside the measure's left margin,
869
+ so annotating never moves the text. That removes the two-column grid, the
870
+ `bf-review--no-rail` collapse, and the breakpoint ladder, and matches the TUI,
871
+ where neither review header carries a progress counter.
872
+ - **A poll, not a file watcher.** An open review re-reads its plan once a second
873
+ and announces when the text stopped matching what the page rendered, because the
874
+ review owns the page and not the file: `/plan review` runs in normal mode, so the
875
+ model can rewrite the plan underneath it. The house rule is a poll (see
876
+ `file-browse`'s note): `fs.watch` is the one part of `node:fs` whose availability
877
+ and event semantics differ per platform, and it would add a long-lived resource
878
+ to start and release. The check only announces — it never writes `pageText`,
879
+ which stays the anchor a save is validated against — so a save through the routes
880
+ says nothing, and an announced edit can be announced once more by the read that
881
+ repaints: that read is what updates `pageText`, so it converges instead of
882
+ looping, at the cost of one extra refetch.
883
+ - **One link is one person, and the token table is the roster.** A shared review's
884
+ unit of access is a link, not a room: a token names a role and binds the name its
885
+ owner posts under, so two people on one link are one writer and the second name
886
+ is refused. The owner hands out one link per teammate and takes one back without
887
+ disturbing the rest, and the exit of a link is the exit of one token, one stream,
888
+ and one marker author — which is why the streams are addressed by token rather
889
+ than by role, and why the link table is pure and separate from the listener that
890
+ answers on it.
891
+
892
+ ## Files
893
+
894
+ | File | Responsibility |
895
+ |---|---|
896
+ | `index.ts` | Wiring: tools, command, flag, shortcut, events, context injection. |
897
+ | `types.ts` | `PlanModeEntry`, `EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `CancelPlanModeDetails`, `SetPlanStepsDetails`. |
898
+ | `policy.ts` | Plan mode's read-only policy, the shared prompt summary, and the bash-refusal guidance. |
899
+ | `git-bash.ts` | `checkReadOnlyGit`: the pure allowlist that restricts `bash` to read-only git commands. |
900
+ | `plans.ts` | Plan-file store: slug/stamp naming, directory resolution (repository, active worktree, or agent directory), containment, atomic `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
901
+ | `todos.ts` | Approval-time todo merge (pure): the last list replayed from the branch, and the merge of the plan's steps into it with the cap respected. |
902
+ | `steps.ts` | Plan-document structure: the outline, step extraction and the `Steps`-section scope, the plan's own goal (`goalSection`), and the checkbox edit. |
903
+ | `sections.ts` | The one fence-aware heading scan: `extractSections`, `sectionKeys` (the key that tells two identically titled sections apart), and the `HEADING` / `FENCE` / `normalizeHeadingText` vocabulary `blocks.ts` reads a heading through. |
904
+ | `format.ts` | Pure result/approval text: the plan preview, the step-cap diagnosis, step labels, the merge summary, and the verification note. |
905
+ | `plan-text.ts` | The plan text a review shows and compares: `sanitizePlanText`, the one normalization both sides use, the per-section `sectionDigests`, and `changedSections`, which hands each section back with its flag. |
906
+ | `runtime.ts` | Enabled state, tool gating, persistence (mode and review baseline), control-tool identity, footer status. |
907
+ | `tools.ts` | Registration entry: composes the five tool registrations in the order the session sees them. |
908
+ | `tools/` | One module per tool (`enter.ts`, `write.ts`, `exit.ts`, `cancel.ts`, `steps.ts`), plus `todo-seed.ts` (the two nested `todo` writes) and `nested.ts` (reading a nested result). |
909
+ | `review.ts` | The review gate: one decision from the TUI screen, a dialog, or the browser, the shared browser lifecycle, and the "mark what was shown" baseline rule. |
910
+ | `review-prompt.ts` | Pure rendering and parsing for the compact remote review: one action table behind both the prompt and the reply parser. |
911
+ | `commands.ts` | `/plan` (enter, optional task), `/plan review [path]`, `/plan list`, `/plan share [stop]`, and `/plan cancel`. |
912
+ | `tui.ts` | `PlanViewComponent` (sectioned Markdown body, changed-section summary and marker, section jump, scroll gauge, the approve `y/N` question, approve/refine/browser/keep). |
913
+ | `plan-index.ts` | `/plan list`: the notice text and the docked index screen (store size and oldest plan, title, step progress, age, focused path; Enter reviews). |
914
+ | `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks), and `contentBlockAt`: the block with content a comment beside blank space belongs to. |
915
+ | `feedback.ts` | Comment codec: authored markers, `stripFeedback`, `readFeedback`/`parseFeedback` (body, blocks, and comments out of one parse), `formatFeedback` (writer-scoped), `displayFeedback`, `carryFeedback`, `feedbackSummary`, and payload validation. |
916
+ | `web.ts` | `createPlanWebReview`: the review session (per-token roles, the change stream, the plan-file poll, the body cap, settle-after-flush, abort) over `lib/http`'s one loopback server. |
917
+ | `web-tokens.ts` | The token table: the role each token names, the collision rule that keeps two roles off one token, and the names the other live links have bound. |
918
+ | `web-streams.ts` | The event streams: who is listening, what a frame is addressed to, and the closed-tab grace that ends the review when the owner's page goes away. |
919
+ | `web-router.ts` | Pure request handler: token role, `Host` allowlist, origin, and method checks through one POST-and-role guard, then the page, comment, owner-save, approve, cancel, and the four share routes. |
920
+ | `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet, approve confirmation, share and name dialogs), its data island, and inline CSS. |
921
+ | `web-share.ts` | Pure sharing rules: the reachable LAN addresses from an injected snapshot, one tokenized URL, the link table (mint, find, revoke, list, state) with its cap, and the reasons a share is unavailable. |
922
+ | `web-share-session.ts` | The shared listener: its bound address, the link table over the session's tokens, `ShareState`, and invite/revoke/stop/close, injected with the request handler, role map, and frame hooks. |
923
+ | `web-client.ts` | The `/client.js` source: state and wiring (comment rail, save/approve/back, keyboard nav, the stream's lifetime) over the three fragments below. |
924
+ | `web-comments.ts` | The client's comment half: selection quotes, the composer, authored cards and who may edit them, delete-with-undo. |
925
+ | `web-collab.ts` | The client's collaboration half: the link list with copy and per-link revoke, the reviewer's name, and the island refetch a stream frame triggers. |
926
+ | `web-markdown.ts` | The client's document half: `safeHref` and the inline/table/code/list rendering (interpolated into `web-client.ts`). |
927
+ | `web-chrome.ts` | The client's chrome half: the state chip, the status line, the header, and reading progress (interpolated into `web-client.ts`). |
928
+ | `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
929
+ | [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
930
+ | [`../../lib/plan-entry.ts`](../../lib/plan-entry.ts) | Plan-mode entry shared with `goal`: the read-only summary, the consent sentence, and the `pi.events` request `plan` answers by enabling the mode. |
931
+ | [`../../lib/shell.ts`](../../lib/shell.ts) | The shell-escape rule the read-only git guard shares with the worktree isolation guard. |
932
+ | [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer (shared with `file-browse`). |
933
+ | [`../../lib/qr.ts`](../../lib/qr.ts) | QR matrix, terminal half-blocks, and inline SVG for the share link (shared with `wechat`). |
934
+ | [`../../lib/http/`](../../lib/http/) | The one loopback server entry, the bind, `Host` allowlist, header sets, and response kit both browser surfaces serve through. |
935
+ | [`../../lib/web/index.ts`](../../lib/web/index.ts) | Shared Bytefly web brand system: tokens, base rules, `tok-*` colors, layout, the bootstrap, the toggle wiring, the shortcut sheet (`keysSheet`, `keysDialogScript`), and shared markup. |
936
+ | [`../../lib/http/index.ts`](../../lib/http/index.ts) | Loopback HTTP layer shared with `file-browse`: the page policy and header sets, `ServeResponse`, the request-envelope helpers, bind/respond/close. |
937
+ | `../../lib/policy.ts` | Shared read-only capability policy (default-deny + `readOnlyHint`). |
938
+ | `../../lib/tui/chrome.ts` | Shared screen header, viewport-row helper, and styled key-hint footer. |
939
+
940
+ ## Testing
941
+
942
+ ```bash
943
+ bun test extensions/plan
944
+ ```
945
+
946
+ Eleven co-located suites, declared in `test/suite-manifest.ts`:
947
+
948
+ - `plans.test.ts` — the plan-file store: naming, directory resolution, atomic
949
+ writes, `read`/`latest`/`index`, containment, `setSteps`, `saveFeedback`, and
950
+ step extraction.
951
+ - `feedback.test.ts` — the comment codec: authored markers, stripping and
952
+ parsing, who a reviewer's write is signed by, carrying feedback onto a
953
+ revision, and displaying stored comments.
954
+ - `tools.test.ts` — registration and all five tools, the approval merge into
955
+ `todo`, the session goal, and the call/result renderings.
956
+ - `review-prompt.test.ts` — the compact remote prompt and its reply parser
957
+ against one action table.
958
+ - `web.test.ts` — the browser review: the comment codec, the router, the page
959
+ roles and page shell, the client script, the stream, the plan-file poll, the
960
+ share rules, and a real loopback session.
961
+ - `web-share.test.ts` — the LAN address selector, `shareUrl`, the link table with
962
+ its cap, and the reasons a share is unavailable.
963
+ - `commands.test.ts` — `/plan review`, `/plan list`, `/plan share`, and the one
964
+ review gate shared by `exit_plan_mode` and the browser, including the abort and
965
+ baseline rules.
966
+ - `runtime.test.ts` — tool gating, persistence and status, control-tool identity,
967
+ the review baseline, branch restore, the read-only git allowlist, and the
968
+ path-argument scan.
969
+ - `tui.test.ts` — the review screen at fixed widths: Markdown body, scrolling,
970
+ section navigation, the gauge, footer hints, the approve question, the inline
971
+ refine editor, and the changed-section markers.
972
+ - `sections.test.ts` — the fence-aware scan: heading shape, fences, the preamble,
973
+ source lines and levels, and the occurrence key.
974
+ - `extension.test.ts` — the Pi wiring: registration, the entry request, the
975
+ session lifecycle, the tool-call guard, injected context, the command, and the
976
+ shortcut.