@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 (324) hide show
  1. package/README.md +66 -52
  2. package/README.zh-CN.md +56 -46
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +507 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
  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 +561 -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} +62 -52
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +298 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +254 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
  25. package/extensions/board/commands.ts +105 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +170 -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 +388 -0
  32. package/extensions/board/result.ts +108 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +90 -0
  35. package/extensions/board/seat.ts +28 -0
  36. package/extensions/board/snapshot.ts +42 -0
  37. package/extensions/board/tools.ts +444 -0
  38. package/extensions/board/transport/router.ts +91 -0
  39. package/extensions/board/transport/server.ts +138 -0
  40. package/extensions/board/types.ts +121 -0
  41. package/extensions/board/view/client.ts +51 -0
  42. package/extensions/board/view/dialog.ts +43 -0
  43. package/extensions/board/view/keys.ts +125 -0
  44. package/extensions/board/view/live.ts +231 -0
  45. package/extensions/board/view/names.ts +91 -0
  46. package/extensions/board/view/page.ts +118 -0
  47. package/extensions/board/view/render.ts +519 -0
  48. package/extensions/board/view/styles.ts +228 -0
  49. package/extensions/board/view/team.ts +327 -0
  50. package/extensions/bytefly/footer/budget.ts +6 -5
  51. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  52. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  53. package/extensions/bytefly/footer/config.ts +19 -8
  54. package/extensions/bytefly/footer/price/price.ts +30 -8
  55. package/extensions/bytefly/footer/register.ts +5 -5
  56. package/extensions/bytefly/footer/render/format.ts +18 -0
  57. package/extensions/bytefly/footer/render/lines.ts +23 -7
  58. package/extensions/bytefly/footer/render/segments.ts +117 -31
  59. package/extensions/bytefly/footer/types.ts +29 -5
  60. package/extensions/bytefly/footer/unpriced.ts +42 -17
  61. package/extensions/bytefly/panel/commands.ts +67 -7
  62. package/extensions/bytefly/panel/filter.ts +68 -0
  63. package/extensions/bytefly/panel/format.ts +16 -15
  64. package/extensions/bytefly/panel/join.ts +56 -13
  65. package/extensions/bytefly/panel/prices.ts +43 -0
  66. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  67. package/extensions/bytefly/panel/settings.ts +75 -43
  68. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  69. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  70. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  71. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  72. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  73. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  74. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  75. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  76. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  77. package/extensions/bytefly/panel/tui.ts +306 -259
  78. package/extensions/bytefly/picker/discovery.ts +0 -2
  79. package/extensions/bytefly/picker/types.ts +0 -2
  80. package/extensions/bytefly/update/check.ts +39 -0
  81. package/extensions/bytefly/update/format.ts +15 -0
  82. package/extensions/bytefly/update/register.ts +73 -34
  83. package/extensions/bytefly/update/state.ts +6 -3
  84. package/extensions/check/commands.ts +2 -1
  85. package/extensions/context/tui.ts +1 -1
  86. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  87. package/extensions/file-browse/files/classify.ts +136 -0
  88. package/extensions/file-browse/files/files.ts +12 -0
  89. package/extensions/file-browse/files/index.ts +90 -0
  90. package/extensions/file-browse/files/list.ts +261 -0
  91. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  92. package/extensions/file-browse/files/tree.ts +101 -0
  93. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  94. package/extensions/file-browse/index.ts +4 -4
  95. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  96. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
  97. package/extensions/file-browse/transport/server.ts +110 -0
  98. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  99. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  100. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  101. package/extensions/file-browse/view/emit.ts +51 -0
  102. package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
  103. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  104. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
  105. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
  106. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  107. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  108. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  109. package/extensions/file-read/schema.ts +2 -1
  110. package/extensions/job/runtime.ts +4 -4
  111. package/extensions/memory/store.ts +12 -5
  112. package/extensions/plan/blocks.ts +23 -2
  113. package/extensions/plan/commands.ts +21 -16
  114. package/extensions/plan/feedback.ts +185 -52
  115. package/extensions/plan/format.ts +10 -3
  116. package/extensions/plan/index.ts +23 -5
  117. package/extensions/plan/plan-files.ts +88 -0
  118. package/extensions/plan/plan-index.ts +43 -9
  119. package/extensions/plan/plan-text.ts +33 -23
  120. package/extensions/plan/plans.ts +27 -34
  121. package/extensions/plan/policy.ts +30 -17
  122. package/extensions/plan/review.ts +3 -27
  123. package/extensions/plan/runtime.ts +52 -11
  124. package/extensions/plan/sections.ts +112 -0
  125. package/extensions/plan/steps.ts +3 -22
  126. package/extensions/plan/todos.ts +51 -3
  127. package/extensions/plan/tools/exit.ts +20 -95
  128. package/extensions/plan/tools/todo-seed.ts +4 -2
  129. package/extensions/plan/tools/write.ts +12 -1
  130. package/extensions/plan/tui.ts +5 -34
  131. package/extensions/plan/types.ts +15 -8
  132. package/extensions/plan/web-blocks.ts +89 -0
  133. package/extensions/plan/web-chrome.ts +3 -5
  134. package/extensions/plan/web-client.ts +179 -330
  135. package/extensions/plan/web-collab.ts +3 -3
  136. package/extensions/plan/web-comments.ts +18 -8
  137. package/extensions/plan/web-composer.ts +80 -0
  138. package/extensions/plan/web-draft.ts +40 -0
  139. package/extensions/plan/web-keys.ts +134 -0
  140. package/extensions/plan/web-outline.ts +192 -0
  141. package/extensions/plan/web-page.ts +4 -8
  142. package/extensions/plan/web-plan.ts +150 -0
  143. package/extensions/plan/web-router.ts +3 -58
  144. package/extensions/plan/web-settle.ts +194 -0
  145. package/extensions/plan/web-share-session.ts +24 -30
  146. package/extensions/plan/web-streams.ts +14 -14
  147. package/extensions/plan/web-tokens.ts +2 -2
  148. package/extensions/plan/web.ts +66 -120
  149. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  150. package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
  151. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  152. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  153. package/extensions/rewind/index.ts +3 -3
  154. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  155. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  156. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  157. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  158. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  159. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  160. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  161. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  162. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  163. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  164. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  165. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  166. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  167. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  168. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  169. package/extensions/session-bus/commands.ts +17 -69
  170. package/extensions/session-bus/format.ts +22 -45
  171. package/extensions/session-bus/index.ts +13 -7
  172. package/extensions/session-bus/join-request.ts +40 -0
  173. package/extensions/session-bus/mail-watch.ts +134 -0
  174. package/extensions/session-bus/notice.ts +32 -4
  175. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  176. package/extensions/session-bus/protocol.ts +19 -49
  177. package/extensions/session-bus/resolve.ts +67 -0
  178. package/extensions/session-bus/result.ts +16 -51
  179. package/extensions/session-bus/rows.ts +11 -5
  180. package/extensions/session-bus/runtime.ts +220 -267
  181. package/extensions/session-bus/store.ts +55 -7
  182. package/extensions/session-bus/tools.ts +31 -66
  183. package/extensions/session-bus/tui.ts +5 -5
  184. package/extensions/session-bus/types.ts +23 -14
  185. package/extensions/subagent/agents.ts +9 -8
  186. package/extensions/subagent/commands.ts +2 -1
  187. package/extensions/subagent/invocation.ts +4 -2
  188. package/extensions/team/commands.ts +145 -0
  189. package/extensions/team/dispatch.ts +133 -0
  190. package/extensions/team/format.ts +119 -0
  191. package/extensions/team/index.ts +34 -0
  192. package/extensions/team/schema.ts +29 -0
  193. package/extensions/team/tools.ts +267 -0
  194. package/extensions/team/types.ts +65 -0
  195. package/extensions/todo/index.ts +4 -1
  196. package/extensions/todo/nudge.ts +8 -2
  197. package/extensions/web-access/fetch/page.ts +4 -1
  198. package/extensions/web-access/fetch/structured.ts +447 -0
  199. package/extensions/web-access/fetch/tool.ts +2 -1
  200. package/extensions/web-access/search/tool.ts +2 -1
  201. package/extensions/wechat/bridge.ts +106 -251
  202. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  203. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  204. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  205. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  206. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  207. package/extensions/wechat/dialog/channels.ts +136 -0
  208. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  209. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  210. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  211. package/extensions/wechat/index.ts +10 -10
  212. package/extensions/wechat/intake.ts +245 -0
  213. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  214. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  215. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  216. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  217. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  218. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  219. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  220. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  221. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  222. package/extensions/wechat/tools.ts +29 -12
  223. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  224. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  225. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  226. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  227. package/extensions/wechat/transport/outbound.ts +142 -0
  228. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  229. package/extensions/wechat/types.ts +10 -1
  230. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  231. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  232. package/extensions/worktree/commands.ts +24 -17
  233. package/extensions/worktree/exit.ts +12 -6
  234. package/extensions/worktree/git.ts +22 -14
  235. package/extensions/worktree/guard.ts +18 -290
  236. package/extensions/worktree/handoff.ts +10 -2
  237. package/extensions/worktree/index.ts +2 -1
  238. package/extensions/worktree/lifecycle.ts +2 -0
  239. package/extensions/worktree/registry.ts +20 -11
  240. package/extensions/worktree/root-tools.ts +13 -3
  241. package/extensions/worktree/runtime.ts +8 -2
  242. package/extensions/worktree/screen.ts +15 -8
  243. package/extensions/worktree/shell.ts +303 -0
  244. package/extensions/worktree/state.ts +1 -1
  245. package/extensions/worktree/status.ts +42 -9
  246. package/extensions/worktree/tools.ts +19 -13
  247. package/lib/README.md +11 -8
  248. package/lib/board/card.ts +479 -0
  249. package/lib/board/links.ts +141 -0
  250. package/lib/board/page-pointer.ts +310 -0
  251. package/lib/board/round-text.ts +81 -0
  252. package/lib/board/round.ts +94 -0
  253. package/lib/board/rules.ts +437 -0
  254. package/lib/board/session.ts +105 -0
  255. package/lib/board/store.ts +497 -0
  256. package/lib/board/view.ts +234 -0
  257. package/lib/errors.ts +12 -4
  258. package/lib/format.ts +35 -0
  259. package/lib/http/guard.ts +112 -0
  260. package/lib/http/index.ts +16 -5
  261. package/lib/http/serve.ts +237 -0
  262. package/lib/http/server.ts +12 -10
  263. package/lib/http/sink.ts +135 -0
  264. package/lib/http/token.ts +38 -0
  265. package/lib/plan-entry.ts +11 -0
  266. package/lib/policy.ts +25 -2
  267. package/lib/requirements.ts +6 -7
  268. package/lib/session-label.ts +64 -0
  269. package/lib/session-roster.ts +201 -0
  270. package/lib/session-tool.ts +184 -0
  271. package/lib/spawn.ts +37 -3
  272. package/lib/stray-fields.ts +45 -0
  273. package/lib/tool-names.ts +38 -2
  274. package/lib/tui/glyphs.ts +3 -2
  275. package/lib/web/README.md +57 -4
  276. package/lib/web/client-runtime.ts +123 -0
  277. package/lib/web/components.ts +5 -0
  278. package/lib/web/index.ts +4 -2
  279. package/lib/web/markup.ts +88 -1
  280. package/lib/web/names.ts +42 -0
  281. package/lib/web/runtime.ts +7 -1
  282. package/lib/web/tokens.ts +25 -2
  283. package/package.json +26 -14
  284. package/skills/next/SKILL.md +4 -3
  285. package/skills/project-docs/SKILL.md +133 -0
  286. package/skills/project-docs/references/audit.md +131 -0
  287. package/skills/project-docs/references/changelog.md +105 -0
  288. package/skills/project-docs/references/discovery.md +127 -0
  289. package/skills/project-docs/references/readme.md +80 -0
  290. package/skills/project-docs/references/release.md +84 -0
  291. package/skills/project-docs/references/roadmap.md +93 -0
  292. package/skills/project-docs/scripts/state.ts +316 -0
  293. package/skills/refactor/SKILL.md +100 -125
  294. package/skills/refactor/references/handoff.md +35 -0
  295. package/skills/refactor/references/signals.md +54 -0
  296. package/skills/refactor/scripts/inventory.ts +587 -0
  297. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  298. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  299. package/extensions/file-browse/README.md +0 -506
  300. package/extensions/file-browse/files.ts +0 -564
  301. package/extensions/file-browse/server.ts +0 -141
  302. package/extensions/job/process.ts +0 -35
  303. package/extensions/plan/split.ts +0 -87
  304. package/extensions/session-bus/README.md +0 -247
  305. package/extensions/session-bus/team.ts +0 -102
  306. package/extensions/wechat/outbound.ts +0 -100
  307. package/lib/plans/sections.ts +0 -172
  308. package/lib/plans/setting.ts +0 -52
  309. package/lib/plans/split.ts +0 -215
  310. package/lib/plans/store.ts +0 -19
  311. package/lib/plans/team.ts +0 -151
  312. package/skills/changelog/SKILL.md +0 -148
  313. package/skills/parallel-dev/SKILL.md +0 -150
  314. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  315. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  316. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  317. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  318. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  319. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  320. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  321. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  322. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  323. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  324. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -16,7 +16,8 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
16
16
  import { type Component, Key, matchesKey, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
17
17
  import { sanitizeLine, shortenPath } from "../../lib/format.ts";
18
18
  import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
19
- import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
19
+ import { detailBlockRows, dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
20
+ import { formatSize } from "../../lib/format.ts";
20
21
  import { clip } from "../../lib/tui/columns.ts";
21
22
  import { emptyState, listRow } from "../../lib/tui/rows.ts";
22
23
  import { titleRule } from "../../lib/tui/rules.ts";
@@ -25,8 +26,12 @@ import type { PlanSummary } from "./plans.ts";
25
26
 
26
27
  /** Rows the screen shows when the terminal height is unknown. */
27
28
  const DEFAULT_ROWS = 14;
28
- /** Fixed rows around the list: header, summary, blank, range, detail, hint, closing rule. */
29
- const CHROME = 7;
29
+ /**
30
+ * Fixed rows around the list body: header, summary, blank, and the closing footer
31
+ * (blank, hint, rule). The focused path adds `detailBlockRows` on top, and `fitRows`
32
+ * adds one more when the range row appears.
33
+ */
34
+ const CHROME = 6;
30
35
  /** Longest title a row draws before the trailing meta wins the column. */
31
36
  const TITLE_WIDTH = 56;
32
37
 
@@ -56,11 +61,33 @@ function localTime(ms: number): string {
56
61
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
57
62
  }
58
63
 
64
+ /**
65
+ * The store's own footprint for one repository: the bytes its plans hold, and the
66
+ * oldest of them. Derived from the summary the directory walk already produced, so
67
+ * reporting a size never becomes a second index that can disagree with the files.
68
+ */
69
+ function footprint(summaries: readonly PlanSummary[]): { bytes: number; oldest: number } {
70
+ let bytes = 0;
71
+ let oldest = summaries[0]?.modified ?? 0;
72
+ for (const summary of summaries) {
73
+ bytes += summary.bytes;
74
+ oldest = Math.min(oldest, summary.modified);
75
+ }
76
+ return { bytes, oldest };
77
+ }
78
+
79
+ /** `2.4 kB — oldest 2d ago`, the store's footprint as the header and the screen say it. */
80
+ function footprintText(summaries: readonly PlanSummary[], nowMs: number): string {
81
+ const { bytes, oldest } = footprint(summaries);
82
+ return `${formatSize(bytes)} — oldest ${relativeAge(oldest, nowMs)}`;
83
+ }
84
+
59
85
  /**
60
86
  * The non-TUI listing: one line per plan with its title, step progress, modified
61
- * time, and path, newest first. The directory is named once in the header.
87
+ * time, and path, newest first. The header names the count, the store's footprint,
88
+ * and the directory once; `now` is injectable so the age can be asserted.
62
89
  */
63
- export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string): string {
90
+ export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string, now: number = Date.now()): string {
64
91
  const where = sanitizeLine(shortenPath(dir));
65
92
  if (summaries.length === 0) {
66
93
  return `No plans in ${where} yet; write_plan saves one there.`;
@@ -69,7 +96,7 @@ export function formatPlanIndex(summaries: readonly PlanSummary[], dir: string):
69
96
  (summary) =>
70
97
  ` ${sanitizeLine(summary.title)} — ${stepsText(summary)} — ${localTime(summary.modified)} — ${sanitizeLine(summary.relativePath)}`,
71
98
  );
72
- return [`Plans (${summaries.length}) — ${where}`, ...lines].join("\n");
99
+ return [`Plans (${summaries.length}) — ${footprintText(summaries, now)} — ${where}`, ...lines].join("\n");
73
100
  }
74
101
 
75
102
  export interface PlanIndexOptions {
@@ -185,7 +212,8 @@ export class PlanIndexComponent implements Component {
185
212
  private summaryLine(width: number): string {
186
213
  const { theme } = this.options;
187
214
  const count = this.summaries.length;
188
- const text = `${paint(theme, "accent", "Plans")} ${paint(theme, "meta", `· ${count} ${count === 1 ? "plan" : "plans"}`)}`;
215
+ const size = footprintText(this.summaries, (this.options.now ?? Date.now)());
216
+ const text = `${paint(theme, "accent", "Plans")} ${paint(theme, "meta", `· ${count} ${count === 1 ? "plan" : "plans"} · ${size}`)}`;
189
217
  const where = paint(theme, "faint", ` · ${sanitizeLine(shortenPath(this.options.dir))}`);
190
218
  return clip(` ${text}${where}`, width);
191
219
  }
@@ -202,8 +230,14 @@ export class PlanIndexComponent implements Component {
202
230
  render(width: number): string[] {
203
231
  const { theme } = this.options;
204
232
  const w = Math.max(1, width);
205
- this.cursor.visible = fitRows(this.options.viewportRows, this.summaries.length, CHROME, DEFAULT_ROWS);
206
233
  this.cursor.sync(this.summaries.length);
234
+ const detail = this.detailLines(w);
235
+ this.cursor.visible = fitRows(
236
+ this.options.viewportRows,
237
+ this.summaries.length,
238
+ CHROME + detailBlockRows(detail),
239
+ DEFAULT_ROWS,
240
+ );
207
241
 
208
242
  const body: string[] = [];
209
243
  if (this.summaries.length === 0) {
@@ -229,7 +263,7 @@ export class PlanIndexComponent implements Component {
229
263
  range: this.cursor.clipped(this.summaries.length)
230
264
  ? formatRange(theme, w, { ...this.cursor.window(this.summaries.length), total: this.summaries.length })
231
265
  : undefined,
232
- detail: this.detailLines(w),
266
+ detail,
233
267
  hint,
234
268
  });
235
269
  }
@@ -16,7 +16,7 @@
16
16
  import { createHash } from "node:crypto";
17
17
  import { stripControlChars } from "../../lib/format.ts";
18
18
  import { displayFeedback, stripFeedback } from "./feedback.ts";
19
- import { extractPlanSections } from "./steps.ts";
19
+ import { extractSections, sectionKeys, type PlanSection } from "./sections.ts";
20
20
 
21
21
  /** Spaces a tab expands to; the terminal's own tab stops are not width-modelled. */
22
22
  const TAB_WIDTH = 4;
@@ -71,7 +71,7 @@ export function planDigest(text: string): string {
71
71
 
72
72
  /** One digest per heading section, in document order, for a persisted baseline. */
73
73
  export function sectionDigests(content: string): SectionDigest[] {
74
- return extractPlanSections(reviewText(content)).map((section) => ({
74
+ return extractSections(reviewText(content)).map((section) => ({
75
75
  title: section.title,
76
76
  digest: digestOf(section.text),
77
77
  }));
@@ -83,30 +83,40 @@ export function sameSections(a: readonly SectionDigest[], b: readonly SectionDig
83
83
  return a.every((section, index) => section.title === b[index].title && section.digest === b[index].digest);
84
84
  }
85
85
 
86
+ /** One section of the text as it reads now, and whether it differs from the baseline. */
87
+ export interface SectionMark {
88
+ /** The section, in the order the document lists it. */
89
+ section: PlanSection;
90
+ /** Whether its content differs from the baseline's section in the same place. */
91
+ changed: boolean;
92
+ }
93
+
86
94
  /**
87
- * Per-section change flags for a second review: `boolean[]` aligned with
88
- * `extractPlanSections(reviewText(current))`, so duplicate headings are flagged
89
- * by occurrence rather than by title. An absent baseline flags nothing, which is
90
- * what a first review of a plan looks like; `stripFeedback` only ever drops
91
- * quote-marker lines, so the flags stay aligned with the sections a screen
92
- * renders from the same content.
95
+ * Per-section change marks for a second review: every section of `current` with
96
+ * whether its content differs from the baseline's section in the same place.
97
+ *
98
+ * The marks carry their sections, so a caller cannot pair the flags with a list it
99
+ * parsed separately — the section list and its marks come out of one scan, in
100
+ * document order. Duplicate headings are told apart by occurrence rather than by
101
+ * title. An absent baseline marks nothing, which is what a first review of a plan
102
+ * looks like; `stripFeedback` only ever drops quote-marker lines, so the marks stay
103
+ * aligned with the sections a screen renders from the same content.
93
104
  */
94
- export function changedSections(baseline: readonly SectionDigest[] | undefined, current: string): boolean[] {
95
- const sections = extractPlanSections(reviewText(current));
96
- if (baseline === undefined) return sections.map(() => false);
105
+ export function changedSections(baseline: readonly SectionDigest[] | undefined, current: string): SectionMark[] {
106
+ const sections = extractSections(reviewText(current));
107
+ if (baseline === undefined) return sections.map((section) => ({ section, changed: false }));
97
108
 
98
- const before = new Map<string, string[]>();
99
- for (const section of baseline) {
100
- const digests = before.get(section.title);
101
- if (digests) digests.push(section.digest);
102
- else before.set(section.title, [section.digest]);
103
- }
109
+ // Same section across two reviews: `sectionKeys` tells a second `## Notes` from
110
+ // the first, so a duplicate heading is never compared with the wrong digest.
111
+ const before = new Map<string, string>();
112
+ sectionKeys(baseline).forEach((key, at) => {
113
+ const digest = baseline[at]?.digest;
114
+ if (digest !== undefined) before.set(key, digest);
115
+ });
104
116
 
105
- const seen = new Map<string, number>();
106
- return sections.map((section) => {
107
- const index = seen.get(section.title) ?? 0;
108
- seen.set(section.title, index + 1);
109
- const prior = before.get(section.title)?.[index];
110
- return prior === undefined || prior !== digestOf(section.text);
117
+ const keys = sectionKeys(sections);
118
+ return sections.map((section, at) => {
119
+ const prior = before.get(keys[at] ?? "");
120
+ return { section, changed: prior === undefined || prior !== digestOf(section.text) };
111
121
  });
112
122
  }
@@ -21,7 +21,6 @@
21
21
  * This module owns paths and bytes only; tool wiring lives in `tools.ts`.
22
22
  */
23
23
 
24
- import { randomBytes } from "node:crypto";
25
24
  import {
26
25
  existsSync,
27
26
  lstatSync,
@@ -38,9 +37,9 @@ import { homedir } from "node:os";
38
37
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
39
38
  import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
40
39
  import { resolveEffectiveCwd } from "../../lib/env.ts";
40
+ import { keepRevision, writeAtomic } from "./plan-files.ts";
41
41
  import { sanitizeLine, shortenPath } from "../../lib/format.ts";
42
42
  import { isInside, isInsideReal } from "../../lib/path.ts";
43
- import { plansDir } from "../../lib/plans/store.ts";
44
43
  import { repoKeyFor } from "../../lib/repo-key.ts";
45
44
  import { carryFeedback, formatFeedback, normalizeAuthor, type PlanAnnotation } from "./feedback.ts";
46
45
  import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } from "./steps.ts";
@@ -49,6 +48,13 @@ import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } f
49
48
  export const MAX_PLAN_BYTES = 256 * 1024;
50
49
  /** Longest slug derived from a plan title. */
51
50
  const MAX_SLUG_LENGTH = 60;
51
+ /** The agent-directory entry plans are filed under. */
52
+ const PLANS_DIRNAME = "plans";
53
+
54
+ /** The plan store: `<agent-dir>/plans`, or `<agent-dir>/plans/<key>` inside a repository. */
55
+ function plansDir(agentDir: string, key?: string): string {
56
+ return key === undefined ? join(agentDir, PLANS_DIRNAME) : join(agentDir, PLANS_DIRNAME, key);
57
+ }
52
58
 
53
59
  /** A plan read from disk. */
54
60
  export interface StoredPlan {
@@ -63,6 +69,11 @@ export interface StoredPlan {
63
69
  /** A plan written to disk, carrying the title that named it. */
64
70
  export interface WrittenPlan extends StoredPlan {
65
71
  title: string;
72
+ /**
73
+ * The revision this write replaced, when it refined an existing plan: the text as the
74
+ * reviewer last read it, kept beside the plan.
75
+ */
76
+ replaced?: { path: string; relativePath: string };
66
77
  }
67
78
 
68
79
  /** One row of the plan index: enough to choose a plan without opening it. */
@@ -94,13 +105,6 @@ export interface WritePlanInput {
94
105
  export interface PlanStore {
95
106
  /** Directory plans are written to for `cwd`. */
96
107
  dirFor(cwd: string): Promise<string>;
97
- /**
98
- * The repository root `cwd` belongs to, or undefined outside a repository.
99
- *
100
- * A repository-scoped setting — the split switch — is read at the root, so a
101
- * session started in a subdirectory still sees what the project turned on.
102
- */
103
- rootFor(cwd: string): Promise<string | undefined>;
104
108
  write(cwd: string, input: WritePlanInput): Promise<WrittenPlan>;
105
109
  /** Read a plan inside the plans directory, or undefined when missing/outside. */
106
110
  read(cwd: string, planPath: string): Promise<StoredPlan | undefined>;
@@ -196,27 +200,6 @@ function resolvePlanArg(dir: string, planPath: string): string {
196
200
  return isAbsolute(text) ? resolve(text) : resolve(dir, text);
197
201
  }
198
202
 
199
- /**
200
- * Write a plan atomically: a hidden sibling temp file, then a rename. A crash
201
- * or a full disk mid-write leaves the previous file intact instead of a
202
- * truncated plan, and the temp name is a dotfile, so neither `latest()` nor a
203
- * plan lookup ever mistakes it for a plan.
204
- */
205
- function writeAtomic(path: string, content: string): void {
206
- const temp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`);
207
- try {
208
- writeFileSync(temp, content, { encoding: "utf-8", flag: "wx" });
209
- renameSync(temp, path);
210
- } catch (error) {
211
- try {
212
- unlinkSync(temp);
213
- } catch {
214
- // The temp file was never created, or the rename already moved it.
215
- }
216
- throw error;
217
- }
218
- }
219
-
220
203
  function uniquePath(dir: string, base: string): string {
221
204
  const taken = (candidate: string): boolean => {
222
205
  try {
@@ -330,8 +313,6 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
330
313
 
331
314
  const dirFor = async (cwd: string): Promise<string> => (await resolve(cwd)).dir;
332
315
 
333
- const rootFor = async (cwd: string): Promise<string | undefined> => (await resolve(cwd)).root;
334
-
335
316
  const write = async (cwd: string, input: WritePlanInput): Promise<WrittenPlan> => {
336
317
  const base = baseOf(cwd);
337
318
  const title = input.title.trim();
@@ -370,8 +351,20 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
370
351
  throw new Error(`The plan is too large with its review comments (${nextBytes} bytes).`);
371
352
  }
372
353
 
354
+ // Both are kept: the plan is what the review and the model read from now on, and the
355
+ // revision is what they read before this call — the only thing a diff, or a comment
356
+ // carried onto a block that moved, can be answered against.
357
+ // A refinement that changed nothing has nothing to keep: the copy would be the plan.
358
+ const replaced = previous === undefined || previous === content ? undefined : keepRevision(path, previous);
373
359
  writeAtomic(path, content);
374
- return { path, relativePath: shortenPath(path), title, content, bytes: nextBytes };
360
+ return {
361
+ path,
362
+ relativePath: shortenPath(path),
363
+ title,
364
+ content,
365
+ bytes: nextBytes,
366
+ ...(replaced === undefined ? {} : { replaced: { path: replaced, relativePath: shortenPath(replaced) } }),
367
+ };
375
368
  };
376
369
 
377
370
  const read = async (cwd: string, planPath: string): Promise<StoredPlan | undefined> => {
@@ -556,5 +549,5 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
556
549
  return summaries;
557
550
  };
558
551
 
559
- return { dirFor, rootFor, write, read, latest, index, setSteps, saveFeedback, takeNotice };
552
+ return { dirFor, write, read, latest, index, setSteps, saveFeedback, takeNotice };
560
553
  }
@@ -14,36 +14,49 @@
14
14
  */
15
15
 
16
16
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
- import { createReadOnlyPolicy, type ReadOnlyPolicy } from "../../lib/policy.ts";
18
- import { ASK_USER_QUESTION_TOOL, GOAL_TOOL, SUBAGENT_TOOL, TODO_TOOL } from "../../lib/tool-names.ts";
17
+ import { createReadOnlyPolicy, WORKSPACE_READERS, type ReadOnlyPolicy } from "../../lib/policy.ts";
18
+ import {
19
+ ASK_USER_QUESTION_TOOL,
20
+ BOARD_TOOL,
21
+ GOAL_TOOL,
22
+ SUBAGENT_TOOL,
23
+ TODO_TOOL,
24
+ WEB_FETCH_TOOL,
25
+ WEB_SEARCH_TOOL,
26
+ } from "../../lib/tool-names.ts";
27
+
19
28
  import { ENTER_TOOL } from "./runtime.ts";
20
29
 
21
30
  /**
22
- * Tools that stay available while planning: structured readers, the trackers,
23
- * the git-only `bash`, and the known web/question readers. They are also the
24
- * tools the path backstop does not second-guess.
31
+ * Tools that stay available while planning: structured readers (documents
32
+ * included), the trackers, the git-only `bash`, and the known web/question
33
+ * readers. They are also the tools the path backstop does not second-guess.
25
34
  */
26
35
  export const PLAN_SAFE_TOOLS = [
27
- "read",
28
- "grep",
29
- "find",
30
- "ls",
36
+ ...WORKSPACE_READERS,
31
37
  TODO_TOOL,
32
38
  GOAL_TOOL,
33
39
  "bash",
34
40
  ASK_USER_QUESTION_TOOL,
35
- "web_search",
36
- "web_fetch",
41
+ WEB_SEARCH_TOOL,
42
+ WEB_FETCH_TOOL,
37
43
  ];
38
44
 
39
45
  /**
40
- * Tools that may stay active while planning. `subagent` is here so it is not
41
- * filtered out, but it is not in {@link PLAN_SAFE_TOOLS}: the `tool_call` guard
42
- * forces it read-only before the path backstop would see it. `powershell` is
43
- * deliberately absent — it is default-denied like any other shell.
46
+ * Tools that may stay active while planning. Two of them are here so they are not
47
+ * filtered out, without being in {@link PLAN_SAFE_TOOLS}: the `tool_call` guard
48
+ * narrows each before the path backstop would see it. `subagent` is forced
49
+ * read-only per call, and `board` admits only its `list` action — a model planning
50
+ * work should be able to read the board it is about to add cards to, and the hint
51
+ * cannot say that because the same tool writes. `powershell` is deliberately
52
+ * absent — it is default-denied like any other shell.
53
+ */
54
+ export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL, BOARD_TOOL];
55
+ /**
56
+ * Tools always blocked while planning, even when annotated read-only.
57
+ * `enter_plan_mode` is denied so it stays hidden while planning; the guard exempts
58
+ * plan's own registration so a call already in flight can still answer "already".
44
59
  */
45
- export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL];
46
- /** Tools always blocked while planning, even when annotated read-only. */
47
60
  const ALWAYS_BLOCKED_TOOLS = ["write", "edit", ENTER_TOOL];
48
61
 
49
62
  /** Shown when a tool call is refused while planning. */
@@ -17,7 +17,6 @@ import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
17
17
  import { askHuman, isRemoteTurn, remoteLanguage } from "../../lib/interaction.ts";
18
18
  import { FULL_SCREEN_OVERLAY } from "../../lib/tui/chrome.ts";
19
19
  import { feedbackSummary, type PlanAnnotation } from "./feedback.ts";
20
- import type { PlanUnit } from "../../lib/plans/team.ts";
21
20
  import type { PlanStore, StoredPlan } from "./plans.ts";
22
21
  import type { SectionDigest } from "./plan-text.ts";
23
22
  import { planReviewPrompt, parsePlanReviewReply } from "./review-prompt.ts";
@@ -64,15 +63,6 @@ export interface ReviewPlanOptions {
64
63
  plan: StoredPlan;
65
64
  /** Aborting the turn closes a browser review and returns the plan unapproved. */
66
65
  signal?: AbortSignal;
67
- /** The split approval would write, so the screen shows what is being approved. */
68
- split?: ReviewSplit;
69
- }
70
-
71
- /** A validated split, as the review screen displays it. */
72
- export interface ReviewSplit {
73
- lead: string;
74
- units: readonly PlanUnit[];
75
- verdict: "pair" | "team";
76
66
  }
77
67
 
78
68
  /** Prompt shown by the dialog-based refine editor in non-TUI modes. */
@@ -94,7 +84,6 @@ async function askPlanReview(
94
84
  ctx: ExtensionToolContext,
95
85
  plan: StoredPlan,
96
86
  baseline: SectionDigest[] | undefined,
97
- split: ReviewSplit | undefined,
98
87
  ): Promise<PlanReviewChoice> {
99
88
  const language = remoteLanguage(ctx) ?? "en";
100
89
  const remote = isRemoteTurn(ctx);
@@ -105,7 +94,6 @@ async function askPlanReview(
105
94
  new PlanViewComponent({
106
95
  plan,
107
96
  baseline,
108
- ...(split === undefined ? {} : { split }),
109
97
  theme,
110
98
  onClose: (action, refinement) => done({ action, refinement }),
111
99
  requestRender: () => tui.requestRender(),
@@ -143,8 +131,6 @@ export interface BrowserReviewOptions {
143
131
  * they are waiting: the page updates itself, the terminal has to be told.
144
132
  */
145
133
  onReviewerComment?: (author: string, count: number) => void;
146
- /** A line the page shows the owner, such as why approval belongs in the terminal. */
147
- note?: string;
148
134
  }
149
135
 
150
136
  /**
@@ -163,7 +149,6 @@ export async function runBrowserReview(
163
149
  canApprove: options.canApprove,
164
150
  baseline: options.baseline,
165
151
  signal: options.signal,
166
- ...(options.note === undefined ? {} : { note: options.note }),
167
152
  ...(options.onReviewerComment === undefined ? {} : { onReviewerComment: options.onReviewerComment }),
168
153
  })
169
154
  .catch(() => undefined);
@@ -182,7 +167,7 @@ export async function runBrowserReview(
182
167
  * cancelled returns to the screen it was opened from.
183
168
  */
184
169
  export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDecision> {
185
- const { ctx, web, plans, baselines, signal, split } = options;
170
+ const { ctx, web, plans, baselines, signal } = options;
186
171
  let plan = options.plan;
187
172
  for (;;) {
188
173
  // An aborted turn has nobody left to answer the screen.
@@ -190,7 +175,7 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
190
175
  // Read the baseline before the screen and mark it after, so what the next
191
176
  // review compares against is the content this one actually showed.
192
177
  const baseline = baselines.reviewed(plan.path);
193
- const choice = await askPlanReview(ctx, plan, baseline, split);
178
+ const choice = await askPlanReview(ctx, plan, baseline);
194
179
  baselines.markReviewed(plan.path, plan.content);
195
180
 
196
181
  if (choice.action === "approve") return { action: "approve", plan, comments: [] };
@@ -206,17 +191,8 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
206
191
  // An unrecognized action keeps planning rather than opening a server.
207
192
  if (choice.action !== "browser") return { action: "keep", plan };
208
193
 
209
- // A split section exists only after approval, and the page anchors comments to
210
- // source lines, so a page cannot show — or approve — a plan it cannot carry.
211
- // The terminal review is where the split is approved.
212
- const splitNote =
213
- split === undefined
214
- ? undefined
215
- : "This plan has a split, which the page cannot show; approve it in the terminal review.";
216
- if (splitNote !== undefined) ctx.ui.notify(splitNote, "info");
217
194
  const result = await runBrowserReview(web, ctx.cwd, plan, {
218
- canApprove: split === undefined,
219
- ...(splitNote === undefined ? {} : { note: splitNote }),
195
+ canApprove: true,
220
196
  baseline,
221
197
  signal,
222
198
  onOpened: (session) => ctx.ui.notify(`Plan review: opened ${session.url}`, "info"),
@@ -11,8 +11,8 @@
11
11
  * markers survive a resume too.
12
12
  *
13
13
  * `write_plan` is the one sanctioned write while planning. The read-only guard
14
- * in `index.ts` permits a control tool only when the registered tool is this
15
- * extension's own (matched by source path), so a same-named tool from another
14
+ * in `index.ts` permits plan's own tools only when the registered tool is this
15
+ * extension's (matched by source path), so a same-named tool from another
16
16
  * extension cannot borrow the exemption.
17
17
  */
18
18
 
@@ -28,7 +28,7 @@ import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
28
28
  import { paint } from "../../lib/tui/tokens.ts";
29
29
  import { createPlanStore, type PlanStore } from "./plans.ts";
30
30
  import { sameSections, sectionDigests, type SectionDigest } from "./plan-text.ts";
31
- import type { PlanModeEntry, ReviewBaselineEntry } from "./types.ts";
31
+ import type { PlanModeEntry, ReviewBaselineEntry, SeededStepsEntry } from "./types.ts";
32
32
 
33
33
  /** Tool the model calls to leave plan mode after approval. */
34
34
  export const EXIT_TOOL = "exit_plan_mode";
@@ -44,13 +44,23 @@ export const SET_PLAN_STEPS_TOOL = "set_plan_steps";
44
44
  export const STATE_TYPE = PLAN_MODE_ENTRY_TYPE;
45
45
  /** Custom-entry type used for a plan's review baseline. */
46
46
  export const BASELINE_TYPE = "plan-baseline";
47
+ /** Custom-entry type used for the steps one approval seeded, so a re-approval can replace them. */
48
+ export const SEEDED_TYPE = "plan-seeded";
47
49
 
48
50
  /** Longest plan-file basename shown in the footer chip. */
49
51
  const STATUS_NAME_LENGTH = 28;
50
52
 
51
- /** Tools plan mode activates itself; the guard permits only this extension's own. */
53
+ /** Tools plan mode activates itself. */
52
54
  const CONTROL_TOOLS = [EXIT_TOOL, CANCEL_TOOL, WRITE_PLAN_TOOL];
53
55
 
56
+ /**
57
+ * Tools the read-only guard exempts when this extension registered them.
58
+ * `enter_plan_mode` is here only so a call already in flight when plan mode came on
59
+ * is answered by the tool ("already in plan mode"); the policy still denies it, so
60
+ * it stays hidden while planning.
61
+ */
62
+ const OWNED_TOOLS = [...CONTROL_TOOLS, ENTER_TOOL];
63
+
54
64
  export interface PlanRuntime {
55
65
  /** Whether plan mode is currently on. */
56
66
  isEnabled(): boolean;
@@ -61,8 +71,8 @@ export interface PlanRuntime {
61
71
  restore(ctx: ExtensionContext): void;
62
72
  /** Plan-file storage for `write_plan` and `exit_plan_mode`. */
63
73
  plans: PlanStore;
64
- /** Whether `toolName` is a plan-owned control tool (not a same-named impostor). */
65
- isControlTool(toolName: string): boolean;
74
+ /** Whether `toolName` is one of plan's own tools (not a same-named impostor) the read-only guard exempts. */
75
+ isOwnTool(toolName: string): boolean;
66
76
  /** Path of the most recently written plan, for the footer chip. */
67
77
  lastPlanPath(): string | undefined;
68
78
  /** Remember a written plan and repaint the footer. */
@@ -71,12 +81,18 @@ export interface PlanRuntime {
71
81
  reviewed(path: string): SectionDigest[] | undefined;
72
82
  /** Remember what a review just showed, as a session entry on this branch. */
73
83
  markReviewed(path: string, content: string): void;
84
+ /** The step texts the last approval of this plan appended, or undefined. */
85
+ seeded(path: string): string[] | undefined;
86
+ /** Record what this approval appended, so a re-approval can replace it. */
87
+ markSeeded(path: string, steps: readonly string[]): void;
74
88
  }
75
89
 
76
90
  /** What the branch carries: the last plan-mode state, and a baseline per plan. */
77
91
  interface BranchState {
78
92
  mode: PlanModeEntry | undefined;
79
93
  baselines: Map<string, SectionDigest[]>;
94
+ /** The step texts each approval appended, per plan path. */
95
+ seeded: Map<string, string[]>;
80
96
  }
81
97
 
82
98
  /** Well-formed `{ title, digest }` pairs from untrusted branch data, or undefined. */
@@ -98,6 +114,7 @@ function readSections(value: unknown): SectionDigest[] | undefined {
98
114
  function readBranchState(ctx: ExtensionContext): BranchState {
99
115
  let mode: PlanModeEntry | undefined;
100
116
  const baselines = new Map<string, SectionDigest[]>();
117
+ const seeded = new Map<string, string[]>();
101
118
  for (const raw of ctx.sessionManager.getBranch()) {
102
119
  const entry = raw as { type?: string; customType?: string; data?: unknown };
103
120
  if (entry.type !== "custom") continue;
@@ -111,6 +128,16 @@ function readBranchState(ctx: ExtensionContext): BranchState {
111
128
  mode = removed ? { enabled: data.enabled, removed } : { enabled: data.enabled };
112
129
  continue;
113
130
  }
131
+ if (entry.customType === SEEDED_TYPE) {
132
+ const data = entry.data as { path?: unknown; steps?: unknown } | null | undefined;
133
+ if (typeof data?.path !== "string" || !Array.isArray(data.steps)) continue;
134
+ // Branch data is untrusted: keep only the strings a step can be.
135
+ seeded.set(
136
+ resolve(data.path),
137
+ data.steps.filter((text): text is string => typeof text === "string"),
138
+ );
139
+ continue;
140
+ }
114
141
  if (entry.customType !== BASELINE_TYPE) continue;
115
142
  const data = entry.data as { path?: unknown; sections?: unknown } | null | undefined;
116
143
  if (typeof data?.path !== "string") continue;
@@ -120,7 +147,7 @@ function readBranchState(ctx: ExtensionContext): BranchState {
120
147
  // still answers a lookup for the plan it names.
121
148
  if (sections) baselines.set(resolve(data.path), sections);
122
149
  }
123
- return { mode, baselines };
150
+ return { mode, baselines, seeded };
124
151
  }
125
152
 
126
153
  /** Compare two paths, resolving symlinks when possible so `/tmp` and `/private/tmp` match. */
@@ -153,17 +180,23 @@ export function createPlanRuntime(
153
180
  // can mark what changed. Persisted with the branch, so `/resume` and `/tree`
154
181
  // compare against the review this branch actually showed.
155
182
  const reviewedPlans = new Map<string, SectionDigest[]>();
183
+ /** The steps each approval appended, so the next approval can replace its own leftovers. */
184
+ const seededPlans = new Map<string, string[]>();
156
185
  // Tools plan mode deactivated, so disabling restores exactly those and does
157
186
  // not re-activate a tool another extension intentionally hid.
158
187
  let removedForPlan: string[] = [];
159
188
 
160
189
  const plans = createPlanStore(pi);
161
190
 
162
- const isControlTool = (toolName: string): boolean => {
163
- if (!CONTROL_TOOLS.includes(toolName)) return false;
191
+ /**
192
+ * Whether `toolName` is plan's own registration, matched by the entry path the
193
+ * host recorded. Older hosts may not record a source; only a recorded mismatch
194
+ * is rejected.
195
+ */
196
+ const isOwnTool = (toolName: string): boolean => {
197
+ if (!OWNED_TOOLS.includes(toolName)) return false;
164
198
  const info = pi.getAllTools().find((tool) => tool.name === toolName);
165
199
  const source = info?.sourceInfo?.path;
166
- // Older hosts may not record a source; only reject a recorded mismatch.
167
200
  return source === undefined || options.entryPath === undefined || samePath(source, options.entryPath);
168
201
  };
169
202
 
@@ -224,6 +257,8 @@ export function createPlanRuntime(
224
257
  // never showed has no digest here, so nothing looks changed until it does.
225
258
  reviewedPlans.clear();
226
259
  for (const [path, sections] of branch.baselines) reviewedPlans.set(path, sections);
260
+ seededPlans.clear();
261
+ for (const [path, steps] of branch.seeded) seededPlans.set(path, steps);
227
262
  const persisted = branch.mode;
228
263
  // `--plan` starts a fresh session in plan mode; an explicit persisted
229
264
  // choice (including a later disable) wins over the launch flag.
@@ -241,13 +276,19 @@ export function createPlanRuntime(
241
276
  if (ctx) setStatus(ctx);
242
277
  },
243
278
  plans,
244
- isControlTool,
279
+ isOwnTool,
245
280
  lastPlanPath: () => lastPlanPath,
246
281
  setLastPlan: (ctx, path) => {
247
282
  lastPlanPath = path;
248
283
  setStatus(ctx);
249
284
  },
250
285
  reviewed: (path) => reviewedPlans.get(resolve(path)),
286
+ seeded: (path) => seededPlans.get(resolve(path)),
287
+ markSeeded: (path, steps) => {
288
+ const key = resolve(path);
289
+ seededPlans.set(key, [...steps]);
290
+ pi.appendEntry(SEEDED_TYPE, { path: key, steps: [...steps] } satisfies SeededStepsEntry);
291
+ },
251
292
  markReviewed: (path, content) => {
252
293
  const key = resolve(path);
253
294
  const sections = sectionDigests(content);