@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
@@ -1,6 +1,6 @@
1
1
  # team — the lead's round over the board
2
2
 
3
- `board` owns the cards. `team` owns the lead's three actions: `assign` hands a card to a
3
+ `board` owns the cards. `team` owns the lead's four actions: `assign` hands a card to a
4
4
  session that is **already running**, `offer` hands one to whoever is free, and `reclaim`
5
5
  takes one back when its owner is gone or has gone quiet. `assign` writes the card and puts
6
6
  a handoff on the session bus, `offer` writes only the moment it was made — never an owner,
@@ -11,32 +11,44 @@ The report says what the peer will actually do with it.
11
11
  Nothing starts a process. A message lands in the peer's inbox and is reported at its next
12
12
  turn, so `assign` and `offer` are messages, not launches.
13
13
 
14
- <img src="../../assets/screens/team.png" alt="The Team tab as a dashboard: on the left, a Needs a decision queue of rows with a coloured kind edge, the reason on its own line, and the owner as a chip on the row's right; on the right, a Teammates list with a state dot, label, you chip, mail badge, a not listening chip on a peer whose watch is off, and a line of what each holds; and one muted Done line under both" width="100%">
14
+ Both halves are **opt-in**, because the board is a workflow a session either runs or does
15
+ not. A lead names both — `PI_ENABLED_EXTENSIONS=board,team` — and a worker session that
16
+ only works cards names neither. They stay independent at runtime: naming one never loads
17
+ the other, and `team` is refused by `PI_DISABLED_EXTENSIONS` like every extension.
18
+
19
+ Naming `team` alone is legal and stays so — the factory needs no board to register — but
20
+ it is the crippled case: every card it moves is one no `board` tool can read. `doctor`
21
+ reports the board row as opt-in and names the switch that adds it, which is the one place
22
+ a reader who took that shortcut is told.
23
+
24
+ <img src="../../assets/screens/team.png" alt="The Team tab as a dashboard: on the left, a Needs a decision queue of rows with a coloured kind edge, the reason on its own line, and the owner as a chip on the row's right; on the right, a Teammates list of one-line rows, each a state dot, a label, a you chip, a mail badge, a not listening chip on a peer whose watch is off, and what that teammate holds at the row's right; and one muted Done line under both" width="100%">
15
25
 
16
26
  *The Team tab: the cards that need the lead, then who is on the team — including an
17
- owner whose label nothing answers to, and a teammate that is not listening.*
27
+ owner whose label nothing answers to, and a teammate that is not listening. A row is one
28
+ line; the owners a bus has no record of collapse to one row*
18
29
 
19
30
  ## Capabilities
20
31
 
21
32
  | Capability | How | What you get |
22
33
  |---|---|---|
23
- | Join the team | `/board` as the lead, `/team join <label>` as a teammate, or any card you work | The session is on the team under a label peers address, listening, and `/team` and the Team tab list it |
24
- | Hand a feature to a running session | `team` action `assign` | The card is `doing`, owned by that session, and a handoff is in its inbox |
34
+ | Join the team | `/board` as the lead, `/team join <label>` as a teammate, or the first card you claim | The session is on the team under a name peers address, listening, and `/team` and the Team tab list it |
35
+ | Hand a feature to a running session | `team` action `assign` | The card is `plan`, owned by that session, and a handoff is in its inbox |
25
36
  | Tell the peer what to do first | `team` action `assign` with `note` | The handoff carries the card, its paths, its plan, and that line |
26
37
  | Hand a feature to whoever is free | `team` action `offer` | Every other live session gets an event naming the card; the card stays unowned until one claims it, and it records when the offer was made, so a lead can tell an offer nobody took from a card nobody has looked at |
27
38
  | Know whether a message will be woken for | an `assign` or `offer` result, `/team`, and the Team tab | All three name the peers whose record says their next turn is when it lands: the result as `Not listening: <labels>`, `/team` as a lowercase `not listening:` inside its presence line, the Team tab as a chip |
28
- | Take a card back | `team` action `reclaim` | The owner clears, the card returns to To do, and its notes stay |
29
- | Keep the round to the team | `team` | Only `assign`, `offer`, and `reclaim`: no card is created, moved, planned, or finished here — that is `board`'s surface |
30
- | See the round without a browser | `/team` | `buildRound`'s summary — the one the page's Team tab is served — laid out as text by `formatRound`: what needs a decision, who holds what, then one line naming the cards a dead owner left behind, and one presence line for who is live, who is not listening, who has gone quiet, and who is gone. The rollup itself is capped at ten lines, then `… N more`, and the gone and presence lines follow the cap rather than counting against it — the cap is a notify's limit, not a screen's. An empty board is one line |
39
+ | Take a card back | `team` action `reclaim` | The owner clears, the card returns to Backlog, and its notes stay |
40
+ | Keep the round to the team | `team` | Only `assign`, `offer`, `reclaim`, and `claim`: no card is created, contracted, approved, or finished here — that is `board`'s surface, which is where a claimed card is planned and moved |
41
+ | See the round without a browser | `/team` | `buildRound`'s summary — the one the page's Team tab is served — laid out as text by `formatRound`: what needs a decision, who holds what, then one line naming the cards a dead owner left behind, and one presence line for who is live, who is not listening, who has gone quiet, and who is gone. The owners the bus has no record of and that hold nothing are one line, `18 owners with no record · 76 done`, in the Team tab's own words — the fold is the same rule and the same sentence on both surfaces, and it keeps a long history from spending the round's cap. The rollup itself is capped at ten lines, then `… N more`, and the gone and presence lines follow the cap rather than counting against it — the cap is a notify's limit, not a screen's. An empty board is one line |
31
42
 
32
43
  ## Quickstart
33
44
 
34
45
  ```
35
- lead: /board → on the team as team-lead, listening
46
+ lead: /board → on the team as lead, listening
36
47
  peer: /team join billing-core → on the team as billing-core, listening
37
48
  lead: team({action:"assign", card:"4", to:"billing-core", note:"start with the webhook"})
38
49
  → #4 handed off to billing-core.
39
- peer: board({action:"plan", card:"4", path:"…"}) → the card records its plan
50
+ peer: board({action:"contract", card:"4", path:"…", goal:"…", acceptance:"…", scopeOut:"…"})
51
+ → the card records its plan and its contract
40
52
  peer: board({action:"note", card:"4", text:"…"}) → a phase, as it happens
41
53
  later: team({action:"reclaim", card:"4"}) → #4 reclaimed from billing-core
42
54
  ```
@@ -49,6 +61,13 @@ lead: team({action:"offer", card:"5", note:"whoever is free"})
49
61
  peer: board({action:"claim", card:"5"}) → the offer becomes work
50
62
  ```
51
63
 
64
+ Nobody free, so the lead takes it:
65
+
66
+ ```
67
+ lead: team({action:"claim", card:"5"}) → #5 is yours, in plan.
68
+ → design it in plan mode, `plan` action `write`, then `board` action `contract`
69
+ ```
70
+
52
71
  ## Joining the team
53
72
 
54
73
  You do not configure a team; there are no members to add. The team is the session bus,
@@ -57,20 +76,22 @@ its surface is the point: `board` and `team` join this session for you.
57
76
 
58
77
  | You are | Action | What happens |
59
78
  |---|---|---|
60
- | The lead | `/board` | The session joins as `team-lead` — the page's own seat name — and listens, so a handoff or an offer is reported at its next turn |
61
- | A teammate | `/team join billing-core` | The session joins as `billing-core`, and Pi's footer and `/resume` show that name |
62
- | Either | any `board` action that writes | The first one joins the session under the name it already has, and listens |
63
- | A viewer | `/board` on a session that is not serving | The page is somebody else's to serve, so this session joins under its own label instead of the seat |
79
+ | The lead | `/board` | The session joins as the board's lead (`board.lead`, `lead` by default) — the name the page serves as — and listens, so a handoff or an offer is reported at its next turn |
80
+ | A teammate | the first card it claims | The session joins under the name it already holds, or a preset teammate name (`tom`, `lily`, `ben`) when it has none, and listens; Pi's footer keeps the name it already had |
81
+ | A teammate who wants a name | `/team join billing-core` | The session joins as `billing-core`, and Pi's footer and `/resume` show that name — the one join that renames |
82
+ | A viewer | `/board` on a session that is not serving | The page is somebody else's to serve, so this session joins under its own name instead of the seat |
64
83
 
65
84
  Joining is listening: mail is reported at that session's next turn, and **no turn is
66
- raised** for it, so being on the team never spends a turn. `/team` says who is live and
67
- who is live but not listening. `/board status` repeats only what the last `/board` in this
68
- session did about the team — a join made by `/team join` or by a card write is not on that
69
- line, and a page that is not running prints no team clause at all.
70
-
71
- **The seat is not a session.** The page is a view: it has one name, `team-lead`, and an id
72
- of its own derived from it, and it writes nothing, so it holds no card for a lead to take
73
- back. Its cards come from the session that serves it, whose label is the same and whose id
85
+ raised** for it, so being on the team never spends a turn. `/team join` with no name draws
86
+ one when Pi has no name for the session — `lily` rather than the repository's basename — and
87
+ renames nothing; a session Pi already names keeps that name. `/team` says who is live and
88
+ who is live but not listening. `/board status` reports the team this session is on — which
89
+ label, and whether mail reaches it — whichever door put it there, and prints no team clause
90
+ only when this session never joined at all.
91
+
92
+ **The seat is not a session.** The page is a view: it has one name — the board's lead — and
93
+ an id of its own, `board:<lead>`, and it writes nothing, so it holds no card for a lead to
94
+ take back. Its cards come from the session that serves it, whose label is the same and whose id
74
95
  is not: a card that session claimed is the session's, and `release` returns it.
75
96
 
76
97
  ## Tool
@@ -80,9 +101,10 @@ is not: a card that session claimed is the session's, and `release` returns it.
80
101
 
81
102
  | Action | Parameters | Behaviour |
82
103
  |---|---|---|
83
- | `assign` | `card`, `to`, `note?` | Checks the card's preconditions, sends the handoff as a `handoff` message, then records the owner |
104
+ | `assign` | `card`, `to`, `note?` | Checks the card's preconditions, sends the handoff as a `handoff` message, then records the owner. `to` that names this session is refused — a card you will take yourself is `claim` |
84
105
  | `offer` | `card`, `note?` | Checks the preconditions a claim would meet, broadcasts the card as an `event` to every other live session, then records the offer on the card — the moment, and nothing else: status and owner are untouched |
85
- | `reclaim` | `card` | Clears the owner and returns the card to To do, keeping its notes and plan |
106
+ | `reclaim` | `card` | Clears the owner and returns the card to Backlog, keeping its notes, contract, and plan |
107
+ | `claim` | `card` | Takes a pool card for this session: it enters `plan`, owned by the round's session, and the claim asks to join the team once — the same operation and the same join `board`'s claim makes. A card somebody holds, one that waits on unfinished work, and a full `plan` lane are refused with the board's own sentence |
86
108
 
87
109
  `card` takes `4` or `#4`. `to` is a teammate's label or id, as `/team` lists them — a
88
110
  label nobody answers to cannot be handed a card, and the result says so.
@@ -108,7 +130,7 @@ the sentence, and a test holds every published parameter to an action that acts
108
130
  | What failed | What the card is | What the lead reads |
109
131
  |---|---|---|
110
132
  | the card is missing, `done`, held by a third session, or waiting on unfinished work | untouched | the refusal, with no message sent at all |
111
- | the send | untouched — `todo`, no owner | `#4 was not handed off to billing-core: …`, plus `The card is unchanged.` |
133
+ | the send | untouched — `backlog`, no owner | `#4 was not handed off to billing-core: …`, plus `The card is unchanged.` |
112
134
  | the assignment, after a successful send | whatever the other writer left | `#4 was handed off to billing-core, but the card changed before it could be assigned: …`, and the peer is told to `board claim` it |
113
135
 
114
136
  `offer` broadcasts **before** it records, for the same reason: an offer nobody received
@@ -136,15 +158,18 @@ not name — goes back to `reclaim` if its holder renames.
136
158
  A handoff is an instruction, not a notification: the peer's notice is headlined
137
159
  `1 handoff`, carries enough of the body to be actionable, and says the work is theirs —
138
160
  read it with `session inbox`, record the plan, and start. The card is already theirs
139
- (`assign` wrote the owner), so there is no claim step; the peer plans, works, notes, and
140
- moves it to `done` or `blocked`.
161
+ (`assign` wrote the owner), so there is no claim step; the peer writes the contract, works,
162
+ notes, and moves it on — into `execute` once the lead has approved the contract, and to
163
+ `done` once the lead has spot-checked it.
141
164
 
142
165
  An offer is advice: nothing is theirs until they claim it, and a card nobody claims stays
143
- `todo`, so it keeps showing up as unassigned in the round — with its age, which is the one
166
+ `backlog`, so it keeps showing up as unassigned in the round — with its age, which is the one
144
167
  thing an event cannot carry. `board` prints it as `offered 1m ago, and nobody took it`, the
145
168
  card face says `offered 1m ago · nobody took it`, and the Team tab's queue says
146
169
  `offered 1m ago — nobody took it` in place of "nobody holds it". A lead reading that has a
147
- decision the plain unassigned row never offered: re-offer, `assign` it, or take it.
170
+ decision the plain unassigned row never offered: re-offer, `assign` it, or take it with
171
+ `claim`, which is yours then — the card sits in `plan` waiting on you, and its contract,
172
+ moves, and finish are `board`'s in this same session.
148
173
 
149
174
  ## Security
150
175
 
@@ -159,15 +184,21 @@ decision the plain unassigned row never offered: re-offer, `assign` it, or take
159
184
 
160
185
  - **Nothing here can start a peer's turn.** A message is reported at the recipient's next
161
186
  turn; a session that is not listening at all reads it only when it reads its inbox.
162
- - **`to` must be on the team.** A session that never opened the board, joined, or worked a
187
+ - **`to` must be on the team.** A session that never opened the board, joined, or claimed a
163
188
  card cannot be handed a card, and an offer to nobody says so.
164
- - **`team-lead` is a label, not a person.** The seat is not a session, so its actor id
165
- `board:team-lead` answers no message; the label is the one the session serving the page
166
- joins under, so a handoff to `team-lead` reaches whoever is serving the page just now —
189
+ - **A held card is not assignable.** `assign` moves a pool card; one somebody already holds
190
+ is refused, because a handoff cannot take work away from its owner — `reclaim` it, then
191
+ assign.
192
+ - **The lead's name is a label, not a person.** The seat is not a session, so its actor id
193
+ `board:<lead>` answers no message; the label is the one the session serving the page joins
194
+ under, so a handoff to it reaches whoever is serving the page just now —
167
195
  and is refused, like any other label, when nobody is. The seat's cards come back with
168
196
  `reclaim`.
169
197
  - **A label-only owner is fragile.** If the bus could not name the session behind `to`,
170
198
  the card records the label, and a rename would need a `reclaim` and a fresh handoff.
199
+ - **A preset name is drawn, not chosen.** An unnamed session that joins draws one of the
200
+ preset teammate names and Pi's footer keeps whatever it had; type `/team join <label>` to
201
+ pick the name and make it Pi's.
171
202
  - **An offer is advice.** A card can sit offered indefinitely; the round keeps counting it
172
203
  as unassigned, which is the honest signal that nobody took it.
173
204
  - **An offer arrives as an `event`.** A burst that mixes one with ordinary messages is
@@ -179,7 +210,7 @@ decision the plain unassigned row never offered: re-offer, `assign` it, or take
179
210
  - **Plan mode makes this unavailable.** Neither `team` nor `session` is a read-only tool, so
180
211
  plan mode refuses both by name and tells the model to exit and get approval; the board
181
212
  stays readable, since `list` is the one action it admits. Assign after the plan is
182
- approved.
213
+ approved by the lead — or the person, with `/board approve`.
183
214
  - **Ownership is advisory.** `assign` does not create a path claim; the peer still claims
184
215
  the paths it edits through `session`.
185
216
 
@@ -188,7 +219,8 @@ decision the plain unassigned row never offered: re-offer, `assign` it, or take
188
219
  - **`board`**: the same store, the same cards, the same rules — `team` calls `lib/board/`
189
220
  directly, so a card it hands over is one `board` can see. Either extension works without
190
221
  the other: without `team` the page still reclaims, and without `board` there is nothing
191
- to assign.
222
+ to assign — which is why the lead enables both, and why enabling one never loads the
223
+ other.
192
224
  - **`session-bus`**: the transport, and the one extension `team` talks to — through
193
225
  `lib/session-tool.ts`, whose `join` is an in-process request (a command context cannot
194
226
  call a tool) and whose `peers` and `send` are nested tool calls. The bus's actions and
@@ -207,9 +239,9 @@ decision the plain unassigned row never offered: re-offer, `assign` it, or take
207
239
  and must work with the bus disabled; `team` is the lead's round and is the only thing
208
240
  that needs the bus. If the round ever stops needing it, `team` folds back in.
209
241
  - **Joining is the bus's, wearing `board`'s clothes.** A lead should not have to learn a
210
- second tool to be on a team, so `/board` joins, a card write joins, and `/team join` is
211
- the one naming action. `wake` stays a bus-level choice and is never armed on a user's
212
- behalf, because it spends turns.
242
+ second tool to be on a team, so `/board` joins, the first claimed card joins, and
243
+ `/team join` is the one naming action. `wake` stays a bus-level choice and is never
244
+ armed on a user's behalf, because it spends turns.
213
245
  - **Push and pull are two verbs.** `assign` writes the owner and sends one message;
214
246
  `offer` broadcasts and writes nothing. One verb with an optional `to` would have two
215
247
  outcomes and one details shape, and a reader could not tell which happened.
@@ -221,10 +253,17 @@ decision the plain unassigned row never offered: re-offer, `assign` it, or take
221
253
  guessing from the send.
222
254
  - **Send first.** A card assigned without a message is a card nobody knows about, while a
223
255
  message for a card that then changed is one the peer can still claim.
224
- - **`reclaim` is here, and in `board`.** It is the compensation for a dispatch that did not
225
- land, so it belongs in the lead's context — and it is the same operation `board` and its
226
- page offer, because a card whose owner went away must not need a second extension to come
227
- back.
256
+ - **`reclaim` and `claim` are here, and in `board`.** Every one of them is one store
257
+ operation, so the lane rules, the WIP limit, the refusal a waiting card gets, and the
258
+ offer record cleared are the store's answer rather than a second one the round keeps in
259
+ step by hand. `reclaim` belongs here because it is the compensation for a dispatch that
260
+ did not land; `claim` belongs here because the round is where a lead sees work nobody
261
+ took, and a lead told to "take it" should not have to leave the tool to do it. The
262
+ card's *life* after a claim — plan, contract, moves, finish — stays `board`'s: the round
263
+ hands work out and takes it, and the card is lived in one place.
264
+ - **Rejected: `assign` to yourself.** Handing a card to your own label records you as the
265
+ owner and mails your own inbox, which is a claim with an extra message and no next step;
266
+ it is refused, naming `claim`.
228
267
  - **Rejected: `reclaim` messaging the previous owner.** A compensation path must not depend
229
268
  on a second delivery — the reclaim is what you do when the owner is gone, so its message
230
269
  would fail exactly when it is needed. The card is the record.
@@ -236,8 +275,8 @@ decision the plain unassigned row never offered: re-offer, `assign` it, or take
236
275
 
237
276
  | File | Responsibility |
238
277
  |---|---|
239
- | `index.ts` | The factory: gate on `PI_DISABLED_EXTENSIONS`, build the session, register the tool and the command |
240
- | `schema.ts` | The tool's three actions and their parameters |
278
+ | `index.ts` | The factory: gate on the opt-in switch (`PI_ENABLED_EXTENSIONS`) and `PI_DISABLED_EXTENSIONS`, build the session, register the tool and the command |
279
+ | `schema.ts` | The tool's four actions and their parameters |
241
280
  | `tools.ts` | The dispatch order, the refusals, and the report |
242
281
  | `dispatch.ts` | The handoff and offer text, the bus behind a seam, and who is not listening |
243
282
  | `commands.ts` | `/team` — the round, and `/team join [<label>]` |
@@ -10,21 +10,19 @@ one update when work lands without one.
10
10
 
11
11
  ## Capabilities
12
12
 
13
- Status records what a reader can rely on: **Shipped** is in a tagged release, **Unreleased** is complete in the current tree but not yet tagged, and **Partial** has one released part and one unreleased part. Rows are ordered by user value, highest first.
14
-
15
- | Capability | How | What you get | Status |
16
- |---|---|---|---|
17
- | Whole-list tracking | `todo` | One call replaces the list; an empty list clears it. | Shipped |
18
- | Live progress rail | the `todo` widget | Progress and the active item, one line above the editor. | Shipped |
19
- | Atomic validation | `todo` validation | At most one `in_progress`, at most 50 items, unique non-empty content, and a known status; a violation returns an error with the unchanged list. | Shipped |
20
- | Terminal-safe normalization | sanitized `content` and `activeForm` | Control characters, newlines, tabs, and repeated spaces collapse to one terminal-safe line before the list is stored. | Shipped |
21
- | Compact model result | the `todo` result | The model gets progress and the active item, not an echo of the whole list, so every update does not repeat the table. | Shipped |
22
- | Transcript rendering | the `todo` transcript block | The call line carries progress; the collapsed rows lead with active work, the expanded rows keep submission order, and a cap ends with `… N more`. | Shipped |
23
- | Full list on demand | `/todos` | A scrollable screen in the order the model submitted. | Shipped |
24
- | Headless listing | `/todos` outside `tui` | RPC, print, and JSON modes get the same list as a notification, with no terminal screen. | Shipped |
25
- | Branch-following state | tool-result `details` | The list survives `/resume` and `/tree`, and abandoned branches never leak in. | Shipped |
26
- | Lag reminder | the `agent_before_settle` nudge | One hidden reminder and one forced continuation when mutating work lands without a todo update. | Shipped |
27
- | Structured result | `structuredContent` | Codemode scripts read the list as data. | Shipped |
13
+ | Capability | How | What you get |
14
+ |---|---|---|
15
+ | Whole-list tracking | `todo` | One call replaces the list; an empty list clears it. |
16
+ | Live progress rail | the `todo` widget | Progress and the active item, one line above the editor. |
17
+ | Atomic validation | `todo` validation | At most one `in_progress`, at most 50 items, unique non-empty content, and a known status; a violation returns an error with the unchanged list. |
18
+ | Terminal-safe normalization | sanitized `content` and `activeForm` | Control characters, newlines, tabs, and repeated spaces collapse to one terminal-safe line before the list is stored. |
19
+ | Compact model result | the `todo` result | The model gets progress and the active item, not an echo of the whole list, so every update does not repeat the table. |
20
+ | Transcript rendering | the `todo` transcript block | The call line carries progress; the collapsed rows lead with active work, the expanded rows keep submission order, and a cap ends with `… N more`. |
21
+ | Full list on demand | `/todos` | A scrollable screen in the order the model submitted. |
22
+ | Headless listing | `/todos` outside `tui` | RPC, print, and JSON modes get the same list as a notification, with no terminal screen. |
23
+ | Branch-following state | tool-result `details` | The list survives `/resume` and `/tree`, and abandoned branches never leak in. |
24
+ | Lag reminder | the `agent_before_settle` nudge | One hidden reminder and one forced continuation when mutating work lands without a todo update. |
25
+ | Structured result | `structuredContent` | Codemode scripts read the list as data. |
28
26
 
29
27
  ## Quickstart
30
28
 
@@ -238,10 +236,12 @@ depends on config.
238
236
  | `schema.ts` | TypeBox parameters; the parser and limits live in `lib/todo-state.ts`. |
239
237
  | `state.ts` | Status queries over the replayed list (pure). |
240
238
  | `nudge.ts` | Lag detection: mutating and file-write classification, due check, reminder text (pure). |
241
- | `format.ts` | Model-facing and transcript text (pure). |
242
- | `tui.ts` | Widget and scrollable `/todos` components, drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
239
+ | `format.ts` | Model-facing and transcript text, plus the item row both surfaces draw (pure). |
240
+ | `render.ts` | The `TodoResult` transcript block over the memoized rail, drawn from `lib/tui/render-cache.ts`. |
241
+ | `widget.ts` | The persistent one-line widget and its `WIDGET_KEY`, drawn from the `lib/tui/` kit. |
242
+ | `view/todo-screen.ts` | The scrollable `/todos` screen, loaded on first use (`titleRule`, `dockListFrame`, `emptyState`). |
243
243
  | `config.ts` | Widget config load/validation (the `todo` section of `bytefly.json`). |
244
244
  | `runtime.ts` | Session-scoped state, lag tracking, and widget synchronization. |
245
245
  | `tools.ts` | `todo` tool registration and rendering. |
246
246
  | `commands.ts` | `/todos`. |
247
- | `tools.test.ts`, `runtime.test.ts`, `ui.test.ts` | The capability suites, declared in `test/suite-manifest.ts`. |
247
+ | `tools.test.ts`, `runtime.test.ts`, `view/todo-screen.test.ts` | The capability suites, declared in `test/suite-manifest.ts`. |
@@ -8,32 +8,30 @@ after a running turn settles.
8
8
 
9
9
  ## Capabilities
10
10
 
11
- Status records what a reader can rely on: **Shipped** is in a tagged release, **Unreleased** is complete in the current tree but not yet tagged, and **Partial** has one released part and one unreleased part. Rows are ordered by user value, highest first.
12
-
13
11
  ### Watching
14
12
 
15
- | Capability | How | What you get | Status |
16
- |---|---|---|---|
17
- | Tail a source | `watch add` | A file is followed from its current end unless `fromStart` is set, and a shrinking file is re-read from the start; a command's merged stdout/stderr is streamed. | Shipped |
18
- | Line matching | `watch add` | An include pattern with an optional exclude list, case-insensitive unless `caseSensitive` is set. | Shipped |
19
- | Match delivery | `watch add`, `watch matches` | Pending matches are reported to the model at the start of the next turn. | Shipped |
20
- | Wake turn | `watch add`, the `wakeOnMatch` key | One triggered turn as soon as the agent is idle, including right after a running turn settles. | Shipped |
21
- | Desktop alert | `my-pi-agent/watch-match`, `notify.enabled`, `notify.matchFound` | A wake match leaves the terminal as an OS notification. | Shipped |
22
- | Live status | `watch add`, the `showStatus` key | A `⊚ N` running chip and a `! N` unreported-match chip, plus one alert per unreported burst. | Shipped |
23
- | Command outcome | `watch status`, `watch list` | A command watcher reports its exit code or error, including the captured signal, instead of only "stopped". | Shipped |
24
- | Liveness detail | `watch list`, `watch status` | A running watcher names its pid and elapsed time; list rows sort running watchers first, newest next. | Shipped |
25
- | Transcript rendering | the `watch` transcript block | The outcome line, at most eight rows, and a `(ctrl+o to expand)` hint keep the transcript compact. | Shipped |
26
- | Sanitized output | `watch matches`, `/watch matches` | Model- and remote-authored output is stripped of control characters before it reaches a terminal or a notification. | Shipped |
13
+ | Capability | How | What you get |
14
+ |---|---|---|
15
+ | Tail a source | `watch add` | A file is followed from its current end unless `fromStart` is set, and a shrinking file is re-read from the start; a command's merged stdout/stderr is streamed. |
16
+ | Line matching | `watch add` | An include pattern with an optional exclude list, case-insensitive unless `caseSensitive` is set. |
17
+ | Match delivery | `watch add`, `watch matches` | Pending matches are reported to the model at the start of the next turn. |
18
+ | Wake turn | `watch add`, the `wakeOnMatch` key | One triggered turn as soon as the agent is idle, including right after a running turn settles. |
19
+ | Desktop alert | `my-pi-agent/watch-match`, `notify.enabled`, `notify.matchFound` | A wake match leaves the terminal as an OS notification. |
20
+ | Live status | `watch add`, the `showStatus` key | A `⊚ N` running chip and a `! N` unreported-match chip, plus one alert per unreported burst. |
21
+ | Command outcome | `watch status`, `watch list` | A command watcher reports its exit code or error, including the captured signal, instead of only "stopped". |
22
+ | Liveness detail | `watch list`, `watch status` | A running watcher names its pid and elapsed time; list rows sort running watchers first, newest next. |
23
+ | Transcript rendering | the `watch` transcript block | The outcome line, at most eight rows, and a `(ctrl+o to expand)` hint keep the transcript compact. |
24
+ | Sanitized output | `watch matches`, `/watch matches` | Model- and remote-authored output is stripped of control characters before it reaches a terminal or a notification. |
27
25
 
28
26
  ### Reading and managing
29
27
 
30
- | Capability | How | What you get | Status |
31
- |---|---|---|---|
32
- | Watcher inventory | `watch list`, `watch status`, `/watch list` | Each watcher's source, state, and line and match counts, singly or as a table. | Shipped |
33
- | Matched lines | `watch matches`, `/watch matches` | The matching lines, sanitized and read from a bounded history. | Shipped |
34
- | Raw output | `watch logs`, `/watch logs` | The merged output tail, for checking a pattern that matches nothing. | Shipped |
35
- | Watcher screen | `/watch` | A dock screen with the watcher list, the focused watcher's detail, and its matches pane. | Shipped |
36
- | Stopping and cleanup | `watch stop`, `watch clear`, `/watch stop`, `/watch clear` | A running watcher stops, stopped ones are removed, and `all` takes the running ones too. | Shipped |
28
+ | Capability | How | What you get |
29
+ |---|---|---|
30
+ | Watcher inventory | `watch list`, `watch status`, `/watch list` | Each watcher's source, state, and line and match counts, singly or as a table. |
31
+ | Matched lines | `watch matches`, `/watch matches` | The matching lines, sanitized and read from a bounded history. |
32
+ | Raw output | `watch logs`, `/watch logs` | The merged output tail, for checking a pattern that matches nothing. |
33
+ | Watcher screen | `/watch` | A dock screen with the watcher list, the focused watcher's detail, and its matches pane. |
34
+ | Stopping and cleanup | `watch stop`, `watch clear`, `/watch stop`, `/watch clear` | A running watcher stops, stopped ones are removed, and `all` takes the running ones too. |
37
35
 
38
36
  ## Quickstart
39
37
 
@@ -224,7 +222,8 @@ The shared seams are `lib/`'s: `lib/spawn.ts` (spawn, tree-kill), `lib/lines.ts`
224
222
  `lib/tui/status-controller.ts` (chips and the alert), and
225
223
  `lib/tui/dock-screen.ts` (the `/watch` frame, navigation, and confirm layer).
226
224
  What stays here is the watcher policy: the tailer, the matcher, the source
227
- model, the command-child lifecycle in `child.ts`, and the watcher vocabulary.
225
+ model, and the watcher vocabulary; the command-child lifecycle is
226
+ `lib/child.ts`, which `job` drives too.
228
227
 
229
228
  The file tailer polls and exposes `tick()` publicly; the interval is a thin
230
229
  wrapper over `tick()`, so tests drive matching deterministically with no real
@@ -248,18 +247,19 @@ outlive the session.
248
247
  | `tools.ts` | The `watch` tool definition, execution, and transcript rendering. |
249
248
  | `schema.ts` | TypeBox parameters and pure `normalizeCall` validation. |
250
249
  | `types.ts` | `WatchAction`/`WatchStatus`, `WatchRecord`, `WatchDetails`, `PendingMatch`. |
251
- | `runtime.ts` | The session-scoped watcher table, matcher, poll tick, and shutdown; spawning and tree-kill come from `child.ts`, line splitting from `lib/lines.ts`. |
252
- | `child.ts` | A command watcher's process lifecycle: spawn, the SIGTERM → SIGKILL grace, and the shutdown drain, over the injected `SpawnFn`/`KillTreeFn` seams. |
253
- | `format.ts` | Pure text formatting; sanitization is `lib/format.ts`'s `sanitizeUntrustedLine`. |
250
+ | `runtime.ts` | The session-scoped watcher table, matcher, poll tick, and shutdown; spawning and tree-kill come from `lib/child.ts`, line splitting from `lib/lines.ts`. |
251
+ | `format.ts` | Pure text formatting, including the watcher row both surfaces draw; sanitization is `lib/format.ts`'s `sanitizeUntrustedLine`. |
254
252
  | `ui.ts` | Status chips and the match alert, over the shared `lib/tui/status-controller.ts` (chips, attached context, `notify`). |
255
- | `tui.ts` | The `WatchResult` transcript block over the shared `TailResult`, and the `/watch` screen as a `DockListScreen` subclass: watcher rows, focus block, counts, verbs. |
253
+ | `render.ts` | The `WatchResult` transcript block over the shared `TailResult`. |
254
+ | `view/watch-screen.ts` | The `/watch` screen as a `DockListScreen` subclass: watcher rows, focus block, counts, verbs, loaded on first use. |
256
255
  | `commands.ts` | The `/watch` command and its completions. |
257
256
  | `config.ts` | The `watch` config section load and normalization. |
258
257
 
259
258
  ## Testing
260
259
 
261
- `runtime.test.ts` covers config, schema, the child supervisor, and the tailer
262
- with a fake clock and fake children; `tools.test.ts` covers the tool and command;
263
- `ui.test.ts` covers registration, formatting, the status chips, and the screen
264
- through the shared `createFakePi`/`fakeCtx` doubles. The test harness is
265
- `test/helpers/fixtures/watch.ts`.
260
+ `runtime.test.ts` covers config, schema, and the tailer with a fake clock and
261
+ fake children; `tools.test.ts` covers the tool and command; `format.test.ts` covers
262
+ the text `format.ts` builds; `view/watch-screen.test.ts` covers registration, the status
263
+ chips, and the screen through the shared `createFakePi`/`fakeCtx` doubles. The test
264
+ harness is `test/helpers/fixtures/watch.ts`, which also builds the `WatchRecord` a
265
+ test varies one field of.
@@ -8,8 +8,6 @@ reads a URL as text. Both are `direct`, active on registration, and read the
8
8
 
9
9
  ## Capabilities
10
10
 
11
- Status records what a reader can rely on: **Shipped** is in a tagged release, **Unreleased** is complete in the current tree but not yet tagged, and **Partial** has one released part and one unreleased part. Rows are ordered by user value, highest first.
12
-
13
11
  | Capability | How | What you get | Status |
14
12
  |---|---|---|---|
15
13
  | Web search | `web_search` | Titles, URLs, snippets, and a direct answer when the provider has one. | Shipped |
@@ -410,11 +408,11 @@ the content type and size.
410
408
  | `fetch/tool.ts` | `web_fetch` definition, activation, rendering |
411
409
  | `fetch/config.ts` | Fetch config type, defaults, clamping |
412
410
  | `fetch/schema.ts` | Params/output schema, `resolveRequest()` |
413
- | `fetch/page.ts` | Fetch + redirect-following + content-type routing; PDF extraction is injected via `FetchDeps` from the shared `lib/pdf.ts` |
411
+ | `fetch/page.ts` | Fetch + redirect-following + content-type routing; PDF extraction is injected via `FetchDeps` from the shared `lib/pdf.ts`. Loaded by the tool's execute, so a session that never fetches pays nothing |
414
412
  | `fetch/structured.ts` | JSON/CSV/XML body → readable text, or `undefined` to keep the raw body |
415
413
  | `fetch/ssrf.ts` | URL/host validation and private-address blocking |
416
414
  | `fetch/extract.ts` | HTML → title + readable text |
417
- | `fetch/find.ts` | Passage search (exact/insensitive/fuzzy) |
415
+ | `fetch/find.ts` | Passage search (exact/insensitive/fuzzy); loaded by the tool's execute |
418
416
  | `fetch/cache.ts` | Per-registration LRU/TTL page cache (`createPageCache`) |
419
417
  | `fetch/format.ts`, `fetch/types.ts` | Result text and data types |
420
418
 
@@ -8,8 +8,6 @@ subprocess per peer.
8
8
 
9
9
  ## Capabilities
10
10
 
11
- Status records what a reader can rely on: **Shipped** is in a tagged release, **Unreleased** is complete in the current tree but not yet in a tagged release, and **Partial** has one released part and one unreleased part. Rows are ordered by user value, highest first.
12
-
13
11
  ### Conversation
14
12
 
15
13
  | Capability | How | What you get | Status |
@@ -28,22 +26,22 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
28
26
 
29
27
  ### Remote control
30
28
 
31
- | Capability | How | What you get | Status |
32
- |---|---|---|---|
33
- | Plan review over chat | automatic: an `exit_plan_mode` on a WeChat turn | The plan arrives as a file, then one compact editor prompt with three actions. | Shipped |
34
- | Forwarded dialogs | `remotePrompts` | A dialog raised inside a WeChat turn is asked and answered in that peer's chat. | Shipped |
35
- | Peer questions | `ask_user_question` | The peer answers their own question, numbered, and a reply cannot reach another peer's prompt. | Shipped |
36
- | Control screen | `/wechat` | Account, connection, per-peer rows, activity, and any waiting prompt on one live screen. | Shipped |
37
- | Prompt cancellation | `/wechat`, `c` on the control screen | One waiting question is withdrawn with a notice, without stopping the peer's turn. | Shipped |
38
- | Peer steering | `/wechat stop <peer>`, `/wechat reset <peer>` | One peer's running turn stops, or their conversation starts fresh. | Shipped |
29
+ | Capability | How | What you get |
30
+ |---|---|---|
31
+ | Plan review over chat | automatic: a `plan` action `exit` on a WeChat turn | The plan arrives as a file, then one compact editor prompt with three actions. |
32
+ | Forwarded dialogs | `remotePrompts` | A dialog raised inside a WeChat turn is asked and answered in that peer's chat. |
33
+ | Peer questions | `ask_user_question` | The peer answers their own question, numbered, and a reply cannot reach another peer's prompt. |
34
+ | Control screen | `/wechat` | Account, connection, per-peer rows, activity, and any waiting prompt on one live screen. |
35
+ | Prompt cancellation | `/wechat`, `c` on the control screen | One waiting question is withdrawn with a notice, without stopping the peer's turn. |
36
+ | Peer steering | `/wechat stop <peer>`, `/wechat reset <peer>` | One peer's running turn stops, or their conversation starts fresh. |
39
37
 
40
38
  ### Security and admission
41
39
 
42
- | Capability | How | What you get | Status |
43
- |---|---|---|---|
44
- | Owner-only admission | `allowedPeers`, `/wechat allow [peer]` | Only the owner who scanned the QR drives the agent until another sender is named. | Shipped |
45
- | Per-peer tool allowlist | `peers[<id>].tools` | An extra admitted peer can be narrowed to named tools; an empty list leaves none. | Shipped |
46
- | One poller per account | `poller.lock` | A second `pi` process is refused instead of stealing the account's messages. | Shipped |
40
+ | Capability | How | What you get |
41
+ |---|---|---|
42
+ | Owner-only admission | `allowedPeers`, `/wechat allow [peer]` | Only the owner who scanned the QR drives the agent until another sender is named. |
43
+ | Per-peer tool allowlist | `peers[<id>].tools` | An extra admitted peer can be narrowed to named tools; an empty list leaves none. |
44
+ | One poller per account | `poller.lock` | A second `pi` process is refused instead of stealing the account's messages. |
47
45
 
48
46
  ### Delivery and reliability
49
47
 
@@ -57,23 +55,23 @@ Status records what a reader can rely on: **Shipped** is in a tagged release, **
57
55
 
58
56
  ### Pairing and transport
59
57
 
60
- | Capability | How | What you get | Status |
61
- |---|---|---|---|
62
- | QR login | `/wechat login` | One scan stores the account and the bridge starts polling it, replacing a running poller. | Shipped |
63
- | Reconnect after expiry | `/wechat login`, `l` on the control screen | A fresh login clears the expired chip and starts a new worker instead of leaving a dead token. | Shipped |
64
- | Automatic start | `session_start`, a stored account | The bridge runs whenever credentials exist: the account is the on/off switch, so there is no start command. | Shipped |
65
- | Pause and resume | `/wechat pause`, `/wechat resume` | Polling stops while running turns finish, the lock is released, and resume takes it back. | Shipped |
66
- | Status chip | `/wechat status` | The status bar names the state — `✆ wechat`, `✆ wechat paused`, or a persistent `✆ wechat expired`. | Shipped |
67
- | Logout | `/wechat logout` | One confirmation discards the credentials and stops polling. | Shipped |
58
+ | Capability | How | What you get |
59
+ |---|---|---|
60
+ | QR login | `/wechat login` | One scan stores the account and the bridge starts polling it, replacing a running poller. |
61
+ | Reconnect after expiry | `/wechat login`, `l` on the control screen | A fresh login clears the expired chip and starts a new worker instead of leaving a dead token. |
62
+ | Automatic start | `session_start`, a stored account | The bridge runs whenever credentials exist: the account is the on/off switch, so there is no start command. |
63
+ | Pause and resume | `/wechat pause`, `/wechat resume` | Polling stops while running turns finish, the lock is released, and resume takes it back. |
64
+ | Status chip | `/wechat status` | The status bar names the state — `✆ wechat`, `✆ wechat paused`, or a persistent `✆ wechat expired`. |
65
+ | Logout | `/wechat logout` | One confirmation discards the credentials and stops polling. |
68
66
 
69
67
  ### Upkeep
70
68
 
71
- | Capability | How | What you get | Status |
72
- |---|---|---|---|
73
- | Health report | `/wechat doctor` | Connection, token age, last poll, lock holder, per-peer queues, and recent failures in one read-only report. | Shipped |
74
- | Idle session cleanup | automatic: the 30-minute idle sweep | An idle peer's Pi session is disposed, so idle conversations do not keep costing context or memory. | Shipped |
75
- | Peer history | `/wechat history <peer> [n]` | That peer's recent messages, ten by default and twenty at most, without a live session. | Shipped |
76
- | Command help | `/wechat help` | Every subcommand with a line each, printed in the transcript. | Shipped |
69
+ | Capability | How | What you get |
70
+ |---|---|---|
71
+ | Health report | `/wechat doctor` | Connection, token age, last poll, lock holder, per-peer queues, and recent failures in one read-only report. |
72
+ | Idle session cleanup | automatic: the 30-minute idle sweep | An idle peer's Pi session is disposed, so idle conversations do not keep costing context or memory. |
73
+ | Peer history | `/wechat history <peer> [n]` | That peer's recent messages, ten by default and twenty at most, without a live session. |
74
+ | Command help | `/wechat help` | Every subcommand with a line each, printed in the transcript. |
77
75
 
78
76
  ## Quickstart
79
77
 
@@ -383,7 +381,7 @@ plan file is sent before one compact editor prompt with the title, first steps,
383
381
  mode status, and three actions. Reply 2 to keep planning or 3 to exit; any other
384
382
  text is a refinement. If the upload fails, the plan text is sent instead. The
385
383
  plan file stays on disk. After a Keep decision the user can say “exit plan mode”; the model
386
- calls `cancel_plan_mode`, which needs no plan path and records no todo or goal.
384
+ calls `plan` action `cancel`, which needs no plan path and records no todo or goal.
387
385
  The flow mirrors the
388
386
  reference client: `getuploadurl` → AES-128-ECB encrypt → `POST` the ciphertext
389
387
  (`Content-Type: application/octet-stream`) → read `x-encrypted-param` → send a
@@ -415,7 +413,7 @@ The same run answers a second question: every message it receives is printed wit
415
413
  the `group_id` and `session_id` the protocol supplied, when it supplied them.
416
414
  Nothing in the bridge routes on either — a conversation is keyed by sender — so a
417
415
  message posted in a group is the only way to learn whether rooms are delivered at
418
- all, which is what the roadmap's group-chat item turns on.
416
+ all, a question a group-chat design would have to answer first.
419
417
 
420
418
  ## Troubleshooting
421
419
 
@@ -520,7 +518,7 @@ in-process).
520
518
  | Peer dialogs | `bindExtensions({ mode: "rpc", uiContext })` on each peer session, with `createPeerUIContext(channel)` routing `select`/`confirm`/`input`/`editor` to that peer and marking the context for `askHuman`'s file/text preface. The terminal's shared `ctx.ui` is deliberately not adapted: a host turn is never answered remotely, so its dialogs stay local even while a peer turn runs. |
521
519
  | Status | The bridge's connection state (`running` / `paused` / `expired` / `stopped`) is published through `onStateChange` to `ctx.ui.setStatus(STATUS_KEYS.wechat, …)`; an expiry keeps a persistent error chip, pause keeps a warning chip, and the status bar renders it on line 2. |
522
520
  | State | Files under `<agent-dir>/wechat/`; the bridge is not authoritative UI, so nothing is stored in the transcript. |
523
- | Lifecycle | The factory only registers. The poll loop starts on `session_start` when an account is stored (and after `/wechat login`); `session_shutdown` closes it. One bridge per extension runtime: it is built once, `open()` is a no-op while running or paused (so the RPC double `session_start` on a session switch cannot start a second poller or resume a paused one), and shutdown is idempotent, so the reload and replacement paths (`/reload`, `/new`, resume, fork — all of which re-run the factory and emit shutdown before the new start) each end with exactly one poller. `pause()` stops the loop and releases the lock while keeping the worker, so a running turn can finish; `resume()` reacquires the lock and starts a new loop. Pause is runtime-local, so a reload or restart starts a fresh running bridge. `/wechat` screens use `ctx.ui.custom`, so they degrade (status) or decline (login, logout) while a peer turn runs. |
521
+ | Lifecycle | The factory only registers, and the bridge module itself loads on that start: the import is deferred behind the account check, so a session with no account never evaluates the bridge, the peer worker, the transport or the lock. The poll loop starts on `session_start` when an account is stored (and after `/wechat login`); `session_shutdown` closes it. One bridge per extension runtime: it is built once, `open()` is a no-op while running or paused (so the RPC double `session_start` on a session switch cannot start a second poller or resume a paused one), and shutdown is idempotent, so the reload and replacement paths (`/reload`, `/new`, resume, fork — all of which re-run the factory and emit shutdown before the new start) each end with exactly one poller. `pause()` stops the loop and releases the lock while keeping the worker, so a running turn can finish; `resume()` reacquires the lock and starts a new loop. Pause is runtime-local, so a reload or restart starts a fresh running bridge. `/wechat` screens use `ctx.ui.custom`, so they degrade (status) or decline (login, logout) while a peer turn runs. |
524
522
 
525
523
  ## Design notes
526
524
 
@@ -594,7 +592,7 @@ status, which is what the old start/stop guidance used to do.
594
592
  | File | Responsibility |
595
593
  |---|---|
596
594
  | `index.ts` | Factory: gate, register the command and tool, wire the lifecycle events, build the bridge, open peer sessions from the host context. |
597
- | `bridge.ts` | The composition root: wires the poll loop, delivery, presence, admission, and inbound modules to the live client/account/cursor/state; owns the connection lifecycle, the status snapshot, and `sendToOwner`. |
595
+ | `bridge.ts` | The composition root: wires the poll loop, delivery, presence, admission, and inbound modules to the live client/account/cursor/state; owns the connection lifecycle, the status snapshot, and `sendToOwner`. Loaded on `session_start` when an account is stored, never at registration, so a session with no account evaluates none of it. |
598
596
  | `intake.ts` | The inbound path: who is refused, what is a replay, what is a command, what answers a pending prompt, and what is queued or turned away; the admission predicates live here too. |
599
597
  | `tools.ts` | The `send_wechat` tool: owner-only proactive text or one image, with the neither-or-both refusal. |
600
598
  | `config.ts` | The `wechat` section of `bytefly.json` loading and normalization. |
@@ -608,7 +606,7 @@ status, which is what the old start/stop guidance used to do.
608
606
  | `transport/inbound.ts` | Inbound media resolution (download, decrypt, save, attach) and the hand-off to a peer's session. |
609
607
  | `transport/media.ts` | Media key/encrypt/decrypt, padded size, upload/download URL, image MIME sniff (undefined when the bytes are no image), filename sanitize, and save helpers. |
610
608
  | `state/state.ts` | `state.json` (cursor, owner, per-peer tokens, recent message ids). |
611
- | `state/credentials.ts` | `credentials.json` (`0600`, every stored account and the active one). |
609
+ | `state/credentials.ts` | `<agent-dir>/wechat/` and its `credentials.json` (`0600`, every stored account and the active one). |
612
610
  | `state/lock.ts` | Poller lockfile. |
613
611
  | `peer/peer-session.ts` | One SDK `AgentSession` per peer: workspace, model, history under the state dir, dialogs bound to that peer, this bridge excluded from its resources, and the channel prompt appended. |
614
612
  | `peer/peer-worker.ts` | Per-peer queue, two peers' turns at once (`PEER_CONCURRENCY`), the fair slot hand-off, prompt/abort/reset, streamed reply parts, idle-session sweep, and the notice an abandoning close sends. |
@@ -628,7 +626,8 @@ status, which is what the old start/stop guidance used to do.
628
626
  | `dialog/prompt.ts` | Pure prompt formatting and reply parsing. |
629
627
  | `dialog/remote-ui.ts` | The peer session's UI context: dialogs and notices bound to that peer, the `askHuman` marker that delivers the file/text preface, and the `remotePrompts` switch. |
630
628
  | `view/login-screen.ts` | The `/wechat login` screen (`ctx.ui.custom`), drawn from the `lib/tui/` kit (`titleRule`, `clip`, `isCloseKey`). |
631
- | `view/status-screen.ts` | The `/wechat` control screen (`ctx.ui.custom`): live facts, a focusable peer list with stop/reset/history keys, and the pure one-line/body renderers, drawn from the `lib/tui/` kit (`titleRule`, `keyValueRow`, `listRow`, `paint`, `IdCursor`). |
629
+ | `text/status.ts` | The status facts (`statusView`) and the one-line summary (`statusLine`) the text modes print. |
630
+ | `view/status-screen.ts` | The `/wechat` control screen (`ctx.ui.custom`): live facts, a focusable peer list with stop/reset/history keys and the body renderer, loaded on first use, drawn from the `lib/tui/` kit (`titleRule`, `keyValueRow`, `listRow`, `paint`, `IdCursor`). |
632
631
 
633
632
  Modules are grouped by the role they play: `transport/` is the only place that talks to iLink, `state/` owns the three files a session leaves on disk, `peer/` runs one peer's session and its queue, `commands/` holds a slash verb, `text/` words everything a peer reads, `dialog/` asks a question over WeChat, and `view/` draws a full-screen surface. `index.ts`, `bridge.ts`, `tools.ts`, `config.ts`, `types.ts`, `errors.ts`, and `timer.ts` stay at the root — the factory, the composition root, the public surface, and the seams the directories import.
634
633