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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/README.md +52 -44
  2. package/README.zh-CN.md +46 -41
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +344 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +446 -404
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +554 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +61 -50
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +390 -317
  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 +285 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +226 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +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} +258 -162
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
  25. package/extensions/board/commands.ts +87 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +72 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +312 -0
  32. package/extensions/board/result.ts +99 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +75 -0
  35. package/extensions/board/seat.ts +29 -0
  36. package/extensions/board/snapshot.ts +41 -0
  37. package/extensions/board/tools.ts +279 -0
  38. package/extensions/board/transport/router.ts +172 -0
  39. package/extensions/board/transport/server.ts +149 -0
  40. package/extensions/board/types.ts +54 -0
  41. package/extensions/board/view/actions.ts +306 -0
  42. package/extensions/board/view/client.ts +41 -0
  43. package/extensions/board/view/keys.ts +121 -0
  44. package/extensions/board/view/live.ts +397 -0
  45. package/extensions/board/view/page.ts +158 -0
  46. package/extensions/board/view/render.ts +496 -0
  47. package/extensions/board/view/styles.ts +211 -0
  48. package/extensions/board/view/team.ts +305 -0
  49. package/extensions/board/writes.ts +99 -0
  50. package/extensions/bytefly/panel/commands.ts +40 -7
  51. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  52. package/extensions/bytefly/panel/settings.ts +3 -20
  53. package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
  54. package/extensions/bytefly/picker/discovery.ts +0 -2
  55. package/extensions/bytefly/picker/types.ts +0 -2
  56. package/extensions/bytefly/update/check.ts +39 -0
  57. package/extensions/bytefly/update/format.ts +15 -0
  58. package/extensions/bytefly/update/register.ts +73 -34
  59. package/extensions/bytefly/update/state.ts +6 -3
  60. package/extensions/context/tui.ts +1 -1
  61. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  62. package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
  63. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  64. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  65. package/extensions/file-browse/index.ts +4 -4
  66. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  67. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -15
  68. package/extensions/file-browse/transport/server.ts +110 -0
  69. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  70. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  71. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  72. package/extensions/file-browse/{html.ts → view/html.ts} +14 -12
  73. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  74. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
  75. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
  76. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  77. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  78. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  79. package/extensions/file-read/schema.ts +2 -1
  80. package/extensions/memory/store.ts +12 -5
  81. package/extensions/plan/blocks.ts +23 -2
  82. package/extensions/plan/commands.ts +21 -16
  83. package/extensions/plan/feedback.ts +147 -38
  84. package/extensions/plan/index.ts +6 -4
  85. package/extensions/plan/plan-index.ts +43 -9
  86. package/extensions/plan/plan-text.ts +33 -23
  87. package/extensions/plan/plans.ts +8 -11
  88. package/extensions/plan/policy.ts +20 -12
  89. package/extensions/plan/review.ts +3 -27
  90. package/extensions/plan/runtime.ts +21 -9
  91. package/extensions/plan/sections.ts +112 -0
  92. package/extensions/plan/steps.ts +3 -22
  93. package/extensions/plan/tools/exit.ts +11 -95
  94. package/extensions/plan/tui.ts +5 -34
  95. package/extensions/plan/types.ts +0 -8
  96. package/extensions/plan/web-client.ts +1 -1
  97. package/extensions/plan/web-page.ts +0 -7
  98. package/extensions/plan/web-router.ts +3 -57
  99. package/extensions/plan/web-share-session.ts +24 -30
  100. package/extensions/plan/web-streams.ts +14 -14
  101. package/extensions/plan/web-tokens.ts +2 -2
  102. package/extensions/plan/web.ts +66 -120
  103. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  104. package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
  105. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  106. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  107. package/extensions/rewind/index.ts +3 -3
  108. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  109. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  110. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  111. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  112. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  113. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  114. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  115. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  116. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  117. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  118. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  119. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  120. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  121. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  122. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  123. package/extensions/session-bus/commands.ts +17 -69
  124. package/extensions/session-bus/format.ts +22 -45
  125. package/extensions/session-bus/index.ts +13 -7
  126. package/extensions/session-bus/join-request.ts +40 -0
  127. package/extensions/session-bus/mail-watch.ts +134 -0
  128. package/extensions/session-bus/notice.ts +32 -4
  129. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  130. package/extensions/session-bus/protocol.ts +19 -49
  131. package/extensions/session-bus/resolve.ts +67 -0
  132. package/extensions/session-bus/result.ts +16 -51
  133. package/extensions/session-bus/rows.ts +4 -2
  134. package/extensions/session-bus/runtime.ts +129 -238
  135. package/extensions/session-bus/store.ts +42 -6
  136. package/extensions/session-bus/tools.ts +31 -66
  137. package/extensions/session-bus/tui.ts +4 -4
  138. package/extensions/session-bus/types.ts +13 -13
  139. package/extensions/subagent/agents.ts +9 -8
  140. package/extensions/subagent/invocation.ts +4 -2
  141. package/extensions/team/commands.ts +112 -0
  142. package/extensions/team/dispatch.ts +133 -0
  143. package/extensions/team/format.ts +105 -0
  144. package/extensions/team/index.ts +33 -0
  145. package/extensions/team/schema.ts +29 -0
  146. package/extensions/team/tools.ts +246 -0
  147. package/extensions/team/types.ts +48 -0
  148. package/extensions/web-access/fetch/page.ts +4 -1
  149. package/extensions/web-access/fetch/structured.ts +447 -0
  150. package/extensions/web-access/fetch/tool.ts +2 -1
  151. package/extensions/web-access/search/tool.ts +2 -1
  152. package/extensions/wechat/bridge.ts +76 -92
  153. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  154. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  155. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  156. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  157. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  158. package/extensions/wechat/dialog/channels.ts +136 -0
  159. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  160. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  161. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  162. package/extensions/wechat/index.ts +10 -10
  163. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  164. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  165. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  166. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  167. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  168. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  169. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  170. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  171. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  172. package/extensions/wechat/tools.ts +29 -12
  173. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  174. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  175. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  176. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  177. package/extensions/wechat/transport/outbound.ts +142 -0
  178. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  179. package/extensions/wechat/types.ts +10 -1
  180. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  181. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  182. package/extensions/worktree/guard.ts +3 -5
  183. package/lib/README.md +8 -6
  184. package/lib/board/card.ts +290 -0
  185. package/lib/board/links.ts +117 -0
  186. package/lib/board/page-pointer.ts +256 -0
  187. package/lib/board/round-text.ts +72 -0
  188. package/lib/board/round.ts +89 -0
  189. package/lib/board/rules.ts +304 -0
  190. package/lib/board/session.ts +105 -0
  191. package/lib/board/store.ts +310 -0
  192. package/lib/board/view.ts +150 -0
  193. package/lib/errors.ts +12 -4
  194. package/lib/format.ts +13 -0
  195. package/lib/http/guard.ts +112 -0
  196. package/lib/http/index.ts +16 -5
  197. package/lib/http/serve.ts +234 -0
  198. package/lib/http/server.ts +12 -10
  199. package/lib/http/sink.ts +110 -0
  200. package/lib/http/token.ts +38 -0
  201. package/lib/policy.ts +12 -2
  202. package/lib/requirements.ts +6 -7
  203. package/lib/session-label.ts +64 -0
  204. package/lib/session-roster.ts +130 -0
  205. package/lib/session-tool.ts +184 -0
  206. package/lib/tool-names.ts +25 -2
  207. package/lib/tui/glyphs.ts +2 -0
  208. package/lib/web/README.md +5 -2
  209. package/lib/web/components.ts +5 -0
  210. package/lib/web/runtime.ts +7 -1
  211. package/lib/web/tokens.ts +25 -2
  212. package/package.json +26 -14
  213. package/skills/next/SKILL.md +4 -3
  214. package/skills/project-docs/SKILL.md +133 -0
  215. package/skills/project-docs/references/audit.md +131 -0
  216. package/skills/project-docs/references/changelog.md +105 -0
  217. package/skills/project-docs/references/discovery.md +127 -0
  218. package/skills/project-docs/references/readme.md +80 -0
  219. package/skills/project-docs/references/release.md +84 -0
  220. package/skills/project-docs/references/roadmap.md +93 -0
  221. package/skills/project-docs/scripts/state.ts +316 -0
  222. package/skills/refactor/SKILL.md +100 -125
  223. package/skills/refactor/references/handoff.md +35 -0
  224. package/skills/refactor/references/signals.md +54 -0
  225. package/skills/refactor/scripts/inventory.ts +587 -0
  226. package/extensions/file-browse/README.md +0 -506
  227. package/extensions/file-browse/server.ts +0 -141
  228. package/extensions/plan/split.ts +0 -87
  229. package/extensions/session-bus/README.md +0 -247
  230. package/extensions/session-bus/team.ts +0 -102
  231. package/extensions/wechat/outbound.ts +0 -100
  232. package/lib/plans/sections.ts +0 -172
  233. package/lib/plans/setting.ts +0 -52
  234. package/lib/plans/split.ts +0 -215
  235. package/lib/plans/store.ts +0 -19
  236. package/lib/plans/team.ts +0 -151
  237. package/skills/changelog/SKILL.md +0 -148
  238. package/skills/parallel-dev/SKILL.md +0 -150
  239. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  240. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  241. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  242. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  243. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  244. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  245. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  246. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  247. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  248. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  249. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -14,26 +14,30 @@
14
14
  */
15
15
 
16
16
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
- import { createReadOnlyPolicy, type ReadOnlyPolicy } from "../../lib/policy.ts";
18
- import { ASK_USER_QUESTION_TOOL, GOAL_TOOL, SUBAGENT_TOOL, TODO_TOOL } from "../../lib/tool-names.ts";
17
+ import { createReadOnlyPolicy, WORKSPACE_READERS, type ReadOnlyPolicy } from "../../lib/policy.ts";
18
+ import {
19
+ ASK_USER_QUESTION_TOOL,
20
+ GOAL_TOOL,
21
+ SUBAGENT_TOOL,
22
+ TODO_TOOL,
23
+ WEB_FETCH_TOOL,
24
+ WEB_SEARCH_TOOL,
25
+ } from "../../lib/tool-names.ts";
19
26
  import { ENTER_TOOL } from "./runtime.ts";
20
27
 
21
28
  /**
22
- * Tools that stay available while planning: structured readers, the trackers,
23
- * the git-only `bash`, and the known web/question readers. They are also the
24
- * tools the path backstop does not second-guess.
29
+ * Tools that stay available while planning: structured readers (documents
30
+ * included), the trackers, the git-only `bash`, and the known web/question
31
+ * readers. They are also the tools the path backstop does not second-guess.
25
32
  */
26
33
  export const PLAN_SAFE_TOOLS = [
27
- "read",
28
- "grep",
29
- "find",
30
- "ls",
34
+ ...WORKSPACE_READERS,
31
35
  TODO_TOOL,
32
36
  GOAL_TOOL,
33
37
  "bash",
34
38
  ASK_USER_QUESTION_TOOL,
35
- "web_search",
36
- "web_fetch",
39
+ WEB_SEARCH_TOOL,
40
+ WEB_FETCH_TOOL,
37
41
  ];
38
42
 
39
43
  /**
@@ -43,7 +47,11 @@ export const PLAN_SAFE_TOOLS = [
43
47
  * deliberately absent — it is default-denied like any other shell.
44
48
  */
45
49
  export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL];
46
- /** Tools always blocked while planning, even when annotated read-only. */
50
+ /**
51
+ * Tools always blocked while planning, even when annotated read-only.
52
+ * `enter_plan_mode` is denied so it stays hidden while planning; the guard exempts
53
+ * plan's own registration so a call already in flight can still answer "already".
54
+ */
47
55
  const ALWAYS_BLOCKED_TOOLS = ["write", "edit", ENTER_TOOL];
48
56
 
49
57
  /** Shown when a tool call is refused while planning. */
@@ -17,7 +17,6 @@ import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
17
17
  import { askHuman, isRemoteTurn, remoteLanguage } from "../../lib/interaction.ts";
18
18
  import { FULL_SCREEN_OVERLAY } from "../../lib/tui/chrome.ts";
19
19
  import { feedbackSummary, type PlanAnnotation } from "./feedback.ts";
20
- import type { PlanUnit } from "../../lib/plans/team.ts";
21
20
  import type { PlanStore, StoredPlan } from "./plans.ts";
22
21
  import type { SectionDigest } from "./plan-text.ts";
23
22
  import { planReviewPrompt, parsePlanReviewReply } from "./review-prompt.ts";
@@ -64,15 +63,6 @@ export interface ReviewPlanOptions {
64
63
  plan: StoredPlan;
65
64
  /** Aborting the turn closes a browser review and returns the plan unapproved. */
66
65
  signal?: AbortSignal;
67
- /** The split approval would write, so the screen shows what is being approved. */
68
- split?: ReviewSplit;
69
- }
70
-
71
- /** A validated split, as the review screen displays it. */
72
- export interface ReviewSplit {
73
- lead: string;
74
- units: readonly PlanUnit[];
75
- verdict: "pair" | "team";
76
66
  }
77
67
 
78
68
  /** Prompt shown by the dialog-based refine editor in non-TUI modes. */
@@ -94,7 +84,6 @@ async function askPlanReview(
94
84
  ctx: ExtensionToolContext,
95
85
  plan: StoredPlan,
96
86
  baseline: SectionDigest[] | undefined,
97
- split: ReviewSplit | undefined,
98
87
  ): Promise<PlanReviewChoice> {
99
88
  const language = remoteLanguage(ctx) ?? "en";
100
89
  const remote = isRemoteTurn(ctx);
@@ -105,7 +94,6 @@ async function askPlanReview(
105
94
  new PlanViewComponent({
106
95
  plan,
107
96
  baseline,
108
- ...(split === undefined ? {} : { split }),
109
97
  theme,
110
98
  onClose: (action, refinement) => done({ action, refinement }),
111
99
  requestRender: () => tui.requestRender(),
@@ -143,8 +131,6 @@ export interface BrowserReviewOptions {
143
131
  * they are waiting: the page updates itself, the terminal has to be told.
144
132
  */
145
133
  onReviewerComment?: (author: string, count: number) => void;
146
- /** A line the page shows the owner, such as why approval belongs in the terminal. */
147
- note?: string;
148
134
  }
149
135
 
150
136
  /**
@@ -163,7 +149,6 @@ export async function runBrowserReview(
163
149
  canApprove: options.canApprove,
164
150
  baseline: options.baseline,
165
151
  signal: options.signal,
166
- ...(options.note === undefined ? {} : { note: options.note }),
167
152
  ...(options.onReviewerComment === undefined ? {} : { onReviewerComment: options.onReviewerComment }),
168
153
  })
169
154
  .catch(() => undefined);
@@ -182,7 +167,7 @@ export async function runBrowserReview(
182
167
  * cancelled returns to the screen it was opened from.
183
168
  */
184
169
  export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDecision> {
185
- const { ctx, web, plans, baselines, signal, split } = options;
170
+ const { ctx, web, plans, baselines, signal } = options;
186
171
  let plan = options.plan;
187
172
  for (;;) {
188
173
  // An aborted turn has nobody left to answer the screen.
@@ -190,7 +175,7 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
190
175
  // Read the baseline before the screen and mark it after, so what the next
191
176
  // review compares against is the content this one actually showed.
192
177
  const baseline = baselines.reviewed(plan.path);
193
- const choice = await askPlanReview(ctx, plan, baseline, split);
178
+ const choice = await askPlanReview(ctx, plan, baseline);
194
179
  baselines.markReviewed(plan.path, plan.content);
195
180
 
196
181
  if (choice.action === "approve") return { action: "approve", plan, comments: [] };
@@ -206,17 +191,8 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
206
191
  // An unrecognized action keeps planning rather than opening a server.
207
192
  if (choice.action !== "browser") return { action: "keep", plan };
208
193
 
209
- // A split section exists only after approval, and the page anchors comments to
210
- // source lines, so a page cannot show — or approve — a plan it cannot carry.
211
- // The terminal review is where the split is approved.
212
- const splitNote =
213
- split === undefined
214
- ? undefined
215
- : "This plan has a split, which the page cannot show; approve it in the terminal review.";
216
- if (splitNote !== undefined) ctx.ui.notify(splitNote, "info");
217
194
  const result = await runBrowserReview(web, ctx.cwd, plan, {
218
- canApprove: split === undefined,
219
- ...(splitNote === undefined ? {} : { note: splitNote }),
195
+ canApprove: true,
220
196
  baseline,
221
197
  signal,
222
198
  onOpened: (session) => ctx.ui.notify(`Plan review: opened ${session.url}`, "info"),
@@ -11,8 +11,8 @@
11
11
  * markers survive a resume too.
12
12
  *
13
13
  * `write_plan` is the one sanctioned write while planning. The read-only guard
14
- * in `index.ts` permits a control tool only when the registered tool is this
15
- * extension's own (matched by source path), so a same-named tool from another
14
+ * in `index.ts` permits plan's own tools only when the registered tool is this
15
+ * extension's (matched by source path), so a same-named tool from another
16
16
  * extension cannot borrow the exemption.
17
17
  */
18
18
 
@@ -48,9 +48,17 @@ export const BASELINE_TYPE = "plan-baseline";
48
48
  /** Longest plan-file basename shown in the footer chip. */
49
49
  const STATUS_NAME_LENGTH = 28;
50
50
 
51
- /** Tools plan mode activates itself; the guard permits only this extension's own. */
51
+ /** Tools plan mode activates itself. */
52
52
  const CONTROL_TOOLS = [EXIT_TOOL, CANCEL_TOOL, WRITE_PLAN_TOOL];
53
53
 
54
+ /**
55
+ * Tools the read-only guard exempts when this extension registered them.
56
+ * `enter_plan_mode` is here only so a call already in flight when plan mode came on
57
+ * is answered by the tool ("already in plan mode"); the policy still denies it, so
58
+ * it stays hidden while planning.
59
+ */
60
+ const OWNED_TOOLS = [...CONTROL_TOOLS, ENTER_TOOL];
61
+
54
62
  export interface PlanRuntime {
55
63
  /** Whether plan mode is currently on. */
56
64
  isEnabled(): boolean;
@@ -61,8 +69,8 @@ export interface PlanRuntime {
61
69
  restore(ctx: ExtensionContext): void;
62
70
  /** Plan-file storage for `write_plan` and `exit_plan_mode`. */
63
71
  plans: PlanStore;
64
- /** Whether `toolName` is a plan-owned control tool (not a same-named impostor). */
65
- isControlTool(toolName: string): boolean;
72
+ /** Whether `toolName` is one of plan's own tools (not a same-named impostor) the read-only guard exempts. */
73
+ isOwnTool(toolName: string): boolean;
66
74
  /** Path of the most recently written plan, for the footer chip. */
67
75
  lastPlanPath(): string | undefined;
68
76
  /** Remember a written plan and repaint the footer. */
@@ -159,11 +167,15 @@ export function createPlanRuntime(
159
167
 
160
168
  const plans = createPlanStore(pi);
161
169
 
162
- const isControlTool = (toolName: string): boolean => {
163
- if (!CONTROL_TOOLS.includes(toolName)) return false;
170
+ /**
171
+ * Whether `toolName` is plan's own registration, matched by the entry path the
172
+ * host recorded. Older hosts may not record a source; only a recorded mismatch
173
+ * is rejected.
174
+ */
175
+ const isOwnTool = (toolName: string): boolean => {
176
+ if (!OWNED_TOOLS.includes(toolName)) return false;
164
177
  const info = pi.getAllTools().find((tool) => tool.name === toolName);
165
178
  const source = info?.sourceInfo?.path;
166
- // Older hosts may not record a source; only reject a recorded mismatch.
167
179
  return source === undefined || options.entryPath === undefined || samePath(source, options.entryPath);
168
180
  };
169
181
 
@@ -241,7 +253,7 @@ export function createPlanRuntime(
241
253
  if (ctx) setStatus(ctx);
242
254
  },
243
255
  plans,
244
- isControlTool,
256
+ isOwnTool,
245
257
  lastPlanPath: () => lastPlanPath,
246
258
  setLastPlan: (ctx, path) => {
247
259
  lastPlanPath = path;
@@ -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;
@@ -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,9 @@ 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
+ const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
217
+ const goal = await finalizeGoal(pi, ctx, decision.plan);
280
218
  const listing =
281
219
  steps.length > 0
282
220
  ? `\n\n${steps
@@ -303,41 +241,19 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
303
241
  const browserNote = decision.comments.length
304
242
  ? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
305
243
  : "";
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
244
  return {
313
245
  content: [
314
246
  {
315
247
  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}`,
248
+ 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
249
  },
318
250
  ],
319
251
  details: {
320
252
  approved: true,
321
- plan: approved.content,
322
- planPath: approved.path,
323
- relativePath: approved.relativePath,
253
+ plan: decision.plan.content,
254
+ planPath: decision.plan.path,
255
+ relativePath: decision.plan.relativePath,
324
256
  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
257
  } satisfies ExitPlanModeDetails,
342
258
  };
343
259
  },
@@ -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;
@@ -104,14 +104,6 @@ export interface ExitPlanModeDetails {
104
104
  cancelled?: boolean;
105
105
  /** True when no interactive UI was available to ask for approval. */
106
106
  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
107
  }
116
108
 
117
109
  /** Structured result carried in `cancel_plan_mode`'s `details`. */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The browser review client, served as `/client.js`.
3
3
  *
4
- * Kept as one self-contained script (like `file-browse/client.ts`) with no
4
+ * Kept as one self-contained script (like `file-browse/view/client.ts`) with no
5
5
  * build step. Its document half — the inline and block Markdown rendering — is
6
6
  * `web-markdown.ts`, a fragment of the same IIFE interpolated below. It renders
7
7
  * the plan from the JSON data island using DOM text nodes only — plan and