@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
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Plan-document sections (pure).
3
+ *
4
+ * One fence-aware scan of a plan's markdown: the review screen navigates by
5
+ * section, `set_plan_steps` looks up `## Steps`, a carried comment finds the
6
+ * block it belongs to, and the browser outline lists the same headings. A second
7
+ * scanner would disagree about a heading inside a fence, so every reader goes
8
+ * through this module.
9
+ *
10
+ * The scan is deliberately smaller than CommonMark: a heading is up to three
11
+ * leading spaces, a space after the hashes, and an optional closing run; a fence
12
+ * is three backticks or tildes anywhere a fence may start. Inside a fence
13
+ * nothing is a heading, which is what keeps a `# comment` in a shell example out
14
+ * of the outline. Every section keeps its raw source lines, so a caller can edit
15
+ * exactly what it read.
16
+ */
17
+
18
+ /** A heading-delimited chunk of a plan, for navigation and in-place edits. */
19
+ export interface PlanSection {
20
+ /** Plain heading text, or `""` for the content before the first heading. */
21
+ title: string;
22
+ /** The section's markdown, including its heading line. */
23
+ text: string;
24
+ /** 0-based source line of the section's heading (0 for the preamble). */
25
+ line: number;
26
+ /** Heading level, or 0 for the preamble before the first heading. */
27
+ level: number;
28
+ }
29
+
30
+ /**
31
+ * A markdown heading outside a fence, capturing its level and text: up to three
32
+ * leading spaces, a space after the hashes, and a closing hash run only when a
33
+ * space separates it. Shared with the block model, so the section split and the
34
+ * browser outline read a heading line the same way, and `## C#` keeps its title.
35
+ */
36
+ export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)(?:\s+#+)?\s*$/;
37
+
38
+ /**
39
+ * A markdown code-fence opener or closer (backticks or tildes). Shared with the
40
+ * block model: a tab-indented fence is prose for both parsers, never code for one
41
+ * and prose for the other.
42
+ */
43
+ export const FENCE = /^ {0,3}(`{3,}|~{3,})/;
44
+
45
+ /**
46
+ * A heading or step label as the review surfaces compare it: inline emphasis and
47
+ * code removed, whitespace collapsed. Exported so a renderer labels an outline
48
+ * entry the same way {@link extractSections} labels its section, which is what
49
+ * lets the browser mark a changed section.
50
+ */
51
+ export function normalizeHeadingText(text: string): string {
52
+ return text
53
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
54
+ .replace(/\*([^*]+)\*/g, "$1")
55
+ .replace(/(?<![A-Za-z0-9_])_([^_]+)_(?![A-Za-z0-9_])/g, "$1")
56
+ .replace(/`([^`]+)`/g, "$1")
57
+ .replace(/\s+/g, " ")
58
+ .trim();
59
+ }
60
+
61
+ /**
62
+ * Sections of a plan, split at heading lines outside code fences. The content
63
+ * before the first heading is a section titled `""`. Pi's `Markdown` renderer
64
+ * exposes no source-line map, so the review screen renders these chunks
65
+ * separately to know where each section begins.
66
+ */
67
+ export function extractSections(plan: string): PlanSection[] {
68
+ const sections: PlanSection[] = [];
69
+ let current: PlanSection | undefined;
70
+ let fence: string | undefined;
71
+
72
+ const flush = (): void => {
73
+ if (current && (current.title !== "" || current.text.trim() !== "")) sections.push(current);
74
+ current = undefined;
75
+ };
76
+
77
+ plan.split("\n").forEach((line, index) => {
78
+ const fenceMatch = line.match(FENCE);
79
+ if (fenceMatch) {
80
+ if (fence === undefined) fence = fenceMatch[1][0];
81
+ else if (fence === fenceMatch[1][0]) fence = undefined;
82
+ } else if (fence === undefined) {
83
+ const heading = line.match(HEADING);
84
+ if (heading) {
85
+ flush();
86
+ current = { title: normalizeHeadingText(heading[2]), text: line, line: index, level: heading[1].length };
87
+ return;
88
+ }
89
+ }
90
+ if (current === undefined) current = { title: "", text: line, line: 0, level: 0 };
91
+ else current.text += `\n${line}`;
92
+ });
93
+
94
+ flush();
95
+ return sections;
96
+ }
97
+
98
+ /**
99
+ * One key per section that tells two identically titled headings apart: the title
100
+ * and which occurrence of it this is, in document order. The review pairs a
101
+ * baseline digest with the section it belongs to by it, and the feedback codec
102
+ * matches a comment's section across a rewrite, so a second `## Notes` never
103
+ * collects the first one's lines.
104
+ */
105
+ export function sectionKeys(sections: readonly { title: string }[]): string[] {
106
+ const seen = new Map<string, number>();
107
+ return sections.map((section) => {
108
+ const occurrence = seen.get(section.title) ?? 0;
109
+ seen.set(section.title, occurrence + 1);
110
+ return `${occurrence}\u0000${section.title}`;
111
+ });
112
+ }
@@ -18,26 +18,7 @@
18
18
  * checkbox or a `[DONE:n]` marker sets the status.
19
19
  */
20
20
 
21
- import {
22
- extractSections as extractPlanSections,
23
- FENCE,
24
- HEADING,
25
- normalizeHeadingText,
26
- } from "../../lib/plans/sections.ts";
27
-
28
- /**
29
- * The section machinery is `lib/plans/sections.ts`, because the split writes
30
- * `## Team` with the same fence-aware scan this module navigates with. These
31
- * re-exports keep the plan extension's public surface one import.
32
- */
33
- export {
34
- extractSections as extractPlanSections,
35
- FENCE,
36
- HEADING,
37
- normalizeHeadingText,
38
- replaceSection,
39
- } from "../../lib/plans/sections.ts";
40
- export type { PlanSection } from "../../lib/plans/sections.ts";
21
+ import { extractSections, FENCE, HEADING, normalizeHeadingText } from "./sections.ts";
41
22
 
42
23
  /** Most steps seeded from one plan. */
43
24
  export const MAX_STEPS = 50;
@@ -295,7 +276,7 @@ const LIST_PREFIX = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/;
295
276
  */
296
277
  export function verificationItem(plan: string): PlanStep | undefined {
297
278
  if (stepsSection(plan) === undefined) return undefined;
298
- const section = extractPlanSections(plan).find(
279
+ const section = extractSections(plan).find(
299
280
  (candidate) => candidate.level > 0 && candidate.title.toLowerCase() === "verification",
300
281
  );
301
282
  if (!section) return undefined;
@@ -394,7 +375,7 @@ export function setStepStatuses(plan: string, updates: readonly StepEdit[]): str
394
375
  * `## Goal` section, so the caller can fall back to the title.
395
376
  */
396
377
  export function goalSection(plan: string): string | undefined {
397
- const section = extractPlanSections(plan).find(
378
+ const section = extractSections(plan).find(
398
379
  (candidate) => candidate.level === 2 && normalizeHeadingText(candidate.title).toLowerCase() === "goal",
399
380
  );
400
381
  if (section === undefined) return undefined;
@@ -22,6 +22,16 @@ export interface TodoMerge {
22
22
  skipped: number;
23
23
  /** Existing items kept as they were. */
24
24
  kept: number;
25
+ /**
26
+ * Untouched items dropped because they were seeded by an earlier approval of this
27
+ * plan and are no longer one of its steps: the plan was renamed or rewritten.
28
+ */
29
+ replaced: number;
30
+ /**
31
+ * Older seeds kept because someone had already started or finished them. Deleting one
32
+ * would erase work a reader did, so they are reported instead of replaced.
33
+ */
34
+ stale: number;
25
35
  /** Plan steps left out because the list reached its cap. */
26
36
  deferred: number;
27
37
  /**
@@ -89,9 +99,38 @@ function contentKey(content: string): string {
89
99
  * (still subject to the same duplicate and cap rules) but is not counted as a
90
100
  * step, because it is not one.
91
101
  */
92
- export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: number, trailing?: PlanStep): TodoMerge {
102
+ export function mergeTodos(
103
+ existing: ExistingTodo[],
104
+ steps: PlanStep[],
105
+ max: number,
106
+ trailing?: PlanStep,
107
+ /** What an earlier approval of this plan seeded, when there was one. */
108
+ previous?: readonly string[],
109
+ ): TodoMerge {
93
110
  const limit = Number.isInteger(max) && max > 0 ? max : DEFAULT_TODO_MAX;
94
- const todos: ExistingTodo[] = existing.map((todo) =>
111
+ // A plan refined after approval is approved again over the list its first approval
112
+ // made. An item this plan seeded that is no longer one of its steps is a leftover, and
113
+ // only an item *this plan seeded* is a candidate: a user's own item is never touched.
114
+ const ours = new Set((previous ?? []).map(contentKey));
115
+ const current = new Set(steps.map((step) => contentKey(step.content)));
116
+ if (trailing) current.add(contentKey(trailing.content));
117
+ let replaced = 0;
118
+ let stale = 0;
119
+ const kept: ExistingTodo[] = [];
120
+ for (const todo of existing) {
121
+ const key = contentKey(todo.content);
122
+ if (ours.has(key) && !current.has(key)) {
123
+ // An untouched leftover is bookkeeping and goes; one someone acted on is
124
+ // evidence of work, and is named rather than deleted.
125
+ if (todo.status === "pending") {
126
+ replaced += 1;
127
+ continue;
128
+ }
129
+ stale += 1;
130
+ }
131
+ kept.push(todo);
132
+ }
133
+ const todos: ExistingTodo[] = kept.map((todo) =>
95
134
  todo.status === "in_progress" && !todo.activeForm ? { ...todo, activeForm: todo.content } : todo,
96
135
  );
97
136
  const seen = new Set(todos.map((todo) => contentKey(todo.content)));
@@ -126,5 +165,14 @@ export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: num
126
165
  }
127
166
  }
128
167
 
129
- return { todos, added, skipped, kept: existing.length, deferred, trailing: trailingOutcome };
168
+ return {
169
+ todos,
170
+ added,
171
+ skipped,
172
+ kept: existing.length - replaced,
173
+ replaced,
174
+ stale,
175
+ deferred,
176
+ trailing: trailingOutcome,
177
+ };
130
178
  }
@@ -12,7 +12,6 @@ import { basename } from "node:path";
12
12
  import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
13
13
  import { Type, type Static } from "typebox";
14
14
  import { sanitizeLine } from "../../../lib/format.ts";
15
- import { messageOf } from "../../../lib/errors.ts";
16
15
  import { lastGoal } from "../../../lib/goal-state.ts";
17
16
  import { GOAL_TOOL } from "../../../lib/tool-names.ts";
18
17
  import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
@@ -25,8 +24,6 @@ import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
25
24
  import { sanitizePlanText } from "../plan-text.ts";
26
25
  import { planTitle, type StoredPlan } from "../plans.ts";
27
26
  import { reviewPlan } from "../review.ts";
28
- import { decideSplit, rosterCard, SPLIT_OFF_NOTE } from "../split.ts";
29
- import { writeTeam } from "../../../lib/plans/team.ts";
30
27
  import { EXIT_TOOL, SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
31
28
  import { goalSection, MAX_STEPS, planStepSummary } from "../steps.ts";
32
29
  import type { ExitPlanModeDetails } from "../types.ts";
@@ -34,27 +31,11 @@ import type { PlanWebReview } from "../web.ts";
34
31
  import { firstTextLine } from "./nested.ts";
35
32
  import { seedTodos } from "./todo-seed.ts";
36
33
 
37
- const PlanUnitParams = Type.Object({
38
- name: Type.String({ description: "Lowercase slug; the owner session takes it as its label." }),
39
- paths: Type.Array(Type.String(), { description: "Repository-relative path prefixes this unit owns." }),
40
- test: Type.String({ description: "The command that proves the unit is done." }),
41
- dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Units that must land first." })),
42
- lead: Type.Optional(Type.Boolean({ description: "True for the one unit this session keeps." })),
43
- });
44
-
45
34
  const ExitPlanModeParams = Type.Object({
46
35
  plan_path: Type.String({
47
36
  description:
48
37
  "Path to the plan file written by write_plan: the absolute path it returned, or the `~/…` form it displayed. A bare name resolves inside the plans directory. The user reviews this file.",
49
38
  }),
50
- units: Type.Optional(
51
- Type.Array(PlanUnitParams, {
52
- description:
53
- "The plan's slices, when it has two or more independent ones the user will run in separate sessions. " +
54
- "Only used when the plan.split setting is on; validation refuses an overlapping, looping, or oversized " +
55
- "split before the review opens. Omit it to approve as one flow.",
56
- }),
57
- ),
58
39
  });
59
40
 
60
41
  type ExitPlanModeArgs = Static<typeof ExitPlanModeParams>;
@@ -73,13 +54,15 @@ interface GoalOutcome {
73
54
  * Approving the plan *is* the decision, so neither a `planning` goal nor a
74
55
  * session with none is asked again. A final goal (`active` or `achieved`) is the
75
56
  * user's own: approving ordinary work never rewrites it. Recording runs through
76
- * the goal tool, so validation and the widget stay goal's, and no model turn is
77
- * spent.
57
+ * the goal tool when it is registered — validation and the widget stay goal's,
58
+ * and no model turn is spent — and stays silent when that sibling extension is
59
+ * not installed, because there is nothing to report and nothing to act on.
78
60
  */
79
- async function finalizeGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
61
+ async function finalizeGoal(pi: ExtensionAPI, ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
80
62
  const objective = planObjective(plan);
81
63
  const current = lastGoal(ctx.sessionManager.getBranch());
82
64
  if (current !== null && current.status !== "planning") return { objective };
65
+ if (!pi.getAllTools().some((tool) => tool.name === GOAL_TOOL)) return { objective };
83
66
  const outcome = await ctx.executeTool(GOAL_TOOL, { objective, status: "active" });
84
67
  const action = current === null ? "recorded" : "promoted";
85
68
  return outcome.isError ? { objective, action, error: firstTextLine(outcome) } : { objective, action };
@@ -145,26 +128,6 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
145
128
  );
146
129
  }
147
130
 
148
- // The split is decided before the review opens, so a refused or rejected split
149
- // never spends the user's review — and the screen can show what it approves.
150
- const units = args.units?.map((unit) => ({
151
- name: unit.name,
152
- paths: [...unit.paths],
153
- test: unit.test,
154
- dependsOn: [...(unit.dependsOn ?? [])],
155
- ...(unit.lead ? { lead: true } : {}),
156
- }));
157
- const split = decideSplit(ctx.cwd, await runtime.plans.rootFor(ctx.cwd), units);
158
- if (split.kind === "refused") {
159
- return failedResult(`Refused: ${split.reason}`, {
160
- approved: false,
161
- plan: file.content,
162
- planPath: file.path,
163
- relativePath: file.relativePath,
164
- } satisfies ExitPlanModeDetails);
165
- }
166
- const pending = split.kind === "team" ? split : undefined;
167
-
168
131
  if (!ctx.hasUI) {
169
132
  return failedResult(
170
133
  `No interactive UI is available to approve the plan. It is saved at ${file.relativePath}; present it and ask the user to approve it in their reply.`,
@@ -187,7 +150,6 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
187
150
  baselines: runtime,
188
151
  plan: file,
189
152
  signal,
190
- ...(pending === undefined ? {} : { split: { ...pending.team, verdict: pending.verdict } }),
191
153
  });
192
154
 
193
155
  if (decision.action === "refine") {
@@ -250,33 +212,18 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
250
212
  };
251
213
  }
252
214
 
253
- // The team becomes real here, never before: the section is what a joining
254
- // session reads, so it appears only after the user approved the split. The
255
- // write happens while plan mode is still on, so a failure leaves the session
256
- // able to retry instead of approved-but-empty.
257
- let approved = decision.plan;
258
- if (pending !== undefined) {
259
- try {
260
- approved = await runtime.plans.write(ctx.cwd, {
261
- title: planTitle(basename(decision.plan.path)),
262
- content: writeTeam(decision.plan.content, { lead: pending.team.lead, units: pending.team.units }),
263
- planPath: decision.plan.path,
264
- });
265
- } catch (writeError) {
266
- return failedResult(
267
- `The plan was approved but its team could not be written: ${messageOf(writeError)}. Plan mode is still on and no steps were seeded; fix the plan file and call exit_plan_mode again.`,
268
- {
269
- approved: false,
270
- plan: decision.plan.content,
271
- planPath: decision.plan.path,
272
- relativePath: decision.plan.relativePath,
273
- } satisfies ExitPlanModeDetails,
274
- );
275
- }
276
- }
277
215
  runtime.disable(ctx);
278
- const { steps, merged, error } = await seedTodos(pi, ctx, approved.content);
279
- const goal = await finalizeGoal(ctx, approved);
216
+ // A plan refined and approved again seeds over the list its first approval made:
217
+ // the steps that approval appended are the ones this one may replace.
218
+ const previous = runtime.seeded(decision.plan.path);
219
+ const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content, previous);
220
+ if (error === undefined && steps.length > 0) {
221
+ runtime.markSeeded(
222
+ decision.plan.path,
223
+ steps.map((step) => step.content),
224
+ );
225
+ }
226
+ const goal = await finalizeGoal(pi, ctx, decision.plan);
280
227
  const listing =
281
228
  steps.length > 0
282
229
  ? `\n\n${steps
@@ -303,41 +250,19 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
303
250
  const browserNote = decision.comments.length
304
251
  ? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
305
252
  : "";
306
- const splitNote =
307
- pending === undefined
308
- ? split.kind === "off"
309
- ? `\n\n${SPLIT_OFF_NOTE}`
310
- : ""
311
- : `\n\n${rosterCard(pending.team, approved.path, pi.getSessionName())}\n\nWork only your own unit (${pending.team.lead}); the other units belong to the sessions the user starts.`;
312
253
  return {
313
254
  content: [
314
255
  {
315
256
  type: "text",
316
- text: `Plan approved. Plan mode is off and write access is restored. The plan file ${approved.relativePath} is the source of truth; follow its steps.${browserNote}${splitNote}${tail}${verification}${goalNote}${listing}`,
257
+ text: `Plan approved. Plan mode is off and write access is restored. The plan file ${decision.plan.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${verification}${goalNote}${listing}`,
317
258
  },
318
259
  ],
319
260
  details: {
320
261
  approved: true,
321
- plan: approved.content,
322
- planPath: approved.path,
323
- relativePath: approved.relativePath,
262
+ plan: decision.plan.content,
263
+ planPath: decision.plan.path,
264
+ relativePath: decision.plan.relativePath,
324
265
  annotations: decision.comments.length > 0 ? decision.comments : undefined,
325
- ...(pending === undefined
326
- ? split.kind === "off"
327
- ? { splitOff: true }
328
- : {}
329
- : {
330
- team: {
331
- lead: pending.team.lead,
332
- verdict: pending.verdict,
333
- units: pending.team.units.map((unit) => ({
334
- name: unit.name,
335
- paths: unit.paths,
336
- test: unit.test,
337
- dependsOn: unit.dependsOn,
338
- })),
339
- },
340
- }),
341
266
  } satisfies ExitPlanModeDetails,
342
267
  };
343
268
  },
@@ -36,14 +36,16 @@ export async function seedTodos(
36
36
  pi: ExtensionAPI,
37
37
  ctx: ExtensionToolContext,
38
38
  plan: string,
39
+ /** What an earlier approval of the same plan appended, when it was approved before. */
40
+ previous?: readonly string[],
39
41
  ): Promise<{ steps: PlanStep[]; merged?: TodoMerge; error?: string }> {
40
42
  const steps = extractPlanSteps(plan);
41
43
  const verification = verificationItem(plan);
42
44
  if (steps.length === 0 && !verification) return { steps };
43
- const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification);
45
+ const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification, previous);
44
46
  // Nothing new to write: every step (and the verification item) is already in
45
47
  // the list, or the list is full, so a rewrite would only churn the widget.
46
- if (merged.added === 0 && merged.trailing !== "added") return { steps, merged };
48
+ if (merged.added === 0 && merged.trailing !== "added" && merged.replaced === 0) return { steps, merged };
47
49
  const outcome = await ctx.executeTool(TODO_TOOL, { todos: merged.todos });
48
50
  if (!outcome.isError) return { steps, merged };
49
51
  // The nested call fails both when the tool is unavailable and when it rejects
@@ -93,8 +93,19 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
93
93
  // write; say so once, where the user is looking.
94
94
  const moved = runtime.plans.takeNotice();
95
95
  if (moved) ctx.ui.notify(moved, "info");
96
+ // A refinement says where the text it replaced went: a reviewer's comment that was
97
+ // carried to a block that moved can be checked against what she actually read.
98
+ const kept =
99
+ file.replaced === undefined ? "" : `, keeping the revision it replaced at ${file.replaced.relativePath}`;
100
+ // A plan that was approved before is now out of step with the todo list, and
101
+ // the list is only re-seeded by approving it again. Say so here, where the
102
+ // refinement is made, instead of letting the two disagree in silence.
103
+ const reseed =
104
+ runtime.seeded(file.path) === undefined
105
+ ? ""
106
+ : " This plan was approved before: its todo items are the ones that approval seeded, and approving it again replaces the steps that changed (an item you started or finished is left alone and named).";
96
107
  return textResult(
97
- `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
108
+ `Wrote the plan to ${file.relativePath}${kept}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}${reseed}`,
98
109
  { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
99
110
  );
100
111
  } catch (error) {
@@ -29,7 +29,6 @@ import {
29
29
  type TuiMouseEventResult,
30
30
  } from "@earendil-works/pi-tui";
31
31
  import { sanitizeLine } from "../../lib/format.ts";
32
- import { renderTeam, type PlanUnit } from "../../lib/plans/team.ts";
33
32
  import {
34
33
  editorTheme,
35
34
  screenHint,
@@ -46,7 +45,6 @@ import { emptyState, checkbox } from "../../lib/tui/rows.ts";
46
45
  import { paint } from "../../lib/tui/tokens.ts";
47
46
  import { planTitle, type StoredPlan } from "./plans.ts";
48
47
  import { changedSections, planDisplayText, type SectionDigest } from "./plan-text.ts";
49
- import { extractPlanSections } from "./steps.ts";
50
48
 
51
49
  /** What the user chose from the read/review screen. */
52
50
  export type PlanViewAction = "approve" | "refine" | "keep" | "browser" | "cancel";
@@ -87,11 +85,6 @@ export interface PlanViewOptions {
87
85
  tui?: TUI;
88
86
  /** Markdown theme for the body; defaults to Pi's active theme. */
89
87
  markdownTheme?: MarkdownTheme;
90
- /**
91
- * The split approval would write. Rendered as a read-only section after the
92
- * plan, because the `## Team` section itself only exists after approval.
93
- */
94
- split?: { lead: string; units: readonly PlanUnit[]; verdict: "pair" | "team" };
95
88
  }
96
89
 
97
90
  /** Rows the screen shows when the terminal height is unknown. */
@@ -207,11 +200,10 @@ export class PlanViewComponent implements Component {
207
200
  const base = this.options.markdownTheme ?? getMarkdownTheme();
208
201
  const theme = planMarkdownTheme(this.theme, base);
209
202
  const current = planDisplayText(this.options.plan.content);
210
- const changed = changedSections(this.options.baseline, current);
211
- const chunks = extractPlanSections(current);
203
+ const marks = changedSections(this.options.baseline, current);
212
204
  const lines: string[] = [];
213
205
  const sections: SectionOffset[] = [];
214
- chunks.forEach((chunk, index) => {
206
+ marks.forEach(({ section: chunk, changed }, index) => {
215
207
  // Markdown pads every line to the full width for backgrounds; this screen
216
208
  // draws no background, so the padding is dropped.
217
209
  const rendered = new Markdown(chunk.text, padding, 0, theme)
@@ -220,38 +212,17 @@ export class PlanViewComponent implements Component {
220
212
  sections.push({
221
213
  title: chunk.title,
222
214
  offset: lines.length,
223
- ...(changed[index] ? { changed: true } : {}),
215
+ ...(changed ? { changed: true } : {}),
224
216
  });
225
217
  lines.push(...rendered);
226
218
  // Sections render independently, so restore the blank line a heading-only
227
219
  // chunk omits before the next section.
228
- if (index < chunks.length - 1 && lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
220
+ if (index < marks.length - 1 && lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
229
221
  });
230
- const split = this.splitSection();
231
- if (split !== undefined) {
232
- if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
233
- const rendered = new Markdown(split, padding, 0, theme)
234
- .render(width)
235
- .map((line) => truncateToWidth(line.trimEnd(), width));
236
- sections.push({ title: "Split", offset: lines.length });
237
- lines.push(...rendered);
238
- }
239
- this.cache = { width, lines, sections, changedCount: changed.filter(Boolean).length };
222
+ this.cache = { width, lines, sections, changedCount: marks.filter((mark) => mark.changed).length };
240
223
  return this.cache;
241
224
  }
242
225
 
243
- /**
244
- * The split as a read-only section: the same table approval writes, so what the
245
- * user approves is what the plan file will carry.
246
- */
247
- private splitSection(): string | undefined {
248
- const split = this.options.split;
249
- if (split === undefined) return undefined;
250
- const owners = split.units.filter((unit) => !unit.lead).length;
251
- const summary = `${split.verdict} · lead + ${owners} owner${owners === 1 ? "" : "s"} · written on approval`;
252
- return ["## Split", "", summary, "", renderTeam({ lead: split.lead, units: [...split.units] })].join("\n");
253
- }
254
-
255
226
  /** The section whose heading is at or above the viewport top. */
256
227
  private currentSection(sections: SectionOffset[]): SectionOffset | undefined {
257
228
  let current: SectionOffset | undefined;
@@ -50,6 +50,21 @@ export interface ReviewBaselineEntry {
50
50
  sections: SectionDigest[];
51
51
  }
52
52
 
53
+ /**
54
+ * What one approval put into the todo list, so the next approval can replace it.
55
+ *
56
+ * A plan refined after it was approved is approved again, and the list still holds the
57
+ * items seeded the first time. Their texts are the only record of which items came from
58
+ * this plan, so a re-approval keeps them to recognise — and replace — its own leftovers
59
+ * without touching an item the user was already tracking.
60
+ */
61
+ export interface SeededStepsEntry {
62
+ /** Resolved path of the approved plan file. */
63
+ path: string;
64
+ /** The step texts the approval appended, in order. */
65
+ steps: string[];
66
+ }
67
+
53
68
  /** Structured result carried in `enter_plan_mode`'s `details`. */
54
69
  export interface EnterPlanModeDetails {
55
70
  /** Whether the user agreed to enter plan mode. */
@@ -104,14 +119,6 @@ export interface ExitPlanModeDetails {
104
119
  cancelled?: boolean;
105
120
  /** True when no interactive UI was available to ask for approval. */
106
121
  unavailable?: boolean;
107
- /** The team approval wrote into the plan, when the plan was split. */
108
- team?: {
109
- lead: string;
110
- units: Array<{ name: string; paths: string[]; test: string; dependsOn: string[] }>;
111
- verdict: "pair" | "team";
112
- };
113
- /** True when the model proposed a split while `plan.split` was off. */
114
- splitOff?: boolean;
115
122
  }
116
123
 
117
124
  /** Structured result carried in `cancel_plan_mode`'s `details`. */
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The review page's block navigation: which block is active, and how a key walks them.
3
+ *
4
+ * The fourth capability out of the review client's template literal. Each function takes
5
+ * the document it reads and returns the index it computed, so the client keeps the one
6
+ * variable (`activeIndex`) and these own the rules: clamp to the blocks that exist, start
7
+ * at the first or last when nothing is active, and land beside the reader rather than
8
+ * jumping to the middle of the page.
9
+ *
10
+ * `el` comes from the runtime, declared here because this source is spliced into the same
11
+ * bundle; the declaration emits nothing.
12
+ */
13
+
14
+ import type { ClientNode } from "../../lib/web/client-runtime.ts";
15
+
16
+ /** The island's view of one block, as far as navigation needs it. */
17
+ export interface IslandBlock {
18
+ start: number;
19
+ kind?: string;
20
+ }
21
+
22
+ /**
23
+ * The rendered blocks.
24
+ *
25
+ * A rebuild replaces them, so this is read fresh rather than remembered: the array a
26
+ * caller holds goes stale the moment `renderPlan` throws the DOM away.
27
+ */
28
+ export function blockElements(planEl: ClientNode | null): ClientNode[] {
29
+ if (!planEl) return [];
30
+ const nodes = planEl.querySelectorAll(".bf-block");
31
+ const blocks: ClientNode[] = [];
32
+ for (let i = 0; i < nodes.length; i++) {
33
+ const block = nodes[i];
34
+ if (block) blocks.push(block);
35
+ }
36
+ return blocks;
37
+ }
38
+
39
+ /**
40
+ * Make one block active and bring it into view, or say nothing when there are no blocks.
41
+ *
42
+ * `null` means "nothing changed": a page with no blocks has no active block, and the
43
+ * caller keeps the index it had rather than being reset to zero.
44
+ */
45
+ export function setActive(planEl: ClientNode | null, index: number): number | null {
46
+ const blocks = blockElements(planEl);
47
+ if (blocks.length === 0) return null;
48
+ const next = Math.max(0, Math.min(blocks.length - 1, index));
49
+ for (let i = 0; i < blocks.length; i++) {
50
+ const block = blocks[i];
51
+ if (block) block.classList.toggle("bf-block--active", i === next);
52
+ }
53
+ const current = blocks[next];
54
+ if (current && current.scrollIntoView) current.scrollIntoView({ block: "nearest" });
55
+ return next;
56
+ }
57
+
58
+ /**
59
+ * The index one step from `activeIndex`, or `null` when there is nothing to walk.
60
+ *
61
+ * With nothing active yet, `j` starts at the top and `k` at the bottom, which is what a
62
+ * reader pressing either key on a fresh page expects.
63
+ */
64
+ export function moveActive(planEl: ClientNode | null, activeIndex: number, delta: number): number | null {
65
+ const blocks = blockElements(planEl);
66
+ if (blocks.length === 0) return null;
67
+ const next = activeIndex < 0 ? (delta < 0 ? blocks.length - 1 : 0) : activeIndex + delta;
68
+ return setActive(planEl, next);
69
+ }
70
+
71
+ /**
72
+ * The rendered block and its island block for a source line, when both exist.
73
+ *
74
+ * Both halves are required: a rendered block with no island entry has nothing to comment
75
+ * on, and an island entry with no rendered block is a comment the page cannot place.
76
+ */
77
+ export function blockAtLine(
78
+ planEl: ClientNode | null,
79
+ blocks: readonly IslandBlock[],
80
+ line: number,
81
+ ): { wrap: ClientNode; block: IslandBlock } | null {
82
+ const wrap = planEl ? planEl.querySelector('.bf-block[data-line="' + line + '"]') : null;
83
+ let block: IslandBlock | undefined;
84
+ for (const candidate of blocks) if (candidate.start === line) block = candidate;
85
+ return wrap && block ? { wrap, block } : null;
86
+ }
87
+
88
+ /** The block rules as source, for the client bundle. */
89
+ export const WEB_BLOCKS_JS = [blockElements, setActive, moveActive, blockAtLine].map(String).join("\n\n");