@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 (298) hide show
  1. package/README.md +46 -30
  2. package/README.zh-CN.md +40 -25
  3. package/assets/screens/board.png +0 -0
  4. package/assets/screens/chrome.png +0 -0
  5. package/assets/screens/file-browse.png +0 -0
  6. package/assets/screens/plan-review.png +0 -0
  7. package/assets/screens/plan.png +0 -0
  8. package/docs/extensions/README.md +22 -23
  9. package/docs/extensions/board.md +397 -168
  10. package/docs/extensions/bytefly.md +234 -197
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +57 -63
  14. package/docs/extensions/file-open.md +9 -11
  15. package/docs/extensions/file-read.md +12 -14
  16. package/docs/extensions/goal.md +15 -17
  17. package/docs/extensions/job.md +54 -42
  18. package/docs/extensions/memory.md +17 -19
  19. package/docs/extensions/plan.md +197 -140
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +145 -0
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +83 -44
  26. package/docs/extensions/todo.md +18 -18
  27. package/docs/extensions/watch.md +31 -31
  28. package/docs/extensions/web-access.md +2 -4
  29. package/docs/extensions/wechat.md +34 -35
  30. package/docs/extensions/worktree.md +56 -50
  31. package/extensions/board/commands.ts +79 -25
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +17 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +5 -20
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +65 -93
  38. package/extensions/board/result.ts +30 -39
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +49 -11
  41. package/extensions/board/snapshot.ts +8 -6
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +154 -55
  44. package/extensions/board/transport/router.ts +3 -5
  45. package/extensions/board/transport/server.ts +59 -28
  46. package/extensions/board/types.ts +36 -42
  47. package/extensions/board/view/card.ts +229 -0
  48. package/extensions/board/view/client.ts +35 -16
  49. package/extensions/board/view/dialog.ts +169 -6
  50. package/extensions/board/view/empty.ts +47 -0
  51. package/extensions/board/view/keys.ts +13 -2
  52. package/extensions/board/view/live.ts +90 -28
  53. package/extensions/board/view/names.ts +13 -4
  54. package/extensions/board/view/page.ts +38 -13
  55. package/extensions/board/view/render.ts +219 -277
  56. package/extensions/board/view/styles.ts +130 -32
  57. package/extensions/board/view/team.ts +297 -76
  58. package/extensions/bytefly/block/fallback.ts +1 -1
  59. package/extensions/bytefly/doctor/runtime.ts +5 -2
  60. package/extensions/bytefly/footer/collect/label.ts +60 -0
  61. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  62. package/extensions/bytefly/footer/config.ts +7 -8
  63. package/extensions/bytefly/footer/register.ts +33 -4
  64. package/extensions/bytefly/footer/render/footer.ts +18 -4
  65. package/extensions/bytefly/footer/render/key.ts +46 -0
  66. package/extensions/bytefly/footer/render/layout.ts +13 -3
  67. package/extensions/bytefly/footer/render/lines.ts +10 -7
  68. package/extensions/bytefly/footer/render/segments.ts +41 -38
  69. package/extensions/bytefly/footer/render/status.ts +0 -11
  70. package/extensions/bytefly/footer/types.ts +7 -0
  71. package/extensions/bytefly/header/component.ts +4 -20
  72. package/extensions/bytefly/header/config.ts +1 -1
  73. package/extensions/bytefly/index.ts +32 -6
  74. package/extensions/bytefly/mode/editor.ts +115 -0
  75. package/extensions/bytefly/panel/commands.ts +210 -120
  76. package/extensions/bytefly/panel/format.ts +4 -4
  77. package/extensions/bytefly/panel/settings.ts +73 -6
  78. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  79. package/extensions/bytefly/update/cache.ts +1 -1
  80. package/extensions/bytefly/update/register.ts +4 -4
  81. package/extensions/bytefly/working/config.ts +5 -7
  82. package/extensions/bytefly/working/format.ts +10 -13
  83. package/extensions/bytefly/working/register.ts +22 -27
  84. package/extensions/check/commands.ts +3 -2
  85. package/extensions/check/render.ts +10 -1
  86. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  87. package/extensions/context/commands.ts +2 -2
  88. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  89. package/extensions/file-browse/config/config.ts +4 -19
  90. package/extensions/file-browse/index.ts +89 -79
  91. package/extensions/file-browse/live/live.ts +1 -1
  92. package/extensions/file-browse/transport/router.ts +12 -4
  93. package/extensions/file-browse/transport/server.ts +61 -28
  94. package/extensions/file-browse/view/chrome.ts +4 -4
  95. package/extensions/file-browse/view/client.ts +3 -1
  96. package/extensions/file-browse/view/html.ts +73 -40
  97. package/extensions/file-browse/view/live-client.ts +2 -2
  98. package/extensions/file-browse/view/markdown.ts +13 -14
  99. package/extensions/file-browse/view/palette.ts +2 -2
  100. package/extensions/file-browse/view/raw-html.ts +9 -9
  101. package/extensions/file-browse/view/tree.ts +1 -1
  102. package/extensions/file-browse/view/urls.ts +20 -6
  103. package/extensions/file-read/render.ts +4 -4
  104. package/extensions/goal/plan-route.ts +2 -2
  105. package/extensions/goal/tui.ts +12 -2
  106. package/extensions/job/child.ts +527 -0
  107. package/extensions/job/clock.ts +142 -0
  108. package/extensions/job/commands.ts +4 -2
  109. package/extensions/job/config.ts +8 -1
  110. package/extensions/job/format.ts +22 -1
  111. package/extensions/job/log-writer.ts +13 -2
  112. package/extensions/job/registry.ts +63 -7
  113. package/extensions/job/render.ts +34 -0
  114. package/extensions/job/runtime.ts +296 -655
  115. package/extensions/job/session.ts +132 -19
  116. package/extensions/job/store.ts +7 -7
  117. package/extensions/job/tools.ts +21 -9
  118. package/extensions/job/types.ts +47 -0
  119. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
  120. package/extensions/job/waiters.ts +44 -1
  121. package/extensions/memory/commands.ts +4 -2
  122. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  123. package/extensions/plan/commands.ts +12 -9
  124. package/extensions/plan/feedback.ts +62 -16
  125. package/extensions/plan/format.ts +8 -2
  126. package/extensions/plan/git-bash.ts +115 -16
  127. package/extensions/plan/index.ts +58 -51
  128. package/extensions/plan/plan-index.ts +4 -2
  129. package/extensions/plan/plan-text.ts +114 -0
  130. package/extensions/plan/plans.ts +5 -54
  131. package/extensions/plan/policy.ts +8 -10
  132. package/extensions/plan/review-lazy.ts +85 -0
  133. package/extensions/plan/review.ts +2 -2
  134. package/extensions/plan/runtime.ts +61 -41
  135. package/extensions/plan/sections.ts +1 -1
  136. package/extensions/plan/steps.ts +3 -3
  137. package/extensions/plan/todos.ts +1 -1
  138. package/extensions/plan/tools/cancel.ts +27 -62
  139. package/extensions/plan/tools/enter.ts +55 -81
  140. package/extensions/plan/tools/exit.ts +221 -238
  141. package/extensions/plan/tools/refusal.ts +18 -0
  142. package/extensions/plan/tools/steps.ts +75 -92
  143. package/extensions/plan/tools/write.ts +77 -120
  144. package/extensions/plan/tools.ts +188 -30
  145. package/extensions/plan/tui.ts +5 -5
  146. package/extensions/plan/types.ts +48 -5
  147. package/extensions/plan/web-blocks.ts +76 -5
  148. package/extensions/plan/web-chrome.ts +4 -0
  149. package/extensions/plan/web-client.ts +102 -2
  150. package/extensions/plan/web-collab.ts +4 -4
  151. package/extensions/plan/web-comments.ts +39 -8
  152. package/extensions/plan/web-composer.ts +14 -3
  153. package/extensions/plan/web-keys.ts +149 -25
  154. package/extensions/plan/web-lazy.ts +53 -0
  155. package/extensions/plan/web-markdown.ts +10 -68
  156. package/extensions/plan/web-outline.ts +22 -2
  157. package/extensions/plan/web-page.ts +56 -86
  158. package/extensions/plan/web-payload.ts +188 -0
  159. package/extensions/plan/web-plan.ts +23 -13
  160. package/extensions/plan/web-router.ts +18 -9
  161. package/extensions/plan/web-settle.ts +14 -7
  162. package/extensions/plan/web-share-session.ts +24 -55
  163. package/extensions/plan/web-share.ts +19 -37
  164. package/extensions/plan/web-write.ts +112 -0
  165. package/extensions/plan/web.ts +67 -131
  166. package/extensions/question/tools.ts +6 -2
  167. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  168. package/extensions/rewind/commands/commands.ts +6 -3
  169. package/extensions/rewind/state/runtime.ts +1 -1
  170. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  171. package/extensions/rewind/view/review-tui.ts +4 -4
  172. package/extensions/serve/config.ts +39 -0
  173. package/extensions/serve/index.ts +85 -0
  174. package/extensions/serve/registry.ts +260 -0
  175. package/extensions/session-bus/commands.ts +11 -6
  176. package/extensions/session-bus/join-request.ts +17 -9
  177. package/extensions/session-bus/mail-watch.ts +26 -17
  178. package/extensions/session-bus/protocol.ts +15 -3
  179. package/extensions/session-bus/resolve.ts +5 -7
  180. package/extensions/session-bus/result.ts +1 -27
  181. package/extensions/session-bus/runtime.ts +40 -12
  182. package/extensions/session-bus/store.ts +101 -19
  183. package/extensions/session-bus/tools.ts +5 -4
  184. package/extensions/session-bus/types.ts +19 -0
  185. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  186. package/extensions/subagent/agents.ts +1 -1
  187. package/extensions/subagent/commands.ts +4 -2
  188. package/extensions/subagent/format.ts +15 -15
  189. package/extensions/subagent/index.ts +1 -1
  190. package/extensions/subagent/render.ts +3 -3
  191. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  192. package/extensions/team/commands.ts +24 -5
  193. package/extensions/team/dispatch.ts +18 -4
  194. package/extensions/team/format.ts +17 -2
  195. package/extensions/team/index.ts +9 -8
  196. package/extensions/team/schema.ts +6 -5
  197. package/extensions/team/tools.ts +107 -45
  198. package/extensions/team/types.ts +3 -2
  199. package/extensions/todo/commands.ts +4 -2
  200. package/extensions/todo/format.ts +11 -0
  201. package/extensions/todo/render.ts +57 -0
  202. package/extensions/todo/runtime.ts +1 -1
  203. package/extensions/todo/tools.ts +1 -1
  204. package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
  205. package/extensions/todo/widget.ts +47 -0
  206. package/extensions/watch/commands.ts +4 -2
  207. package/extensions/watch/format.ts +36 -7
  208. package/extensions/watch/render.ts +57 -0
  209. package/extensions/watch/runtime.ts +27 -22
  210. package/extensions/watch/tools.ts +4 -4
  211. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  212. package/extensions/web-access/fetch/tool.ts +12 -4
  213. package/extensions/wechat/commands/commands.ts +18 -14
  214. package/extensions/wechat/index.ts +72 -37
  215. package/extensions/wechat/state/credentials.ts +1 -1
  216. package/extensions/wechat/state/state.ts +1 -1
  217. package/extensions/wechat/text/status.ts +112 -0
  218. package/extensions/wechat/types.ts +2 -2
  219. package/extensions/wechat/view/status-screen.ts +4 -103
  220. package/extensions/worktree/commands.ts +11 -9
  221. package/extensions/worktree/enter.ts +2 -2
  222. package/extensions/worktree/exit.ts +2 -2
  223. package/extensions/worktree/index.ts +5 -5
  224. package/extensions/worktree/screen.ts +3 -3
  225. package/extensions/worktree/status.ts +2 -2
  226. package/extensions/worktree/tools.ts +113 -116
  227. package/lib/README.md +28 -13
  228. package/lib/board/actions.ts +111 -0
  229. package/lib/board/artifact.ts +149 -0
  230. package/lib/board/card.ts +163 -52
  231. package/lib/board/config.ts +58 -0
  232. package/lib/board/contract.ts +113 -0
  233. package/lib/board/join.ts +38 -0
  234. package/lib/board/lanes.ts +182 -0
  235. package/lib/board/lead.ts +130 -0
  236. package/lib/board/limits.ts +78 -0
  237. package/lib/board/links.ts +31 -10
  238. package/lib/board/page-pointer.ts +30 -5
  239. package/lib/board/read.ts +83 -0
  240. package/lib/board/reason.ts +102 -0
  241. package/lib/board/round-text.ts +68 -6
  242. package/lib/board/round.ts +31 -15
  243. package/lib/board/rules.ts +151 -307
  244. package/lib/board/seat.ts +35 -0
  245. package/lib/board/session.ts +199 -51
  246. package/lib/board/skipped.ts +22 -0
  247. package/lib/board/steps.ts +487 -0
  248. package/lib/board/store.ts +119 -36
  249. package/lib/board/tone.ts +92 -0
  250. package/lib/board/view.ts +81 -63
  251. package/lib/bus-request.ts +83 -0
  252. package/{extensions/watch → lib}/child.ts +40 -21
  253. package/lib/env.ts +65 -7
  254. package/lib/format.ts +13 -0
  255. package/lib/http/index.ts +17 -11
  256. package/lib/http/mount.ts +142 -0
  257. package/lib/http/port.ts +30 -0
  258. package/lib/http/serve.ts +170 -46
  259. package/lib/http/server.ts +3 -3
  260. package/lib/lazy.ts +45 -0
  261. package/lib/plan-entry.ts +46 -21
  262. package/lib/plans/paths.ts +82 -0
  263. package/lib/policy.ts +9 -6
  264. package/lib/report.ts +3 -2
  265. package/lib/requirements.ts +10 -10
  266. package/lib/session-label.ts +129 -7
  267. package/lib/session-roster.ts +43 -1
  268. package/lib/session-tool.ts +39 -22
  269. package/lib/stray-fields.ts +7 -2
  270. package/lib/tool-names.ts +5 -10
  271. package/lib/tui/README.md +29 -11
  272. package/lib/tui/chrome.ts +16 -15
  273. package/lib/tui/cursor.ts +18 -3
  274. package/lib/tui/glyphs.ts +11 -11
  275. package/lib/tui/render-cache.ts +29 -0
  276. package/lib/tui/tokens.ts +33 -2
  277. package/lib/tui/tool.ts +33 -4
  278. package/lib/web/README.md +21 -0
  279. package/lib/web/client-runtime.ts +27 -1
  280. package/lib/web/components.ts +5 -1
  281. package/lib/web/index.ts +1 -0
  282. package/lib/web/markdown.ts +83 -0
  283. package/lib/web/markup.ts +20 -2
  284. package/lib/web/runtime.ts +9 -10
  285. package/package.json +8 -2
  286. package/skills/init/SKILL.md +5 -5
  287. package/skills/next/SKILL.md +3 -5
  288. package/extensions/board/config.ts +0 -36
  289. package/extensions/board/seat.ts +0 -28
  290. package/lib/tui/index.ts +0 -34
  291. package/skills/project-docs/SKILL.md +0 -133
  292. package/skills/project-docs/references/audit.md +0 -131
  293. package/skills/project-docs/references/changelog.md +0 -105
  294. package/skills/project-docs/references/discovery.md +0 -127
  295. package/skills/project-docs/references/readme.md +0 -80
  296. package/skills/project-docs/references/release.md +0 -84
  297. package/skills/project-docs/references/roadmap.md +0 -93
  298. package/skills/project-docs/scripts/state.ts +0 -316
@@ -17,6 +17,7 @@ import { createHash } from "node:crypto";
17
17
  import { stripControlChars } from "../../lib/format.ts";
18
18
  import { displayFeedback, stripFeedback } from "./feedback.ts";
19
19
  import { extractSections, sectionKeys, type PlanSection } from "./sections.ts";
20
+ import { planBlocks } from "./blocks.ts";
20
21
 
21
22
  /** Spaces a tab expands to; the terminal's own tab stops are not width-modelled. */
22
23
  const TAB_WIDTH = 4;
@@ -27,6 +28,14 @@ export interface SectionDigest {
27
28
  title: string;
28
29
  /** SHA-256 of the section's normalized text, heading included. */
29
30
  digest: string;
31
+ /**
32
+ * Short digests of this section's content blocks, in order.
33
+ *
34
+ * Absent in a baseline a build before block marks wrote, which is why every reader
35
+ * treats it as optional: a section with no block digests marks nothing, and the page
36
+ * says it cannot compare at that depth rather than reading as "nothing changed".
37
+ */
38
+ blocks?: string[];
30
39
  }
31
40
 
32
41
  /**
@@ -60,6 +69,17 @@ function digestOf(text: string): string {
60
69
  return createHash("sha256").update(text.trim()).digest("hex");
61
70
  }
62
71
 
72
+ /**
73
+ * A short, stable digest of one block's lines.
74
+ *
75
+ * Short because it is persisted once per content block in a session entry: eight hex
76
+ * characters tell two blocks apart for the length of one review, which is all a change
77
+ * mark needs, and keep a long plan's baseline small enough to sit in the branch.
78
+ */
79
+ export function blockDigest(lines: readonly string[]): string {
80
+ return digestOf(lines.join("\n")).slice(0, 8);
81
+ }
82
+
63
83
  /**
64
84
  * A short, stable id for one plan text. The review page keys this tab's unsaved
65
85
  * draft on it, so a draft is restored for the plan it was written against and
@@ -74,9 +94,103 @@ export function sectionDigests(content: string): SectionDigest[] {
74
94
  return extractSections(reviewText(content)).map((section) => ({
75
95
  title: section.title,
76
96
  digest: digestOf(section.text),
97
+ // Blank blocks are separators, not content: a plan that gained a blank line has not
98
+ // changed, and a digest per separator would say it had.
99
+ blocks: planBlocks(section.text)
100
+ .filter((block) => block.kind !== "blank")
101
+ .map((block) => blockDigest(block.lines)),
77
102
  }));
78
103
  }
79
104
 
105
+ /** Where the blocks changed, in the document the page is about to render. */
106
+ export interface BlockChangeMarks {
107
+ /** Absolute 0-based start lines of the blocks that are new or edited. */
108
+ starts: number[];
109
+ /** Content blocks each section no longer holds, keyed by the section's heading line. */
110
+ removed: Record<number, number>;
111
+ /**
112
+ * Whether the baseline carried block digests at all.
113
+ *
114
+ * False for a baseline a build before block marks wrote: section marks still mean
115
+ * something, block marks cannot, and the page says which instead of drawing nothing.
116
+ */
117
+ compared: boolean;
118
+ }
119
+
120
+ /**
121
+ * Where the blocks changed since the baseline, as the rendered document numbers them.
122
+ *
123
+ * The pages marks what it draws, so every line here is a line of `current`: a section's
124
+ * blocks are numbered from its own heading, which is why the section's line is added
125
+ * back. Sections are paired the way {@link changedSections} pairs them — by occurrence,
126
+ * so a second `## Notes` is never compared with the first — and a section the baseline
127
+ * never held is all new: every block in it marks, because adding or renaming a heading is
128
+ * a refinement rather than a reason to refuse the comparison. Only a baseline section
129
+ * written before block digests is beyond comparing at that depth, and it alone clears
130
+ * `compared`.
131
+ */
132
+ export function blockChangeMarks(baseline: readonly SectionDigest[] | undefined, current: string): BlockChangeMarks {
133
+ const sections = extractSections(reviewText(current));
134
+ if (baseline === undefined) return { starts: [], removed: {}, compared: false };
135
+ const before = new Map<string, SectionDigest>();
136
+ sectionKeys(baseline).forEach((key, at) => {
137
+ const section = baseline[at];
138
+ if (section !== undefined) before.set(key, section);
139
+ });
140
+ const starts: number[] = [];
141
+ const removed: Record<number, number> = {};
142
+ let compared = true;
143
+ sectionKeys(sections).forEach((key, at) => {
144
+ const section = sections[at];
145
+ if (section === undefined) return;
146
+ const prior = before.get(key);
147
+ if (prior !== undefined && prior.blocks === undefined) {
148
+ compared = false;
149
+ return;
150
+ }
151
+ const mine = planBlocks(section.text).filter((block) => block.kind !== "blank");
152
+ // An unheld section pairs with no blocks, so every block in it is new.
153
+ const priorBlocks = new Set(prior?.blocks ?? []);
154
+ for (const block of mine) {
155
+ if (!priorBlocks.has(blockDigest(block.lines))) starts.push(section.line + block.start);
156
+ }
157
+ const shrunk = Math.max(0, (prior?.blocks?.length ?? 0) - mine.length);
158
+ if (shrunk > 0) removed[section.line] = shrunk;
159
+ });
160
+ return { starts, removed, compared };
161
+ }
162
+
163
+ /** What one section's blocks look like against the baseline's. */
164
+ export interface BlockMarks {
165
+ /** Indexes, among the section's content blocks, of the blocks that are new or edited. */
166
+ changed: number[];
167
+ /** How many content blocks the section no longer holds: the part it shrank by. */
168
+ removed: number;
169
+ }
170
+
171
+ /**
172
+ * Mark the blocks a section gained or edited since the baseline.
173
+ *
174
+ * Membership rather than position: a block is changed when its digest is not among the
175
+ * baseline's, so an inserted step marks itself and leaves every block after it alone —
176
+ * the index-by-index pairing a diff would use would mark the whole rest of the section.
177
+ * A block that left cannot be drawn in place, so the section reports how much shorter it
178
+ * has become instead — the shrinkage, not the set difference, so an edited block reads
179
+ * as one change rather than as one removal beside one addition.
180
+ *
181
+ * An absent or blockless baseline marks nothing: a build before block marks wrote no
182
+ * block digests, and claiming every block changed would be a worse lie than saying the
183
+ * comparison cannot be made at that depth.
184
+ */
185
+ export function changedBlocks(prior: readonly string[] | undefined, current: readonly string[]): BlockMarks {
186
+ if (prior === undefined) return { changed: [], removed: 0 };
187
+ const before = new Set(prior);
188
+ return {
189
+ changed: current.flatMap((digest, index) => (before.has(digest) ? [] : [index])),
190
+ removed: Math.max(0, prior.length - current.length),
191
+ };
192
+ }
193
+
80
194
  /** Whether two baselines describe the same content, section for section. */
81
195
  export function sameSections(a: readonly SectionDigest[], b: readonly SectionDigest[]): boolean {
82
196
  if (a.length !== b.length) return false;
@@ -33,13 +33,12 @@ import {
33
33
  unlinkSync,
34
34
  writeFileSync,
35
35
  } from "node:fs";
36
- import { homedir } from "node:os";
37
- import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
36
+ import { join } from "node:path";
38
37
  import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
39
38
  import { resolveEffectiveCwd } from "../../lib/env.ts";
40
39
  import { keepRevision, writeAtomic } from "./plan-files.ts";
41
40
  import { sanitizeLine, shortenPath } from "../../lib/format.ts";
42
- import { isInside, isInsideReal } from "../../lib/path.ts";
41
+ import { isContainedPlan, isPlanFileName, plansDirFor, resolvePlanArg } from "../../lib/plans/paths.ts";
43
42
  import { repoKeyFor } from "../../lib/repo-key.ts";
44
43
  import { carryFeedback, formatFeedback, normalizeAuthor, type PlanAnnotation } from "./feedback.ts";
45
44
  import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } from "./steps.ts";
@@ -48,14 +47,6 @@ import { extractPlanSteps, matchesStepText, setStepStatuses, type StepUpdate } f
48
47
  export const MAX_PLAN_BYTES = 256 * 1024;
49
48
  /** Longest slug derived from a plan title. */
50
49
  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
- }
58
-
59
50
  /** A plan read from disk. */
60
51
  export interface StoredPlan {
61
52
  /** Absolute path. */
@@ -160,46 +151,6 @@ export function stamp(date: Date): string {
160
151
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
161
152
  }
162
153
 
163
- /** A plan file name: markdown and not a dotfile, so the self-ignore file is never a target. */
164
- function isPlanFileName(path: string): boolean {
165
- const name = basename(path);
166
- return name.toLowerCase().endsWith(".md") && !name.startsWith(".");
167
- }
168
-
169
- /**
170
- * Whether `path` may be read, written, or deleted as a plan. Beyond lexical
171
- * containment (shared with the other path guards through `lib/path.ts`), this
172
- * rejects dotfiles (including the directory's own `.gitignore`) and resolves
173
- * symlinks, so a link placed inside the plans directory cannot redirect a read
174
- * or write outside it.
175
- */
176
- function isContainedPlan(dir: string, path: string): boolean {
177
- if (!isInside(dir, path) || !isPlanFileName(path)) return false;
178
- try {
179
- // A symlink itself must never be the target; a missing file is a new plan.
180
- if (lstatSync(path).isSymbolicLink()) return false;
181
- } catch {
182
- // Missing (ENOENT) is expected; any other error falls through to the
183
- // real-path check, which rejects the path if it cannot be resolved.
184
- }
185
- return isInsideReal(dir, dirname(path));
186
- }
187
-
188
- /**
189
- * Resolve a `plan_path` argument against the store.
190
- *
191
- * A plan lives outside the project, so a path relative to the working directory
192
- * can no longer name one. Absolute paths and the `~/…` form the tools display are
193
- * accepted, and a bare name resolves inside the store — which is what a reader of
194
- * a tool result has in hand.
195
- */
196
- function resolvePlanArg(dir: string, planPath: string): string {
197
- const text = planPath.trim();
198
- if (text === "~") return homedir();
199
- if (text.startsWith("~/") || text.startsWith(`~${sep}`)) return resolve(homedir(), text.slice(2));
200
- return isAbsolute(text) ? resolve(text) : resolve(dir, text);
201
- }
202
-
203
154
  function uniquePath(dir: string, base: string): string {
204
155
  const taken = (candidate: string): boolean => {
205
156
  try {
@@ -300,7 +251,7 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
300
251
  const cached = dirCache.get(base);
301
252
  if (cached) return cached;
302
253
  const repo = await repoKeyFor(pi, base);
303
- const dir = plansDir(getAgentDir(), repo.fromGit ? repo.key : undefined);
254
+ const dir = plansDirFor(getAgentDir(), repo);
304
255
  dirCache.set(base, { dir, root: repo.fromGit ? repo.root : undefined });
305
256
  if (repo.fromGit) {
306
257
  // The keyed directory is the store; a store an older version left in the
@@ -387,7 +338,7 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
387
338
 
388
339
  const setSteps = async (cwd: string, planPath: string, updates: StepUpdate[]): Promise<StoredPlan> => {
389
340
  const plan = await read(cwd, planPath);
390
- if (!plan) throw new Error("No plan file there; pass a plan_path written by write_plan.");
341
+ if (!plan) throw new Error("No plan file there; pass a plan_path written by `plan` action `write`.");
391
342
  const steps = extractPlanSteps(plan.content);
392
343
  const total = steps.length;
393
344
  const outOfRange = updates.find(
@@ -433,7 +384,7 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
433
384
  options: { author?: string } = {},
434
385
  ): Promise<StoredPlan> => {
435
386
  const plan = await read(cwd, planPath);
436
- if (!plan) throw new Error("No plan file there; pass a plan_path written by write_plan.");
387
+ if (!plan) throw new Error("No plan file there; pass a plan_path written by `plan` action `write`.");
437
388
  const author = normalizeAuthor(options.author);
438
389
  // An untagged entry is the writer's own; one tagged with a different name is
439
390
  // another writer's comment, so it is dropped rather than re-signed.
@@ -25,8 +25,6 @@ import {
25
25
  WEB_SEARCH_TOOL,
26
26
  } from "../../lib/tool-names.ts";
27
27
 
28
- import { ENTER_TOOL } from "./runtime.ts";
29
-
30
28
  /**
31
29
  * Tools that stay available while planning: structured readers (documents
32
30
  * included), the trackers, the git-only `bash`, and the known web/question
@@ -53,23 +51,23 @@ export const PLAN_SAFE_TOOLS = [
53
51
  */
54
52
  export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL, BOARD_TOOL];
55
53
  /**
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".
54
+ * Tools always blocked while planning, even when annotated read-only. The `plan`
55
+ * tool is not one of them: plan mode keeps it active in both modes, because its
56
+ * actions are the mode's own verbs and each one refuses outside its mode.
59
57
  */
60
- const ALWAYS_BLOCKED_TOOLS = ["write", "edit", ENTER_TOOL];
58
+ const ALWAYS_BLOCKED_TOOLS = ["write", "edit"];
61
59
 
62
60
  /** Shown when a tool call is refused while planning. */
63
- export const BLOCKED_GUIDANCE = "Call exit_plan_mode and get approval before using it.";
61
+ export const BLOCKED_GUIDANCE = "Call `plan` action `exit` and get approval before using it.";
64
62
 
65
63
  /** Appended to a refused `bash` call; names the readers and the exit hatch. */
66
64
  export const BASH_BLOCKED_GUIDANCE =
67
- "Use the read, grep, find, and ls tools for files, or call exit_plan_mode and get approval before using other shell commands.";
65
+ "Use the read, grep, find, and ls tools for files, or call `plan` action `exit` and get approval before using other shell commands.";
68
66
 
69
67
  /**
70
68
  * Build plan mode's read-only policy: known readers and the plan/goal trackers
71
- * are allowed, write/edit/shell/`enter_plan_mode` are always blocked, and every
72
- * other tool must carry the MCP `readOnlyHint` to pass.
69
+ * are allowed, write/edit and the shells are always blocked, and every other tool
70
+ * must carry the MCP `readOnlyHint` to pass.
73
71
  */
74
72
  export function createPlanPolicy(pi: ExtensionAPI): ReadOnlyPolicy {
75
73
  return createReadOnlyPolicy({
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The plan review gate, loaded when a review actually asks a human.
3
+ *
4
+ * `review.ts` owns the loop, the TUI screen (`tui.ts`), the compact remote prompt,
5
+ * and the baseline rule — about 1,000 lines that registration never needs:
6
+ * `/plan review` and the `plan` tool's `exit` action are the only two doors in. Both import this
7
+ * module instead, so the loop loads once, on the first review of the session.
8
+ *
9
+ * The types stay here rather than in `review.ts` because a type import is erased:
10
+ * the `plan` tool declares its `exit` details from them at registration time.
11
+ */
12
+
13
+ import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
14
+ import type { SectionDigest } from "./plan-text.ts";
15
+ import type { PlanStore, StoredPlan } from "./plans.ts";
16
+ import type { PlanAnnotation } from "./feedback.ts";
17
+ import type { PlanReviewResult, PlanReviewSession, PlanWebReview } from "./web.ts";
18
+
19
+ /** A choice from the review screen, with the refinement the screen carried, if any. */
20
+ export interface PlanReviewChoice {
21
+ action: "approve" | "refine" | "keep" | "cancel";
22
+ refinement?: string;
23
+ }
24
+
25
+ /** What a review decided, and the plan state it decided on. */
26
+ export type ReviewDecision =
27
+ | { action: "approve"; plan: StoredPlan; comments: PlanAnnotation[] }
28
+ | {
29
+ action: "refine";
30
+ plan: StoredPlan;
31
+ /** The refinement request: the saved comments, or what the user typed. */
32
+ refinement: string;
33
+ comments: PlanAnnotation[];
34
+ /** True when the request came from the browser, which is reported differently. */
35
+ fromBrowser: boolean;
36
+ }
37
+ | { action: "keep"; plan: StoredPlan }
38
+ /** Leave plan mode without approving; the plan file stays for a later review. */
39
+ | { action: "cancel"; plan: StoredPlan };
40
+
41
+ /** What the previous review showed, so this one can mark what changed. */
42
+ export interface ReviewBaselines {
43
+ /** The sections the last review of `path` showed, when this branch has one. */
44
+ reviewed(path: string): SectionDigest[] | undefined;
45
+ /** Remember what a review just showed, as a session entry on this branch. */
46
+ markReviewed(path: string, content: string): void;
47
+ }
48
+
49
+ /** Everything one review needs: whom to ask, where to ask, and what to compare against. */
50
+ export interface ReviewPlanOptions {
51
+ ctx: ExtensionToolContext;
52
+ web: PlanWebReview;
53
+ /** Re-read the plan after a browser review that may have changed it. */
54
+ plans: PlanStore;
55
+ baselines: ReviewBaselines;
56
+ plan: StoredPlan;
57
+ /** Aborting the turn closes the browser server. */
58
+ signal?: AbortSignal;
59
+ }
60
+
61
+ /** The review module, as the loader returns it. */
62
+ export type ReviewModule = typeof import("./review.ts");
63
+
64
+ /** How a review reaches its implementation; production uses the dynamic import. */
65
+ export type ReviewLoader = () => Promise<ReviewModule>;
66
+
67
+ /** Ask the human about `plan` and return one decision, loading the gate on first use. */
68
+ export async function reviewPlan(options: ReviewPlanOptions, load?: ReviewLoader): Promise<ReviewDecision> {
69
+ const review = await (load ?? (() => import("./review.ts")))();
70
+ return review.reviewPlan(options);
71
+ }
72
+
73
+ /** The browser-only review `/plan review` opens, loaded on first use. */
74
+ export async function runBrowserReview(
75
+ web: PlanWebReview,
76
+ cwd: string,
77
+ plan: StoredPlan,
78
+ options: Parameters<ReviewModule["runBrowserReview"]>[3],
79
+ load?: ReviewLoader,
80
+ ): Promise<PlanReviewResult | undefined> {
81
+ const review = await (load ?? (() => import("./review.ts")))();
82
+ return review.runBrowserReview(web, cwd, plan, options);
83
+ }
84
+
85
+ export type { PlanReviewSession };
@@ -4,7 +4,7 @@
4
4
  * A review has three presentations — the scrollable TUI screen, a dialog-only
5
5
  * editor, and a remote turn's compact editor prompt — and one optional browser
6
6
  * server. This module owns the loop that switches between
7
- * them, so `exit_plan_mode` and `/plan review` share one lifecycle and only map
7
+ * them, so the `plan` tool's `exit` action and `/plan review` share one lifecycle and only map
8
8
  * the returned {@link ReviewDecision} to their own result.
9
9
  *
10
10
  * It also owns the review baseline: a review marks the content the human was
@@ -118,7 +118,7 @@ async function askPlanReview(
118
118
 
119
119
  /** How a browser review may decide, and what to report when it opens. */
120
120
  export interface BrowserReviewOptions {
121
- /** Whether the browser may approve the plan (only the `exit_plan_mode` flow). */
121
+ /** Whether the browser may approve the plan (only the `plan` tool's `exit` flow). */
122
122
  canApprove: boolean;
123
123
  /** The section digests the previous review showed, for the changed markers. */
124
124
  baseline?: SectionDigest[];
@@ -2,18 +2,18 @@
2
2
  * Session-scoped state and tool gating for plan mode.
3
3
  *
4
4
  * Enabling plan mode removes every tool the policy does not consider read-only
5
- * from the active set and activates the plan-mode control tools: `exit_plan_mode`
6
- * (present the plan for approval) and `write_plan` (save the plan artifact under
7
- * the plans directory). Disabling restores exactly what it hid. The enabled flag is
8
- * persisted as a custom session entry so `/resume` and `/tree` follow the
9
- * branch, and it is also mirrored into the footer status. The review baseline
10
- * travels the same way, as one digest per heading section, so the changed-section
11
- * markers survive a resume too.
5
+ * from the active set, and keeps the one `plan` tool active in both modes: its
6
+ * actions are what ask to enter, save the artifact under the plans directory,
7
+ * present it, tick it, and leave. Disabling restores exactly what it hid. The
8
+ * enabled flag is persisted as a custom session entry so `/resume` and `/tree`
9
+ * follow the branch, and it is also mirrored into the footer status. The review
10
+ * baseline travels the same way, as one digest per heading section, so the
11
+ * changed-section markers survive a resume too.
12
12
  *
13
- * `write_plan` is the one sanctioned write while planning. The read-only guard
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
- * extension cannot borrow the exemption.
13
+ * The `plan` tool's `write` action is the one sanctioned write while planning. The
14
+ * read-only guard in `index.ts` permits that tool only when the registered one is
15
+ * this extension's (matched by source path), so a same-named tool from another
16
+ * extension cannot borrow the exemption, and each action refuses outside its mode.
17
17
  */
18
18
 
19
19
  import { basename, resolve } from "node:path";
@@ -23,23 +23,25 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
23
23
  import { sanitizeLine } from "../../lib/format.ts";
24
24
  import { PLAN_MODE_ENTRY_TYPE } from "../../lib/plan-state.ts";
25
25
  import type { ReadOnlyPolicy } from "../../lib/policy.ts";
26
- import { PLAN_ENTER_TOOL } from "../../lib/tool-names.ts";
26
+ import { PLAN_TOOL as PLAN_TOOL_NAME } from "../../lib/tool-names.ts";
27
27
  import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
28
- import { paint } from "../../lib/tui/tokens.ts";
29
28
  import { createPlanStore, type PlanStore } from "./plans.ts";
30
29
  import { sameSections, sectionDigests, type SectionDigest } from "./plan-text.ts";
31
30
  import type { PlanModeEntry, ReviewBaselineEntry, SeededStepsEntry } from "./types.ts";
32
31
 
33
- /** Tool the model calls to leave plan mode after approval. */
34
- export const EXIT_TOOL = "exit_plan_mode";
35
- /** Tool the model calls to leave plan mode without approving. */
36
- export const CANCEL_TOOL = "cancel_plan_mode";
37
- /** Tool the model calls to ask to enter plan mode. */
38
- export const ENTER_TOOL = PLAN_ENTER_TOOL;
39
- /** Tool the model calls to save the plan artifact. */
40
- export const WRITE_PLAN_TOOL = "write_plan";
41
- /** Tool the model calls to keep an approved plan's steps current. */
42
- export const SET_PLAN_STEPS_TOOL = "set_plan_steps";
32
+ /** The one model-facing tool: every planning verb is one of its actions. */
33
+ export const PLAN_TOOL = PLAN_TOOL_NAME;
34
+
35
+ /**
36
+ * The verbs of the one `plan` tool, in the order the schema offers them.
37
+ *
38
+ * One action tool rather than five tools, so every verb shares a name and a
39
+ * registration; the order is the one the description walks them in.
40
+ */
41
+ export const PLAN_ACTIONS = ["enter", "write", "exit", "steps", "cancel"] as const;
42
+
43
+ /** One verb of the `plan` tool. */
44
+ export type PlanAction = (typeof PLAN_ACTIONS)[number];
43
45
  /** Custom-entry type used for plan-mode state. */
44
46
  export const STATE_TYPE = PLAN_MODE_ENTRY_TYPE;
45
47
  /** Custom-entry type used for a plan's review baseline. */
@@ -50,16 +52,13 @@ export const SEEDED_TYPE = "plan-seeded";
50
52
  /** Longest plan-file basename shown in the footer chip. */
51
53
  const STATUS_NAME_LENGTH = 28;
52
54
 
53
- /** Tools plan mode activates itself. */
54
- const CONTROL_TOOLS = [EXIT_TOOL, CANCEL_TOOL, WRITE_PLAN_TOOL];
55
-
56
55
  /**
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.
56
+ * The one tool plan mode keeps in the active set in both modes. Every other tool
57
+ * follows the policy, which is about writing files and cannot answer for a tool
58
+ * whose actions are the mode's own verbs: hiding it while planning would hide the
59
+ * way to write the plan and the way out.
61
60
  */
62
- const OWNED_TOOLS = [...CONTROL_TOOLS, ENTER_TOOL];
61
+ const MODE_TOOLS = [PLAN_TOOL];
63
62
 
64
63
  export interface PlanRuntime {
65
64
  /** Whether plan mode is currently on. */
@@ -69,9 +68,9 @@ export interface PlanRuntime {
69
68
  toggle(ctx: ExtensionContext): void;
70
69
  /** Restore state from the branch and the `--plan` flag. */
71
70
  restore(ctx: ExtensionContext): void;
72
- /** Plan-file storage for `write_plan` and `exit_plan_mode`. */
71
+ /** Plan-file storage for the `write` and `exit` actions. */
73
72
  plans: PlanStore;
74
- /** Whether `toolName` is one of plan's own tools (not a same-named impostor) the read-only guard exempts. */
73
+ /** Whether `toolName` is the `plan` tool (not a same-named impostor) the read-only guard exempts. */
75
74
  isOwnTool(toolName: string): boolean;
76
75
  /** Path of the most recently written plan, for the footer chip. */
77
76
  lastPlanPath(): string | undefined;
@@ -100,9 +99,19 @@ function readSections(value: unknown): SectionDigest[] | undefined {
100
99
  if (!Array.isArray(value)) return undefined;
101
100
  const sections: SectionDigest[] = [];
102
101
  for (const raw of value) {
103
- const section = raw as { title?: unknown; digest?: unknown } | null | undefined;
102
+ const section = raw as { title?: unknown; digest?: unknown; blocks?: unknown } | null | undefined;
104
103
  if (typeof section?.title !== "string" || typeof section.digest !== "string") return undefined;
105
- sections.push({ title: section.title, digest: section.digest });
104
+ // Branch data is untrusted, so the block digests are kept only when they are a list
105
+ // of strings: a baseline written before block marks simply has none, and one that
106
+ // holds something else is read at section depth rather than repaired.
107
+ const blocks = Array.isArray(section.blocks)
108
+ ? section.blocks.filter((entry): entry is string => typeof entry === "string")
109
+ : undefined;
110
+ sections.push(
111
+ blocks === undefined
112
+ ? { title: section.title, digest: section.digest }
113
+ : { title: section.title, digest: section.digest, blocks },
114
+ );
106
115
  }
107
116
  return sections;
108
117
  }
@@ -185,6 +194,9 @@ export function createPlanRuntime(
185
194
  // Tools plan mode deactivated, so disabling restores exactly those and does
186
195
  // not re-activate a tool another extension intentionally hid.
187
196
  let removedForPlan: string[] = [];
197
+ // Mode tools this enable had to activate, so disabling puts the user's own
198
+ // active set back instead of widening it.
199
+ let addedForPlan: string[] = [];
188
200
 
189
201
  const plans = createPlanStore(pi);
190
202
 
@@ -194,7 +206,7 @@ export function createPlanRuntime(
194
206
  * is rejected.
195
207
  */
196
208
  const isOwnTool = (toolName: string): boolean => {
197
- if (!OWNED_TOOLS.includes(toolName)) return false;
209
+ if (toolName !== PLAN_TOOL) return false;
198
210
  const info = pi.getAllTools().find((tool) => tool.name === toolName);
199
211
  const source = info?.sourceInfo?.path;
200
212
  return source === undefined || options.entryPath === undefined || samePath(source, options.entryPath);
@@ -202,7 +214,7 @@ export function createPlanRuntime(
202
214
 
203
215
  /** Tools plan mode would hide from the current active set. */
204
216
  const captureHidden = (): string[] => [
205
- ...new Set(pi.getActiveTools().filter((name) => !policy.isAllowed(name) && !CONTROL_TOOLS.includes(name))),
217
+ ...new Set(pi.getActiveTools().filter((name) => !policy.isAllowed(name) && !MODE_TOOLS.includes(name))),
206
218
  ];
207
219
 
208
220
  /**
@@ -214,12 +226,18 @@ export function createPlanRuntime(
214
226
  const active = pi.getActiveTools();
215
227
  if (enabled) {
216
228
  // Keep only tools the policy considers safe to expose while read-only
217
- // (structured readers and the plan/goal trackers) plus the control tools.
218
- pi.setActiveTools([...new Set([...active.filter((name) => policy.isAllowed(name)), ...CONTROL_TOOLS])]);
229
+ // (structured readers and the plan/goal trackers), plus the `plan` tool
230
+ // itself, whose actions are the mode's own vocabulary.
231
+ addedForPlan = MODE_TOOLS.filter((name) => !active.includes(name));
232
+ pi.setActiveTools([...new Set([...active.filter((name) => policy.isAllowed(name)), ...MODE_TOOLS])]);
219
233
  return;
220
234
  }
235
+ // A mode tool this enable had to activate goes back to how the user left it,
236
+ // rather than staying active because the mode needed it for a while.
237
+ const kept = active.filter((name) => !addedForPlan.includes(name));
238
+ addedForPlan = [];
221
239
  const restored = addHidden ? removedForPlan : [];
222
- pi.setActiveTools([...new Set([...active.filter((name) => !CONTROL_TOOLS.includes(name)), ...restored])]);
240
+ pi.setActiveTools([...new Set([...kept, ...restored])]);
223
241
  removedForPlan = [];
224
242
  };
225
243
 
@@ -227,7 +245,9 @@ export function createPlanRuntime(
227
245
  lastPlanPath ? `${GLYPHS.plan} plan · ${statusName(lastPlanPath)}` : `${GLYPHS.plan} plan`;
228
246
 
229
247
  const setStatus = (ctx: ExtensionContext): void => {
230
- ctx.ui.setStatus(STATUS_KEYS.planMode, enabled ? paint(ctx.ui.theme, "warn", statusText()) : undefined);
248
+ // Published unstyled: the chip's text is what a footer without bytefly shows, and
249
+ // bytefly's mode segment paints the mode by role instead of reading this pill.
250
+ ctx.ui.setStatus(STATUS_KEYS.planMode, enabled ? statusText() : undefined);
231
251
  };
232
252
 
233
253
  const setEnabled = (next: boolean, ctx: ExtensionContext | undefined, persist: boolean): void => {
@@ -2,7 +2,7 @@
2
2
  * Plan-document sections (pure).
3
3
  *
4
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
5
+ * section, the `plan` tool's `steps` action looks up `## Steps`, a carried comment finds the
6
6
  * block it belongs to, and the browser outline lists the same headings. A second
7
7
  * scanner would disagree about a heading inside a fence, so every reader goes
8
8
  * through this module.
@@ -31,7 +31,7 @@ export const MAX_STEP_LENGTH = 200;
31
31
  * section that carries the end state — the same surface before and after when the
32
32
  * change alters one that already exists, one frame when it is new — in a fence
33
33
  * that stays untagged unless every line is code, so the reviewer sees the result
34
- * before the steps that build it. Guidance only: `write_plan` never rejects or
34
+ * before the steps that build it. Guidance only: the `write` action never rejects or
35
35
  * rewrites a plan that omits a section, and a change with no visible surface drops
36
36
  * `Visual`.
37
37
  */
@@ -268,7 +268,7 @@ const LIST_PREFIX = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/;
268
268
  * list markers and inline emphasis, and the lines are joined with `; `, prefixed
269
269
  * with `Verify:` and elided like a long step. The item is deliberately *not* a
270
270
  * plan step: it is not in the `Steps` section, so it must not count against the
271
- * step cap or appear in `set_plan_steps` numbering.
271
+ * step cap or appear in the `steps` action's numbering.
272
272
  *
273
273
  * A plan with no `Steps` heading feeds its shallowest list as steps (the older
274
274
  * rule), so that list already covers the verification bullets; appending a
@@ -297,7 +297,7 @@ export function extractPlanSteps(plan: string): PlanStep[] {
297
297
  return topLevelItems(plan).map((item) => ({ content: elide(item.step.content), status: item.step.status }));
298
298
  }
299
299
 
300
- /** The full cost of a plan's step list, for the warning `write_plan` reports. */
300
+ /** The full cost of a plan's step list, for the warning the `write` action reports. */
301
301
  export interface PlanStepSummary {
302
302
  /** Top-level items in the step list, before the {@link MAX_STEPS} cap. */
303
303
  total: number;
@@ -44,7 +44,7 @@ export interface TodoMerge {
44
44
  /** Fallback list cap when the registered todo tool does not advertise one. */
45
45
  export const DEFAULT_TODO_MAX = 50;
46
46
 
47
- /** What the tick-time mirror did for one `set_plan_steps` batch. */
47
+ /** What the tick-time mirror did for one `steps` batch. */
48
48
  export interface TodoMirror {
49
49
  /** The list after the matching items were brought in line with the plan. */
50
50
  todos: ExistingTodo[];