@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,22 +1,69 @@
1
1
  # board — one shared feature board per repository
2
2
 
3
3
  Every Pi session on a repository works the same cards through the tools, and a repository
4
- has **one** page that shows them: the session that starts it serves it under the seat
5
- `team-lead`, and every other session opens that same page instead of a second one. The
4
+ has **one** page that shows them: the session that starts it serves it under the seat —
5
+ the board's lead, `board.lead`, `lead` by default — and every other session opens that same
6
+ page instead of a second one. The
6
7
  page is a **view** — it cannot change a card — so what a browser shows and what the
7
8
  terminal holds cannot disagree. A card is one **feature**: its outcome, who holds it,
8
- where the work happens, the plan it is executing, how urgent it is, what it waits for,
9
- and the notes that explain how it got there.
9
+ where the work happens, the contract it promises, the plan it is executed against, how
10
+ urgent it is, what it waits for, and the notes that explain how it got there.
11
+
12
+ A card moves through **five lanes**, and the flow is a pull one: `backlog` is the pool
13
+ nobody holds, `plan` is where the plan is designed and the contract written and signed,
14
+ `execute` is where the work happens, `blocked` is a card waiting on a person, and `done` is
15
+ closed. The order is the board's, and each call can only follow the one before it:
16
+
17
+ ```
18
+ board claim 4 the card is yours, in plan
19
+ /plan (plan mode on) design it, plan write saves the artifact
20
+ plan exit the user approves the plan
21
+ board contract 4 path … goal, acceptance, scopeOut, and that file — it must exist
22
+ board approve 4 or /board approve 4 as the person; the holder may never sign
23
+ board move 4 execute the plan is re-checked, then the work starts
24
+ board review 4 or /board review 4, before it can be called done
25
+ board move 4 done
26
+ ```
27
+
28
+ Two gates are humans'. A card cannot enter `execute` until **the lead** — `board.lead` in
29
+ `bytefly.json`, `lead` by default — has approved the contract, and cannot be
30
+ finished until the lead has spot-checked the work. The lead is a team name, so it signs as a session that
31
+ joined under it, and never while holding the card: a lead releases a card before signing it.
32
+ The person at the keyboard is the other way in — `/board approve` and `/board review` write
33
+ as `you` — which is what makes a one-session flow possible, and the reason a repository
34
+ nobody configured can still finish work. A
35
+ card cannot skip the artifact either: `contract` requires the plan file `plan` action `write`
36
+ returned, inside this repository's plan store, and a move to `execute` or `done` re-checks
37
+ that the file is still there. `plan` and `execute` are held to a WIP limit, so the board
38
+ says "make room" instead of quietly accumulating work nobody is reviewing.
10
39
 
11
40
  The steps of the work are deliberately *not* on the board. They live in the
12
- owner's plan and in `todo`; the board answers who is doing what, and what needs a
41
+ owner's plan and in `todo`; the board answers who is holding what, and what needs a
13
42
  decision.
14
43
 
15
- <img src="../../assets/screens/board.png" alt="The board page: a toolbar with the seat the page is served as, a live badge, Board and Team tabs, done-of-total progress, a filter, the keys button and a theme toggle; four lane trays, each with a dot, a count and a fold control; and cards carrying an owner chip, path hints and a reason line, with no button on any of them" width="100%">
44
+ The board is **opt-in**. Its tool, its command, and its page belong to a workflow a
45
+ session either runs or does not, so nothing is registered until the session asks for
46
+ it:
47
+
48
+ ```bash
49
+ PI_ENABLED_EXTENSIONS=board pi
50
+ ```
16
51
 
17
- *`/board`: the four lanes, with the round one tab away and every card opened by its title.*
52
+ `team` — the lead's half of the same workflow, and the only extension that can hand a
53
+ card to a peer — is opt-in beside it, so a lead names both:
54
+ `PI_ENABLED_EXTENSIONS=board,team`. They stay independent: one name never loads the
55
+ other.
18
56
 
19
- <img src="../../assets/screens/card-dialog.png" alt="The card dialog on one doing card: the id and title with the lane as a dot and a word, a head/value ladder reading owner, updated, in lane, last edited, plan and paths, each value on one line, then a Notes section whose sentences are attributed under them" width="100%">
57
+ The extension is still declared in the package manifest, so Pi discovers it and
58
+ documents its surface with the rest; the gate is the variable, in `lib/env.ts` beside
59
+ `PI_DISABLED_EXTENSIONS`, which still wins when it names `board`. `doctor` reports the
60
+ row as an opt-in extension and names the switch.
61
+
62
+ <img src="../../assets/screens/board.png" alt="The board page: a toolbar with the repository's name and a live badge, Board and Team tabs, done-of-total progress, a filter, the keys button and a theme toggle; five lane trays, each with a dot, a count and a fold control; and cards carrying an owner chip, a note count and its age, path hints, a line naming what the card waits for, a line naming the cards it holds up, and a reason line, with no button on any of them" width="100%">
63
+
64
+ *`/board`: the five lanes, with the round one tab away and every card opened by its title.*
65
+
66
+ <img src="../../assets/screens/card-dialog.png" alt="The card dialog on one executing card: the id and title with the lane as a dot and a word, a head/value ladder reading owner, updated, in lane, last edited, plan, goal, acceptance, scope out, approved, reviewed and paths, each value on one line, then a Notes section whose sentences are attributed under them" width="100%">
20
67
 
21
68
  *The card dialog: the record of one card — a ladder of facts, then its notes at body size
22
69
  (`screenshot:board --dialog card`).*
@@ -25,30 +72,32 @@ decision.
25
72
 
26
73
  | Capability | How | What you get |
27
74
  |---|---|---|
28
- | A shared feature list for one repository | `board` action `list` | Every open card in lane order — `todo`, `doing`, `blocked` — or one card, or one lane; the `done` cards are left out with a count of how many, and `status: "done"` reads them |
29
- | The lead's rollup | `board` action `list` with `round: true` | Every owner with its live cards and, beside the count, what it has finished — an owner whose last card is done stays listed — then unassigned, unplanned, blocked, idle, and done |
30
- | Publish a feature | `board` action `add` | A `todo` card with path hints and a first note |
31
- | Take a feature | `board` action `claim` | The card is `doing` and owned by this session |
32
- | Hand it back | `board` action `release` | The card is `todo` again, with no owner |
33
- | Change lane | `board` action `move` | Wherever the owner moves it: `doing`, `blocked`, `done` |
34
- | Record what happened | `board` action `note` | A note appended to the card's history, attributed and stamped |
35
- | Record the plan | `board` action `plan` | The plan file the card is executing, so "unplanned" work is visible |
75
+ | A shared feature list for one repository | `board` action `list` | Every open card in lane order — `backlog`, `plan`, `execute`, `blocked` — or one card, or one lane; the `done` cards are left out with a count of how many, and `lane: "done"` reads them |
76
+ | The lead's rollup | `board` action `list` with `round: true` | Every owner with its live cards and, beside the count, what it has finished — an owner whose last card is done stays listed, and the owners a bus has no record of and that hold nothing are one line, `18 owners with no record · 76 done` — then unassigned (the pool), unplanned (a contract still missing a line, or a plan never recorded — either one refuses `execute`), awaiting approval, blocked, idle, and done |
77
+ | Publish a feature | `board` action `add` | A `backlog` card with path hints and a first note (one line, at most 1000 characters) |
78
+ | Take a feature | `board` action `claim` | The card is pulled into `plan` and owned by this session — or refused, naming the count, when `plan` is full. The result names the next step, because the claim is what commits the session to a plan |
79
+ | Hand it back | `board` action `release` | The card is back in `backlog` with no owner; the contract and its approval stay, because the plan did not change |
80
+ | Write the contract | `board` action `contract` | The plan file and the three lines the card promises — `goal`, `acceptance`, `scopeOut` — written in one call; `path` is required, and it must be the plan `plan` action `write` saved in this repository's plan store, so a contract cannot name a file that does not exist. Any change to a line or to the file drops the approval, because the sign named both |
81
+ | Approve the contract | `board` action `approve`, or `/board approve <id>` | The lead records that the wording is right — a session that joined under `board.lead`, and never the holder — or the person at the keyboard signs; only then may the card enter `execute`. `text` adds a note, recorded with the sign, because a signer is exactly the session `note` refuses on a held card |
82
+ | Spot-check the work | `board` action `review`, or `/board review <id>` | The lead records that they looked at the finished work in `execute` — never the holder — or the person does; only then may the card be finished. `text` adds a note, recorded with the sign, so "checked, and this is still wrong" is one call |
83
+ | Change lane | `board` action `move` | Wherever the board allows from where the card is: `plan` to `execute` (no unfinished dependency, a complete contract, a plan that is still in the store, an approval by somebody other than the holder) or `blocked`, `execute` to `plan` (re-plan), `blocked`, or `done` (no unfinished dependency, spot-checked, the same plan check), `blocked` back to `plan` when that lane has room, or to `execute` once its contract is approved and its plan is there |
84
+ | Record what happened | `board` action `note` | A note appended to the card's history, attributed and stamped — one line, at most 1000 characters; its holder writes it, or anyone while nobody holds the card, the way `edit` works |
36
85
  | Fix what a card says | `board` action `edit` | A new title, path hints, priority, or the cards it waits for — by its holder, or by anyone while nobody holds it |
37
86
  | Say how urgent it is | `board` action `add` or `edit` with `priority` | `high` sorts first inside its lane, and the card says so |
38
- | Record what a card waits for | `board` action `edit` with `dependsOn` | An unfinished dependency refuses a claim and a move to `doing` or `done`, and the reason line names it; the card it waits on answers the other half — `list` on it names the cards waiting on it, and its dialog row `waited on by` says the same |
39
- | Hand a card to a teammate | `team` action `assign` | The owner is recorded **and** a handoff is sent to that session's inbox. The page cannot do this: a browser has no way to message a peer, so a page assignment that recorded an owner and told nobody is gone with the write path |
40
- | Take a card back from the tool | `board` action `reclaim` | The owner clears and the card returns to To do, without the page and without `team` |
87
+ | Record what a card waits for | `board` action `edit` with `dependsOn` | An unfinished dependency refuses a claim and a move to `execute` or `done`, and the reason line names it; the card it waits on answers the other half — `list` on it names the cards waiting on it, and its dialog row `waited on by` says the same |
88
+ | Hand a card to a teammate | `team` action `assign` | The owner is recorded **and** a handoff is sent to that session's inbox; a pool card comes with `plan`, and a card somebody already holds is refused — `reclaim` it, then assign. The page cannot do this: a browser has no way to message a peer, so a page assignment that recorded an owner and told nobody is gone with the write path |
89
+ | Take a card back from the tool | `board` action `reclaim` | The owner clears and the card returns to `backlog`, without the page and without `team` |
41
90
  | Find a card without reading every lane | `board` action `list` with `query` | The cards whose title, holder, path hint, plan, or note text matches — the same rule the page's filter uses, from one body (`MATCHES_QUERY_JS`) |
42
91
  | See the work you hold | `board` action `list` with `mine` | The cards this session holds, by the same rule a claim is checked against |
43
- | Set a finished card aside | `board` action `archive` | A `done` card's file moves into an `archive/` pile beside the board with its notes, plan, owner, and landed commit still readable, and the board stops reading it: the card named by `card`, or every finished card when none is named. It is a decision a person makes, never a threshold in the background, and nothing un-archives |
44
- | Delete a card | `board` action `drop` | The card and its notes are gone, and its id is spent: a record of the title, the lane, who ended it, and when stays in `ended/`, so a note or a handoff that names that id cannot come to mean a different feature. A `todo` or `done` card is anyone's to drop, an archived one included, one in flight only its holder's, and one that a card waits on cannot leave at all |
45
- | One page per repository | `/board` | Serves a loopback page as `team-lead` with a **Board** and a **Team** tab, following the store live; a page already served is opened instead |
46
- | Watch somebody else's page | `/board` on a session that is not serving | The same page, because there is one: every page is a read view, so a second session gets exactly what the first one sees instead of a writable rival. It joins the team under its own label, so a handoff can still reach it |
47
- | Read a board at its real size | the `Done` fold, an empty lane's rail | `Done` starts folded at six cards or more, its count in the head, and a reader's toggle decides from then on. A lane holding no cards narrows to its head, word, and count, so the lanes carrying work keep the width |
48
- | Open a card | the card's title, or `Enter` on a focused one | The title is the only door, because it is the only one needed: it opens the dialog holding the card whole — its lane, its owner, its paths, what it waits for, its plan, and every note |
92
+ | Set a finished card aside | `board` action `archive` | A `done` card's file moves into an `archive/` pile beside the board with its notes, plan, contract, owner, and landed commit still readable, and the board stops reading it: the card named by `card`, or every finished card when none is named. A card that already names it keeps that link — it is inert, because a finished dependency stops blocking — though a new link has to name a card on the board. It is a decision a person makes, never a threshold in the background, and nothing un-archives |
93
+ | Delete a card | `board` action `drop` | The card and its notes are gone, and its id is spent: a record of the title, the lane, who ended it, and when stays in `ended/`, so a note or a handoff that names that id cannot come to mean a different feature. A `backlog` or `done` card is anyone's to drop, an archived one included, one in `plan`, `execute`, or `blocked` only its holder's, and one that a card waits on cannot leave at all |
94
+ | One page per repository | `/board` | Serves a loopback page as the board's lead with a **Board** and a **Team** tab, following the store live; a page already served is opened instead |
95
+ | Watch somebody else's page | `/board` on a session that is not serving | The same page, because there is one: every page is a read view, so a second session gets exactly what the first one sees instead of a writable rival. It joins the team under its own name, so a handoff can still reach it |
96
+ | Read a board at its real size | the `Done` fold, an empty lane's rail, the Team tab's own folds | `Done` starts folded at six cards or more, its count in the head, and a reader's toggle decides from then on. A lane holding no cards narrows to its head, word, and count, and a lane of finished cards stops widening at three, so the lanes carrying work keep the width. On the Team tab the queue and the roster share one cap, so both stay on one screen, and the ids past the strip's sixth fold behind its count |
97
+ | Open a card | the card, or `Enter` on a focused one | The whole card is the door — its title stays the focusable button — and it opens the dialog holding the card whole: its lane, its owner, its paths, what it waits for, its plan, and every note. A drag across a card's prose selects it instead of opening it |
49
98
  | See the lead's round at a glance | `/board team`, or `/team` with no browser | A queue of cards that need a decision, each with the reason and its way out, then who is on the team and what they hold |
50
- | Join the team | `/board`, or any card you work | Opening the page joins this session to the team — as `team-lead` when it serves the page, under its own name when it opens one somebody else serves — and the first card a tool writes joins it too, so nothing has to be armed by hand. `/board status` says which team a `/board` in this session put you on |
51
- | See who is around | `/board team` | The Teammates list: every session this repository's bus has recorded, plus every owner a card names, each stating `live`, `quiet`, `no record`, or `—` when no bus is loaded |
99
+ | Join the team | `/board`, or the first card you claim | Opening the page joins this session to the team — as the lead's name when it serves the page, under the name it already holds when it opens one somebody else serves — and so does the first card it claims, so nothing has to be armed by hand. Joining never renames Pi's session: the footer keeps the name you gave it, and a `/team join` name is kept rather than replaced by the id-derived one. `/board status` reports the team this session is on, whichever door it used |
100
+ | See who is around | `/board team` | The Teammates list: every session this repository's bus has recorded, plus every owner a card names, each stating `live`, `quiet`, `no record`, or `—` when no bus is loaded. An owner the bus has no record of and that holds nothing is one row stating how many there are and what they finished, unfolded by a click — the list only grows, because a label that ever claimed a card stays an owner — while one that still holds a card keeps its own row |
52
101
 
53
102
  ## Quickstart
54
103
 
@@ -57,21 +106,31 @@ you: publish two features for the billing work
57
106
  → board add "Billing webhook accepts orders" paths ["src/billing"]
58
107
  → board add "Retry branches merge cleanly"
59
108
  lead: board edit 2 dependsOn ["1"] priority "high"
60
- → #2 waits on #1 (todo), and sorts first in To do
61
- peer: claim the first, plan it, note the phases, finish it
62
- → board claim 1 → /plan → board plan 1 <plan file> → board note 1 … → board move 1 done
109
+ → #2 waits on #1 (backlog), and sorts first in Backlog
110
+ peer: pull the first, design it, then write down what it promises
111
+ → board claim 1
112
+ → next: design it in plan mode, `plan` action `write`, then `contract` with the path it returned
113
+ → /plan … plan write … plan exit (the user reads the plan and approves it)
114
+ → board contract 1 path <the file `plan` action `write` returned> goal … acceptance … scopeOut …
115
+ → board move 1 execute — refused: the contract is not approved yet
116
+ lead: board approve 1 (/team join lead first; else /board approve 1)
117
+ peer: board move 1 execute → board note 1 …
118
+ lead: board review 1 (or the person: /board review 1)
119
+ peer: board move 1 done
63
120
  → board claim 2 — refused while #1 is unfinished, allowed once it is done
64
121
  lead: board list round: true
65
- → 1 owner · 2 features · 0 unassigned · 1 unplanned
122
+ → 1 owner · 2 features · 0 unassigned · 0 unplanned · 0 awaiting approval
66
123
  ```
67
124
 
68
125
  ## Commands
69
126
 
70
127
  | Command | Who | Effect |
71
128
  |---|---|---|
72
- | `/board` | lead | Serves the page as `team-lead`, or opens the one another session already serves — and opens it again when it is already up, so a closed tab is one command away. The line it prints names the address |
129
+ | `/board` | the lead | Serves the page as the board's lead (`board.lead`, `lead` by default), or opens the one another session already serves — and opens it again when it is already up, so a closed tab is one command away. The line it prints names the repository and the address |
73
130
  | `/board team` | lead | The same, opened on the Team tab |
74
- | `/board status` | both | One line: the seat, the port, how many tabs are watching, the store, and the team the last `/board` joined — or the page being watched, and whether the note behind it still answers |
131
+ | `/board approve <id>` | the person | Signs the card's contract as `you`, the other actor the gate takes: a session must be the lead (`board.lead`), and the holder may sign nothing at all. Only a card in `plan` with a complete contract can be signed, and the refusal is the tool's own sentence |
132
+ | `/board review <id>` | the person | Records the spot-check of finished work as `you`; only a card in `execute` can be signed |
133
+ | `/board status` | both | One line: the seat, the repository, the port, how many tabs are watching, the store's path, and the team the last `/board` joined — or the page being watched, and whether the note behind it still answers |
75
134
  | `/board stop` | lead | Stops the page this session serves; a watched page that has stopped answering is dropped instead, and a watched page that is alive is its owner's to stop |
76
135
  | `/team` | both | The round in the terminal: the rollup, the work a dead owner left behind, then who is live, who has gone quiet, and who is gone |
77
136
 
@@ -87,18 +146,50 @@ one a crash left. `/board status` probes the note it is watching, and names one
87
146
  answers nobody; `/board stop` then drops it (a token-safe clear, so a page that came back
88
147
  keeps its own note) instead of waiting for the next `/board` to trip over it.
89
148
 
149
+ **Who signs.** The two gates take the lead's signature and nothing else: `approve` and
150
+ `review` accept a session only while its team name is `board.lead`, and never while that
151
+ session holds the card — a lead releases a card before signing it, so the gate is never its
152
+ own. **The page serves as `board.lead`** (`lead` by default), so the session that opens
153
+ `/board` takes that name and can sign — the shipped board has a signer without being
154
+ configured. A repository that wants its own name sets `{"board": {"lead": "gavin"}}`, and the
155
+ page, the roster, and the gate follow it. It is a label check and nothing more, which is the
156
+ half of "somebody read this" a file can hold.
157
+
158
+ **The person's door.** A repository worked by one session would otherwise never get a card
159
+ past `plan` or `execute`, so `/board approve` and `/board review` write the same two signs as
160
+ `you` — a label, with no session id — whatever the lead is called and whether or not the
161
+ session typing it holds the card. The card says who signed, and the two read differently
162
+ because they are different: `you` is the person who read the plan, the lead is a second pair
163
+ of eyes.
164
+
90
165
  **Joining.** One thing, done for you: `/board` puts this session on the team — as
91
- `team-lead` when it serves the page, under its own name when it watches one — and so does
92
- the first card you work. The join is also the listening: mail is reported at that
93
- session's next turn, and no turn is raised, so being on the team never spends a turn. A
94
- session that has neither opened the board nor touched a card is not on it. `/board
95
- status` reports the join `/board` made itself: a session that joined by writing a card
96
- is on the team, and its status line does not say so.
97
-
98
- The page still serves as a seat: `/board status` names `team-lead`, the toolbar chip says
99
- so, and a card a browser claimed **before** the page stopped writing still names
100
- `board:team-lead` as its owner — `reclaim` is how a lead takes that back from the
101
- terminal.
166
+ the lead's name when it serves the page, under its own name when it watches one — and so does
167
+ the first card it claims. A read, a new card, a note, a plan, or an edit enlists nobody: a
168
+ teammate is a session that holds work. The join is also the listening: mail is reported at
169
+ that session's next turn, and no turn is raised, so being on the team never spends a turn.
170
+ A session that has neither opened the board nor claimed a card is not on it.
171
+
172
+ **The two names.** A session's team name is the seat — the board's lead — while it serves the
173
+ page, which is also what makes it the lead unless `board.lead` names someone else; otherwise
174
+ it is the name the session already holds: `/team join billing-core` names
175
+ it, and the cards it claims, the notes it writes, and the join a claim makes all say
176
+ `billing-core` rather than a name derived past it. A session the bus has no record of
177
+ takes a preset teammate name — `tom`, `lily`, `ben` — drawn from its id and walked past the
178
+ names live teammates hold, so `billing-core` before it names itself is `lily` to the team;
179
+ once every preset name is held it is `team-agent-` plus the last six characters of its id
180
+ (`team-agent-9f21ac`). Until it joins, that name is only a draw: a live peer that takes it
181
+ moves the session on to the next free one, and a card written before then keeps the name it
182
+ was written with. The name is never Pi's session name, which means a rename cannot strand a
183
+ card and the bus will not let two live sessions settle on one name, while nothing here ever
184
+ renames your session: `/team join <label>` is the one door that does. Ownership is the id,
185
+ so a name that changes leaves the card where it was. A card records the name as its owner,
186
+ so the chip, the roster, and the bus record are one word — and `/board status` says it
187
+ whichever door put this session on the team, including a claim made with no page open.
188
+
189
+ The page still serves as a seat: `/board status` names the lead, the Team tab marks that
190
+ row `you`, and a card a browser claimed **before** the page stopped writing still names
191
+ `board:<lead>` as its owner — `reclaim` is how a lead takes that back from the terminal. The toolbar does not print the seat: a board's subject is its repository, so the
192
+ name there is the project's, and the seat is read off the roster where it belongs.
102
193
 
103
194
  ## Tool
104
195
 
@@ -107,20 +198,25 @@ terminal.
107
198
 
108
199
  | Action | Parameters | Result |
109
200
  |---|---|---|
110
- | `list` | `status?`, `mine?`, `card?`, `query?`, `round?`, `archived?` | the cards in lane order, one card, or what a query or `mine` matches — a query reads a card's title, holder, path hints, plan, and the text of its notes; a listing that names no lane leaves out the `done` cards and says how many, since a finished card is history rather than work; `round: true` adds the rollup, which always covers the whole board; `archived: true` reads the archive instead of the board, with `query` and `mine` narrowing it and `status`/`round` refused |
111
- | `add` | `title`, `paths?`, `priority?`, `note?` | the new `todo` card |
112
- | `claim` | `card` | the card, now `doing` and owned by this session |
113
- | `release` | `card` | the card, back in `todo`, keeping any block it recorded |
114
- | `reclaim` | `card` | the card, back in `todo`, whoever held it |
115
- | `move` | `card`, `status`, `blockedBy?` | the card in its new lane; `blockedBy` (one line, ≤200 characters) is what a block waits on, and a move to `done` records the commit `HEAD` resolved to, when the directory is a repository |
201
+ | `list` | `lane?`, `mine?`, `card?`, `query?`, `round?`, `archived?` | the cards in lane order, one card, or what a query or `mine` matches — a query reads a card's title, holder, path hints, plan, and the text of its notes; a listing that names no lane leaves out the `done` cards and says how many, since a finished card is history rather than work; `round: true` adds the rollup, which always covers the whole board; `archived: true` reads the archive instead of the board, with `query` and `mine` narrowing it and `lane`/`round` refused |
202
+ | `add` | `title`, `paths?`, `priority?`, `note?` | the new `backlog` card |
203
+ | `claim` | `card` | the card, pulled into `plan` and owned by this session — refused, naming the count, when `plan` is at its limit (the limit counts the whole board, not one session's cards); the result ends with the next step, which is plan mode. `team`'s `claim` writes the same operation for a lead taking work from its round |
204
+ | `release` | `card` | the card, back in `backlog` with no owner; the contract and its approval stay |
205
+ | `reclaim` | `card` | the card, back in `backlog`, whoever held it |
206
+ | `move` | `card`, `lane`, `blockedBy?`, `blocker?` | the card in its new lane, through the one transition the board allows from where it is; `blocker` (`contract`, `dependency`, or `decision`) is required for `blocked` and is recorded with `blockedBy` (one line, ≤200 characters) as the card's one `blocked` record — sent with any other lane it is refused, and it cannot change while the card is parked, because a reason is what the card was parked on; a re-plan from `execute` or a resume from `blocked` is an entry into `plan` and so is held to that lane's limit. A move to `execute` or `done` re-checks the plan the card recorded — a deleted file, or one outside the store, is refused with `contract` as the way out — and a move to `done` records the commit `HEAD` resolved to, when the directory is a repository |
116
207
  | `note` | `card`, `text` | the card with the note appended |
117
- | `plan` | `card`, `path` | the card with its plan recorded |
208
+ | `contract` | `card`, `path`, `goal?`, `acceptance?`, `scopeOut?` | the card with its plan file and its contract recorded, and any approval dropped: a changed line is an unapproved one. `path` is required and must resolve inside this repository's plan store (a bare name is read there, `~/…` is accepted) and exist; a path outside it is refused with the store named |
209
+ | `approve` | `card`, `text?` | the card with its contract approved, by the lead and not by its holder — the gate in front of `execute` (see **Who signs**); `text` is a note recorded in the same write. The person signs the same gate with `/board approve <id>` |
210
+ | `review` | `card`, `text?` | the card with a spot-check recorded, by the lead and not by its holder — the gate in front of `done`; `text` is a note recorded in the same write. The person signs with `/board review <id>` |
118
211
  | `edit` | `card`, `title?`, `paths?`, `priority?`, `dependsOn?` | the card with what it says corrected; an omitted field is left alone, and `dependsOn: []` removes every link |
119
212
  | `archive` | `card?` | the ids set aside — one finished card, or every finished card when no card is named; a card still in flight is refused |
120
213
  | `drop` | `card` | the id that was dropped, whose number is now spent |
121
214
 
122
215
  `card` takes `4` or `#4`. A refused call is an error result that still carries the
123
- card, so the next attempt starts from the truth.
216
+ card, so the next attempt starts from the truth. A `list` that names a card refuses the
217
+ fields that only narrow a listing — `lane`, `query`, `mine`, and `round` — because its
218
+ answer is that one card; `archived` is kept, because it decides which pile the card is
219
+ read from.
124
220
 
125
221
  The parameters are one flat object, so a field is published to every action at once.
126
222
  An action reads only the fields in its row above, and a call carrying one that its
@@ -154,7 +250,7 @@ path the board does not serve.
154
250
  GET /theme.js the theme script — no token
155
251
  GET /app.js the client script — no token
156
252
  GET / the document (?view=board|team), whose tab reads
157
- board · <store key> · <N> open
253
+ board · <repo> · <N> open
158
254
  GET /index.html the same document, for a link that names it
159
255
  GET /api/ping whether the page answers, and nothing else: a session
160
256
  deciding whether to attach reads this, not the snapshot
@@ -169,37 +265,54 @@ answering its browser would otherwise be declared dead and taken over.
169
265
  ## Layout
170
266
 
171
267
  One page, two tabs, one lifecycle. The kit shell keeps the toolbar and the footer
172
- pinned and makes the main pane the only scroll box; the page adds no second one.
268
+ pinned and makes the main pane the only scroll box; the page adds no second one. The
269
+ toolbar leads with the repository the page is a board of, as a quiet chip named the way the
270
+ directory holding it is, with the root's whole path on its hover — the name fits the line,
271
+ and the path is what tells two clones of one repository apart. That name arrives in the
272
+ document, so a slow frame does not leave the toolbar unnamed. The tab carries the same name
273
+ for the same reason: a store key is a hash, and no reader can say it out loud. The footer
274
+ is the board's own account: the store's path, the count of cards, and the size they occupy
275
+ — plus how many card files this build could not read, named with the store's own reason on
276
+ the hover (`0099.json (not a card record)`), so a board that is not readable never reads as
277
+ a board that is empty.
173
278
 
174
279
  | Band | What it holds |
175
280
  |---|---|
176
- | Toolbar | the mark, the seat the page is served as, as a chip, a live badge (`connecting…`, `live` or `live · 3m`, `reconnecting…`), then one group: the Board\|Team switch, done-of-total progress, a filter, the keys button, and the theme toggle |
177
- | Lanes | trays, not a bare list: one column per lane, sized by the work it holds — a lane with two cards is wider than one with a single card, and a lane showing nothing, because it holds no cards or because it is folded, narrows to a rail of dot, word, and count — so the lanes carrying work keep the width. Three or four across above 1180px, two at 1180px or narrower, one lane plus the lane switch at 860px or narrower — where it opens on the first lane that holds a card. Each head sticks while the pane scrolls, carries its count as a pill, and folds its cards away: `Done` starts folded at six cards or more, since finished work is history, and a reader's toggle decides from then on |
178
- | Cards | the id and a two-line title (the page's loudest text and its only door), an owner chip and an age, a muted priority chip when it is not `normal`, path chips, and one reason line in one voice — what a card waits for, a blocked card's recorded reason or else its last note (or `blocked, with nothing recorded`), `no plan recorded`, `planned`, `✓ done by <owner>` with the commit it landed as, and the last note of a card that is none of those — up to two lines at the Team tab's size, with the whole sentence as its tooltip, because the reason is the reason to act on the card. A card draws **no button**: claiming, moving, editing, and annotating are the tools' work, and a face that offered them would be offering a refusal. A hovered card lifts a pixel; a finished one recedes, title and owner chip muted |
179
- | Team | two panels and a strip. **Needs a decision** is one row per card — a card that is both idle and unplanned arrives once, keeping its strongest reason — laid out as two lines: the id, title and owner chip on the first, the reason on the second — a blocked card's recorded `blockedBy`, else its last note, and `offered 2h ago — nobody took it` for a card whose offer went unanswered — with the whole sentence as its tooltip, and its kind as a left edge (`--danger` blocked, `--warn` idle, `--dim` unplanned or waiting, the same `--dim` dashed for nobody's). The row is a reason to pick the card up in the terminal, and it draws no button: claiming and reclaiming are the tools' work. An unassigned card that waits on unfinished work is a row of its own kind, ranked just under a blocker. Above 1180px the queue stops at one screen's height and scrolls inside its panel, so the roster and the Done strip stay on the same page as its top. **Teammates** is one row per person — a state dot, the label, a `you` chip, a `not listening` chip on a live peer whose inbox watch is off, a mail badge, then `live · 3 held · 1 done · oldest 21m` (a quiet session reads `quiet for 2h`, because a word without an age is no reason to reclaim) — sorted you, live, quiet, no record, and an owner whose label nothing answers to keeps its row. Done is one quiet strip whose ids open their cards. Both panels honour the toolbar's filter, and their headings read `shown/total` the way a lane head does. Below 1180px the queue comes first and the roster follows |
180
- | Card dialog | the record of one card, in two named sections. The head carries the id, the title, and the lane as a dot and a word. **The card** is a ladder — one fact per row, labels sized to the longest of them so the values line up: owner, priority when it is not `normal`, updated, in lane, last edited, landed, waits on, waited on by, plan, paths. `in lane` is the lane the card is in and how long it has held it, read from the card's transitions rather than from `updatedAt`, which a note or an edit moves; a card whose history does not say omits the row. The plan and each path are one line that elides, with the whole value on the hover title; what it waits for reads `#2 (doing)`, the lanes that make the id a decision. Empty rows take their labels with them. **Notes · N** is the cargo at body size on a 68ch measure, each sentence with its attribution under it and a rule between notes, and `M earlier notes dropped` when the store capped them. The body is the only scroll, so the head and `Close` stay put. Nothing in it writes, every field is a set as `textContent`, and the title underneath every card is the way in. `j`/`k` walk to the next or previous card without closing it, and an `sr-only` live region announces `card 2 of 3: <title>` — a title that changes inside an open modal is not announced on its own |
181
- (feat(team): an offer leaves a record, so its age can be read)
182
-
183
- Colour is a role: `--muted` for `todo`, `--accent` for `doing`, `--danger` for
184
- `blocked` and destructive choices, `--success` for `done`. The Team tab's decision
281
+ | Toolbar | the mark, the repository's name as a quiet chip with the root's whole path on hover, a live badge (`connecting…`, `live` or `live · 3m`, `reconnecting…`), then one group: the Board\|Team switch, done-of-total progress (from three cards up, and only above 1180px), a filter, the keys button, and the theme toggle |
282
+ | Lanes | trays, not a bare list: one column per lane, sized by the work it holds — a lane with two cards is wider than one with a single card, and one holding no cards, or folded, narrows to a rail of dot, word, and count — except `done`, which draws as if it held three cards however much history it has, because the lanes still being worked are the ones a reader is working in. One row of five above 1180px, two per row at 1180px or narrower, one lane plus the lane switch at 860px or narrower — where it opens on the first lane that holds a card, and the switch is a group of pressed buttons rather than a second set of tabs, because there is no tabpanel behind it. A filter that matches nothing replaces the lanes with one sentence — `No card matches “x”` — and a `Clear filter` button, so five trays of `no match` never read as a board with no work. Each head sticks to the pane's own top edge while the pane scrolls, carries its count as a pill — `2/3` on `Plan` and `1/5` on `Execute`, the limit and the count in one fact — names itself from the same numbers for a screen reader (`Plan, 1 of 3`, `Backlog, 2 cards`, `Backlog, 1 of 2` under a filter), and folds its cards away: `Done` starts folded at six cards or more, since finished work is history, and a reader's toggle decides from then on |
283
+ | Cards | the id and a two-line title (the page's loudest text, and the focusable half of its door: the whole card opens it), an owner chip and an age whose hover says which fact it is (`updated 2d ago`), a quiet outlined chip for a priority that is not `normal`, `2 notes` when it carries any history — the count, with the last note's age beside it only when that age reads differently from the card's own updated stamp, since a note is itself a write and the same moment printed twice is two bare numbers nobody can tell apart, and a card with ten notes and one with none used to draw the same face — path hints in the mono face, and one reason line in one voice, from `lib/board/reason.ts`: what a card waits for, named by title as well as id (`waits on #31 (execute) The page's roster key…`), a blocked card's recorded reason or else its last note (or `blocked, with nothing recorded`), `no contract yet — missing …`, `contract ready — needs approval`, `contract approved by <who>`, `executing — no spot-check yet`, `spot-checked by <who>`, `offered 2h ago — nobody took it`, and `✓ done by <owner>` with the commit it landed as — up to two lines at the Team tab's size, with the whole sentence as its tooltip, because the reason is the reason to act on the card. A card holding others up says so below it — `holds up #79 (backlog) The page's roster key…` — the one fact its own row cannot carry. A title and a path hint elide **inside** the card: a chip is one line by design, so a path or a label longer than the card is clipped rather than crossing into the lane beside it, and its whole value is the hover title. The hints past the third are counted by a `+N` chip whose hover lists them one per line, and which are in the list again as text a screen reader reads. What a card cannot show — the reason, the holds-up line, the hints behind a `+N` chip — is named by the title button's `aria-describedby`, so the keyboard that reaches the card by `j`/`k` reads the same sentences a pointer gets on hover. A card draws **no button**: claiming, moving, editing, and annotating are the tools' work, and a face that offered them would be offering a refusal. A hovered card lifts a pixel; a finished one recedes, title and owner chip muted; the card the reader is on wears an accent edge and the ones they have opened keep a quiet one, so walking a lane and coming back does not look like arriving for the first time |
284
+ | Team | two panels and a strip. **Needs a decision** is one row per card — a card that is both idle and unplanned arrives once, keeping its strongest reason — laid out as two lines: the id, title and owner chip on the first, the reason on the second — a blocked card's recorded `blockedBy`, else its last note, and `offered 2h ago — nobody took it` for a card whose offer went unanswered — with the whole sentence as its tooltip, and its kind as a left edge, in the tone `lib/board/tone.ts` gives that kind (`--danger` blocked, and `gone` — an owner whose process is not running, ranked above a card merely sitting still; `--warn` approval, and idle; `--dim` unplanned or waiting, the same `--dim` dashed for nobody's). The tone is read from one table, so the row's rank and its colour cannot disagree, and a danger row is the only one whose reason sentence is coloured with its edge. The row is a reason to pick the card up in the terminal, and it draws no button: claiming and reclaiming are the tools' work. An unassigned card that waits on unfinished work is a row of its own kind, ranked just under a blocker. Above 1180px the queue stops at one screen's height and scrolls inside its panel, so the roster and the Done strip stay on the same page as its top. **Teammates** is one row per person — a state dot, the label, a `you` chip, a `not listening` chip on a live peer whose inbox watch is off, a mail badge, then `live · 3 held · 1 done · oldest 21m` (a quiet session reads `quiet for 2h`, because a word without an age is no reason to reclaim) — sorted you, live, quiet, no record, and an owner whose label nothing answers to keeps its row. Done is one quiet strip whose ids open their cards. Both panels honour the toolbar's filter, and their headings read `shown/total` the way a lane head does. Below 1180px the queue comes first and the roster follows |
285
+ | Card dialog | the record of one card, in two named sections. The head carries the id, the title, and the lane as a dot and a word. **The card** is a ladder — one fact per row, labels sized to the longest of them so the values line up: owner, priority when it is not `normal`, updated, in lane, last edited, landed, waits on, waited on by, plan, goal, acceptance, scope out, approved, reviewed, paths. `in lane` is the lane the card is in and how long it has held it, read from the card's transitions rather than from `updatedAt`, which a note or an edit moves; a card whose history does not say omits the row. The plan and each path are one line that elides, with the whole value on the hover title; what it waits for reads `#2 (execute) <title>`, the lane that makes the id a decision and the card it names, and the contract's three lines are rungs of their own so a reader compares what was promised with what was done. Empty rows take their labels with them. **Notes · N** is the cargo at body size on a 68ch measure, each sentence with its attribution under it and a rule between notes, and a note's prose is inline Markdown read by the kit's one renderer — `code`, **bold**, *italic*, and a link to an `http`, `https`, or `mailto` target become atoms, and any other target stays the text it was, so a note can never become markup — and `M earlier notes dropped` when the store capped them. The body is the only scroll, so the head and `Close` stay put. Nothing in it writes, every field is a set as `textContent` — a note's prose is the page's one exception, handed to the kit's inline renderer, which appends text and four atoms and never parses markup — and the card underneath every title is the way in. `j`/`k` walk to the next or previous card without closing it, and an `sr-only` live region announces `card 2 of 3: <title>` — a title that changes inside an open modal is not announced on its own |
286
+ Colour is a role, one per lane, read from `LANE_RULES`: `--muted` for `backlog`,
287
+ `--warn` for `plan`, `--accent` for `execute`, `--danger` for `blocked` and destructive
288
+ choices, `--success` for `done`. The Team tab's decision
185
289
  rows sit on the kit's `--raised`, the one step above `--surface` that lets a block read
186
290
  as a plane instead of an outline; every lane, `Done` included, keeps that `--surface`
187
291
  tray, so a finished card still sits on something and history recedes through its muted
188
- title and chip instead of through a hole in the row. A card takes its colour
189
- from the lane it sits in — the head's dot, and the reason line's tone — rather than
190
- from a bar of its own, so one card does not shout twice. The page states a lane and
191
- a card with `data-status`, so a theme change repaints it without a rule of its own.
292
+ title and chip instead of through a hole in the row. A card takes its lane's colour on the head's
293
+ dot and the dialog's state, and its reason line takes the tone of the reason's kind — `--danger`
294
+ for a block, `--warn` for a contract, an approval, or a spot-check somebody still owes, `--muted`
295
+ for what is merely true of the card. The rule behind those words: **warn is a person who must act
296
+ next, danger is a refusal or a loss, and anything else is quiet**. One table in
297
+ `lib/board/tone.ts` answers for all of it, and the terminal reads the same table through
298
+ `extensions/board/tone.ts`, so a lane is one colour on both surfaces. The page states a lane with
299
+ `data-lane` and a decision row's tone with `data-tone`, so a theme change repaints either without
300
+ a rule of its own.
192
301
 
193
302
  The board as a posting card — the same rules, drawn to be read at half width:
194
303
 
195
- <img src="../../assets/cards/board.png" alt="One board, every session: the page's seat and live badge; four lane trays — To do, Doing, Blocked, Done — each with a dot, a count pill and a fold; one card carrying an id, a title, an owner chip, a priority chip, its age, a path chip and one reason line, with no action on it; and the lead's round with two owner lines and the counts unassigned, unplanned, blocked, idle and done" width="100%">
304
+ <img src="../../assets/cards/board.png" alt="One board, every session: the page's repository name and live badge; five lane trays — Backlog, Plan, Execute, Blocked, Done — each with a dot, a count pill and a fold; one card carrying an id, a title, an owner chip, a priority chip, its age, a path chip and one reason line, with no action on it; and the lead's round with two owner lines — each dot in the colour of the state it reports, success for live and warn for quiet — and the counts unassigned, unplanned, awaiting approval, blocked, idle and done" width="100%">
196
305
 
197
306
  *`board`: the lanes, one card's atoms, and the lead's round — three readings of one board, drawn by `screenshot:cards board`.*
198
307
 
199
- <img src="../../assets/cards/board-team.png" alt="The three team verbs as three bands: assign sends a handoff to a named peer and writes the card as doing, owned by that peer, before the peer reads the mail; offer broadcasts an event to every other live session without recording an owner, so the first claim wins; reclaim returns the card to To do with its notes kept and no handoff sent" width="100%">
308
+ <img src="../../assets/cards/board-team.png" alt="The three team verbs as three bands: assign sends a handoff to a named peer and writes the card into plan, owned by that peer, before the peer reads the mail; offer broadcasts an event to every other live session without recording an owner, so the first claim wins; reclaim returns the card to Backlog with its notes kept and no handoff sent" width="100%">
200
309
 
201
310
  *`board-team`: `assign`, `offer`, and `reclaim` — the two that reach the bus, and the one that writes the card alone (`screenshot:cards team`).*
202
311
 
312
+ <img src="../../assets/cards/board-parallel.png" alt="Two lanes of three rows each. In the plan lane, #4 billing-core, #5 api-core, and #6 web-core each carry a contract node reading contract, 契约, and plan file, side by side, with goal, acceptance, and scope out named under them; every row crosses one vertical approve line, a signature only the lead may give and never while holding the card. In the execute lane the same three run at once under their owners, each row crossing a vertical review spot-check line into done. Each lane's floor states its count — 3/3 contracts, 3/5 owners executing — and the round beneath counts 0 unplanned, 1 awaiting approval, and 2 done; the foot says execute opens only on the lead's approval and that assign and offer hand a card over without starting a turn" width="100%">
313
+
314
+ *`board-parallel`: plan three at once, then execute three at once — `approve` is the lead's signature on the contract, `review` the lead's look before `done`, and both are lines every row crosses (`screenshot:cards parallel`).*
315
+
203
316
  ## Keys
204
317
 
205
318
  | Key | What it does |
@@ -211,9 +324,13 @@ The board as a posting card — the same rules, drawn to be read at half width:
211
324
  | `Esc` | Close a dialog, or clear the filter |
212
325
  | `?` | The keyboard sheet |
213
326
 
214
- A dialog traps `Tab`, closes on `Esc` or a backdrop click, and returns focus to
215
- whatever opened it — the kit's `dialogScript`, shared with the review and browse
216
- pages. A field keeps its keys: `/` inside the filter box types a slash.
327
+ A dialog traps `Tab` in both directions, closes on `Esc` or a backdrop click, and returns
328
+ focus to whatever opened it — the kit's `dialogScript`, shared with the review and browse
329
+ pages. A field keeps its keys: `/` inside the filter box types a slash. A card dialog keeps
330
+ the page's keys too: `/`, `t`, and `?` do nothing while it is open, and `j`, `k`, `Enter`,
331
+ and `Esc` are the ones still read at it — the sheet says so in a line under its table. A
332
+ kit dialog whose focusable elements are two or more cycles between them; one with a single
333
+ button stays on that button.
217
334
 
218
335
  ## Security
219
336
 
@@ -235,26 +352,35 @@ pages. A field keeps its keys: `/` inside the filter box types a slash.
235
352
  the cards accumulate — most of this repository's own board was finished cards — and
236
353
  reading them on every call is a cost that grows with the history rather than with the
237
354
  work. A `list` that names no lane therefore leaves `done` out and prints the count, which
238
- is what keeps the omission from reading as a smaller board; `status: "done"` reads them,
355
+ is what keeps the omission from reading as a smaller board; `lane: "done"` reads them,
239
356
  and `round` counts every card, because a rollup that hid half the board would not be one.
240
357
  The page bounds its own View the same way, folding a long `done` lane. `archive` is how a
241
358
  board stops paying for its history at all: the card leaves the board and its read.
242
- - **A finished card stays finished.** `done` is terminal for every transition — claim,
243
- assign, release, move, and plan all refuse it — so a card a dependency was recorded
244
- against cannot un-finish and reopen the order after the fact, and `archive` does not
245
- change that: it moves a `done` card's file, and never its lane. The one exception is
246
- `edit`: a title, path hints, priority, or `dependsOn` can still be corrected by whoever
247
- holds it, or by anyone while nobody does, because a title is what a reader reads and a
248
- finished feature whose title is wrong would stay wrong for as long as the board keeps
249
- history. A card finished by
250
- mistake is dropped and added again: the new card takes a new number, and the dropped
251
- one's title and lane stay in `ended/` under the id nothing will use again. Its notes
252
- do not come with it.
359
+ - **A card planned before the plan was required must record one.** `contract` now demands
360
+ the file `plan` action `write` saved, and a move to `execute` or `done` re-checks it, so a card
361
+ whose contract names a path outside this repository's plan store — or no plan at all —
362
+ cannot move until its owner calls `contract` again with a real one. That is the fix, not a
363
+ migration: the refusal names `contract`, the plan tool is where the file comes from, and
364
+ nothing is rewritten behind a read. A card already in `execute` is held to the same check
365
+ on its way to `done`.
366
+ - **A finished card stays finished.** `done` is terminal — claim, assign, release,
367
+ reclaim, move, contract, approve, review, and offer all refuse it — so a card a
368
+ dependency was recorded against cannot un-finish and reopen the order after the fact,
369
+ and `archive` does not change that: it moves a `done` card's file, and never its lane.
370
+ The one exception is `edit`: a title, path hints, priority, or `dependsOn` can still be
371
+ corrected by whoever holds it, or by anyone while nobody does, because a title is what a
372
+ reader reads and a finished feature whose title is wrong would stay wrong for as long as
373
+ the board keeps history. A card finished by mistake is dropped and added again: the new
374
+ card takes a new number, and the dropped one's title and lane stay in `ended/` under the
375
+ id nothing will use again. Its notes do not come with it.
253
376
  - **A card file this build cannot read is reported, not skipped.** A file written by a
254
377
  newer build, a half-written one, or a record moved under another name is named by
255
378
  `list` with the reason and counted in the page's footer beside the store's size, so a
256
- board that is not readable never reads as a board that is empty. `done` stays
257
- terminal until a `reopen` is designed.
379
+ board that is not readable never reads as a board that is empty. The record is version
380
+ `2`; a version `1` file — the shape before the lane and the block were named — is read by
381
+ mapping it and is rewritten as `2` on its next write, so nothing is lost and nothing is
382
+ migrated behind a read. A version above this build's is named by number rather than
383
+ guessed at. `done` stays terminal until a `reopen` is designed.
258
384
  - **Links are recorded by the tool.** A link names an id, so the page shows what a
259
385
  card waits for but does not edit it; `board edit` records it while the card is being
260
386
  planned, and `board drop` refuses to strand one.
@@ -275,7 +401,7 @@ pages. A field keeps its keys: `/` inside the filter box types a slash.
275
401
  with no tool-side twin is assigning **without** messaging the peer: `team`'s `assign`
276
402
  sends a handoff, and nothing else records an owner silently.
277
403
  - **A seat can outlive its writes.** A card claimed from the page before this change still
278
- names `board:team-lead` as its owner, and `reclaim` is how a lead takes it back.
404
+ names `board:<lead>` as its owner, and `reclaim` is how a lead takes it back.
279
405
  - **A lane history is short, and it starts when the field did.** A card keeps the last
280
406
  eight lane transitions, dropping the oldest and counting what it dropped, so the record
281
407
  is bounded at write time rather than by a janitor — nothing prunes a board. A card
@@ -292,17 +418,15 @@ it always meant: when the card was last touched, which is not when it last chang
292
418
  - **The page stops a minute after its last tab leaves, or two minutes after it starts
293
419
  with no tab at all.** Closing the tab to run a command and coming back is fine, and a
294
420
  teammate's watching tab counts as one. The second grace covers the page a browser never
295
- opened — `openInBrowser` swallows a failure, and a port held by a page nobody saw is a
296
- port held for the session. Either way `/board` starts it again, on the same port, and
297
- prints the address it reopened.
298
- - **The port is found, or pinned.** With no setting the page prefers a free port in
299
- 4780–5779, falls back to whatever free port the OS gives it if that one is taken, and
300
- remembers the answer for the session; `board.port` in `bytefly.json` pins it, so a
301
- bookmarked URL survives a restart. A pinned port that is already taken fails the start —
302
- `port 4780 is in use — change "port" in the bytefly.json board section, or stop what
303
- answers on it` — because a quiet fallback would move the page out from under the
304
- bookmark. A live note still wins: one repository has one page, so this session attaches to
305
- the port that note names rather than serving a second one.
421
+ opened — `openInBrowser` swallows a failure, and a prefix held by a page nobody saw is a
422
+ prefix held for the session. Either way `/board` starts it again, on the session's port,
423
+ and prints the address it reopened.
424
+ - **The prefix is `/board` on the session's one server.** The port belongs to `serve`, so
425
+ `/board`, `/browse`, and a browser plan review answer on one origin; that port is the
426
+ project's own number, and `serve.port` in `bytefly.json` pins another, so a bookmarked
427
+ URL survives a restart. A live note still
428
+ wins: one repository has one page, so this session attaches to the page that note names
429
+ rather than serving a second one — and a session that attaches binds nothing at all.
306
430
  - **The note is believed only while it answers.** A page that crashed leaves its note
307
431
  behind; the next `/board` probes it, takes it over when nothing replies, and attaches to
308
432
  the page that won when two sessions start in the same instant. A clean exit — `/board
@@ -310,35 +434,63 @@ it always meant: when the card was last touched, which is not when it last chang
310
434
  is the one a crash left. A note it cannot read at
311
435
  all is named with its path — "another session is serving" is only said of a page that was
312
436
  seen answering.
437
+ - **A page that cannot read the board says so once.** A snapshot is built on every tick, and
438
+ a read can fail under a running page — the board's directory removed, or replaced by
439
+ something that is not one. The frame is dropped rather than raised, the session is told
440
+ once, and the next tick tries again, so a board that comes back is followed again without
441
+ a second `/board`; a notice per tick would be a transcript nobody reads.
313
442
 
314
443
  ## Pi integration
315
444
 
316
- - **`session-bus`**: a card's owner is the bus label, so a card names a session a
445
+ - **`session-bus`**: a card's owner is the session's team name, so a card names a session a
317
446
  peer can be addressed by. The board calls one thing on it — the join, through
318
447
  `lib/session-tool.ts`, because a command context cannot call a tool and the request
319
448
  therefore travels on `pi.events` — and reads its records otherwise, which is why the
320
- page works with the bus disabled. A failed join is a clause on the `/board` line, not
321
- a failed page. The seat is the name the page is served and reported under, and `/board
322
- status` says which session is on the team through it.
323
- - **`plan`**: `board plan` records a plan file's path; the plan extension owns the
324
- file. The board stays available while planning, and only `list` works: the same tool
325
- writes, so its read-only hint cannot say that, and plan mode reads the action in the
326
- call instead — `add`, `claim`, or any other verb is refused with the exit guidance.
327
- The plan is recorded **after** approval, when the writing verbs work again.
449
+ page works with the bus disabled. The join asks for the name the board already writes
450
+ and never renames the session, so the footer stays the user's. A failed join is a clause
451
+ on the `/board` line, not a failed page. The seat is the name the page is served and
452
+ reported under, and `/board status` says which session is on the team through it.
453
+ - **`plan`**: `board contract` records the file `plan` action `write` saved; the plan extension owns
454
+ it, and `lib/plans/paths.ts` is the one statement of where it lives — the board reads the
455
+ same module, so the two cannot disagree about what a plan path is. The board stays
456
+ available while planning, and only the reads in `lib/board/actions.ts` work: the same tool
457
+ writes, so its read-only hint cannot say that, and plan mode reads the action in the call
458
+ instead (`lib/policy.ts`'s `isBoardRead`). Every other verb is refused with the board's own
459
+ sentence, which names the order — claim before you plan, record the plan once it is
460
+ approved — so a model that tries to claim mid-plan is told what to do instead. The contract
461
+ is recorded **after** the plan is approved, when the writing verbs work again.
462
+ - **`bytefly`**: the board's own section of the shared document, in one place:
463
+ `{"board": {"wip": {"plan": 3, "execute": 5}, "lead": "gavin"}}`. Each number is clamped to
464
+ `1..20` and the page's heads show the numbers the store read once; `lead` is the team name the
465
+ two signatures must carry **and** the name the page serves as, so setting it renames the lead
466
+ everywhere at once. The project file overrides the global one. Every key falls
467
+ back on its own, so setting one leaves the others alone. `/bytefly settings` carries all
468
+ three as rows — `Plan limit`, `Execute limit`, and `Lead` — and a change lands in the file at
469
+ once and in the board at the next session, because the section is read when the store
470
+ opens.
328
471
  - **`todo`**: the approved plan's steps are seeded into `todo`. Steps never become
329
472
  cards. A `list` call is a read for the lag reminder too, so reading the board never
330
473
  forces a todo update, while `add` or `claim` counts as work like any other write.
331
474
  - **`worktree`**: the store is keyed by the repository's common git directory, so
332
475
  every worktree shares one board.
333
- - **`bytefly`**: the page's port appears as a `board` status chip, and `doctor`
334
- reports nothing new — the board needs no binary and no per-machine setup; its one
335
- setting, `board.port`, is a `bytefly.json` edit.
476
+ - **`serve`**: the page mounts `/board` on the session's one server, which draws the
477
+ single `◉ <port>` chip. **`bytefly`**: the bar shows that chip, and `doctor` reports
478
+ nothing new — the board needs no binary and no per-machine setup.
336
479
 
337
480
  ## Design notes
338
481
 
339
- - **One JSON file per card**, in `<agentDir>/board/<repo-key>/cards/`, one per ended
482
+ - **One JSON file per card**, in `<agent-dir>/board/<repo-key>/cards/`, one per ended
340
483
  card in `ended/`, and one per archived card in `archive/`. The listing is the index, so
341
484
  a reader needs no cache and no daemon.
485
+ - **The plan is proved, not asserted.** `contract` requires the path `plan` action `write` returned,
486
+ `lib/board/artifact.ts` resolves it and checks that it is a markdown file inside this
487
+ repository's plan store, and a move to `execute` or `done` re-checks that the file is still
488
+ there. The board still never reads a plan — a `stat` is the whole check — but a card can no
489
+ longer be executed against a path to nothing, which is what "the plan it is executed
490
+ against" had been claiming while `execute` asked no question at all. The proof is a fact
491
+ the tool hands the state machine, so `rules.ts` stays pure and a surface added later cannot
492
+ pass the gate by forgetting it; the stored path is the resolved one, so a card reads the
493
+ same absolute file from every session.
342
494
  - **An id is never reused.** The next id is the highest in `cards/`, `archive/` *or*
343
495
  `ended/` plus one, so a number cannot be handed to a second feature — which is the only
344
496
  thing that lets a note, a handoff, or a link name a card and still mean it. A record
@@ -351,7 +503,7 @@ it always meant: when the card was last touched, which is not when it last chang
351
503
  drop still leaves a record because the card is gone, while an archived card is not gone
352
504
  at all — and why a read by id looks in both piles: a card is where it lies. The
353
505
  decision is explicit because nothing in this board prunes by itself, and because an
354
- automatic sweep would make `status: "done"` return something different past a threshold
506
+ automatic sweep would make `lane: "done"` return something different past a threshold
355
507
  nobody could see. The board, the rollup, and the page read `cards/` alone — the only
356
508
  look at the archive is the directory listing a lane-less `list` counts, so the omission
357
509
  cannot pass for a deleted history — and an archive therefore lowers the board's size
@@ -363,7 +515,11 @@ it always meant: when the card was last touched, which is not when it last chang
363
515
  - **The page is a dumb renderer** over one snapshot built on the server, pushed
364
516
  only when a card's revision, or a teammate's state, mail, or listening flag, moves: a
365
517
  heartbeat every ten seconds is not news, and a frame nobody needs costs the page a
366
- parse.
518
+ parse. The snapshot carries the lanes rather than a count beside them — each with its
519
+ key, label, empty word, colour role, limit (where it has one), and count — plus `total`,
520
+ the rollup, the store's size and path, the files it could not read, and the repository's
521
+ name and root. There is no `counts` object to disagree with the trays, and no lane table
522
+ in the browser.
367
523
  - **A query reads what a card says, not just what it is called.** A card's reasoning lives
368
524
  in its notes and in the plan it is executing, so leaving both out of the search made the
369
525
  two fields that hold the argument the two a reader could not find. The order is the
@@ -388,20 +544,25 @@ it always meant: when the card was last touched, which is not when it last chang
388
544
  the prefix and could not see the whole name, so a rule with nothing behind it (or a
389
545
  name with no rule) survived until a capture showed it.
390
546
  - **The page is given what it prints.** A card arrives with the priority it acts on and
391
- the unfinished dependencies that block it (`waits`, as `#2 (doing)`), so the client
392
- holds no copy of a rule `lib/board` owns — the copies it did hold had already drifted
393
- from the tool's. The one body that cannot cross as data is `plural`, and it crosses as
394
- source (`PLURAL_JS`), because a second wording for `1 card` is the same mistake in a
395
- smaller place.
547
+ the unfinished dependencies that block it (`waits`, as `#2 (execute) Patch the gateway`),
548
+ and a lane arrives
549
+ with its words, colour, limit, and count, so the client holds no copy of a rule
550
+ `lib/board` owns — the copies it did hold had already drifted from the tool's. What cannot
551
+ cross as data crosses as source, because either it needs the reader's clock or it must be
552
+ the same sentence on both surfaces: `plural` (one wording for `1 card`), `matchesQuery`
553
+ (a filter that finds the card the tool would), `reasonFor` (one reason line, whose
554
+ `offered … ago` has to tick rather than freeze at snapshot time), `formatAge`, the tab
555
+ title, and the placeholder lane count. Each is a `*_JS` string spliced into the bundle by
556
+ `view/client.ts`, and `extensions/board/view/pages.test.ts` holds the splices in place.
396
557
  - **An offer is a record, not a reservation.** `team offer` broadcasts a card and then
397
558
  writes `offeredAt` — the moment, and nothing else — because an event leaves nothing
398
559
  behind and a card three sessions ignored read exactly like one nobody had looked at.
399
560
  Every answer clears it: a claim, an assignment, and a release each delete the field, so
400
- its presence means the offer is still open. The page reads it in two places and the
401
- tool in a third — `offered 1m ago · nobody took it` under the card, `offered 1m ago —
402
- nobody took it` as the Team tab's reason, `offered: 1m ago, and nobody took it` in
403
- `list <card>` — and the age is `lib/format.ts`'s one rule, handed to the page's bundle
404
- as source so the transcript and the page cannot say one age two ways.
561
+ its presence means the offer is still open. The page reads it in two places — the card
562
+ face and the Team tab's row — and both print the one sentence `lib/board/reason.ts`
563
+ builds, `offered 1m ago — nobody took it`; `list <card>` says `offered: 1m ago, and
564
+ nobody took it`, because the transcript has no reason slot to share. The age itself is
565
+ `lib/format.ts`'s one rule, handed to the page's bundle as source.
405
566
  - **`waits on` answers two questions, on purpose.** The page names the *unfinished*
406
567
  dependencies in one line — "why can I not start" — and `board`'s result names every
407
568
  dependency with its lane, including finished ones — "what is this card tied to". Both
@@ -409,16 +570,18 @@ it always meant: when the card was last touched, which is not when it last chang
409
570
  a drift.
410
571
  - **A link is stored once, and read both ways.** `dependsOn` lives on the waiting card
411
572
  alone, so nothing on disk says what a card is holding up; `openDependentsOf` answers
412
- it over the board, `list <card>` prints `waited on by N cards: #2 (todo)`, and the
573
+ it over the board, `list <card>` prints `waited on by N cards: #2 (backlog)`, the page's
574
+ card face says `holds up #2 (backlog) <title>`, and the
413
575
  dialog has the rung the card's own row cannot. Both skip a finished card, unlike the
414
576
  drop refusal, which keeps every edge: "what is held up" is about work, and "would this
415
577
  drop strand a link" is about the file.
416
578
  - **One page is found, not configured.** A note beside the cards carries the port, the
417
579
  seat, and the token; it is created exclusively, so two sessions starting at
418
580
  once cannot both serve, and the loser attaches to the winner. The alternative —
419
- a setting that names the lead — was rejected: the role moves, the seat's name does
420
- not. The port is the one thing a file may pin: `board.port` states where the page
421
- listens, never who serves it.
581
+ a setting that named the page's address — was rejected: the role moves, the seat's name
582
+ does not, and a second session has to find the page rather than pick one. (The lead is a
583
+ different question, and it *is* a setting: see **Who signs**.) The port in the note is the
584
+ session's, never a number this page chose.
422
585
  - **The page is a view because a browser is not an identity.** Every page write was made
423
586
  as the seat, so a page `assign` had to write down that nobody had been told, and the
424
587
  client's copy of the card rules drifted from `lib/board/rules.ts` — it offered a drop
@@ -429,79 +592,145 @@ it always meant: when the card was last touched, which is not when it last chang
429
592
  reverse edge is computed over the board, because a copy of the same edge on the card
430
593
  being waited for is a copy that drifts; the cycle check and the drop rule are what
431
594
  keep the graph whole instead.
432
- - **A priority is a field, not a lane.** The four lanes are the board's vocabulary and
433
- a fifth would double the counts and the heads; inside a lane, priority then age is the
434
- order, and a card says its priority only when it is not the ordinary one.
595
+ - **A priority is a field, not a lane.** Urgency is not a stage of the work, so it stays
596
+ a field: inside a lane, priority then the id the card was given is the order — never the
597
+ moment it was last touched, so a note or a claim cannot move a card out from under a reader
598
+ working down the lane — and a card says its priority only when it is not the ordinary one.
599
+ The lanes are the flow's stages, which is why a fifth one had to be argued for in the note
600
+ below rather than added as "urgent".
435
601
  - **`edit` leaves an omitted field alone.** It is the one action whose job is "make it
436
602
  say this", so an absent field must mean "not this field" — a caller that sends only a
437
603
  title must not clear the path hints and links it never mentioned.
438
- - **A lane change is recorded where every transition ends.** `applyOp` decides each
604
+ - **A lane change is recorded where every transition ends.** `steps.ts` decides each
439
605
  transition, but `write()` in `rules.ts` is the one place all of them pass through, so
440
- the log is appended there rather than from six operations that would each have to
441
- remember it. A write that leaves the lane alone — a note, a plan, an edit, a claim by
442
- the holder — appends nothing, because the question the log answers is "how long has
443
- this been blocked", and `updatedAt` cannot answer it: filing a note against a blocked
444
- card moves that stamp without moving the card.
606
+ the log is appended there rather than from thirteen operations that would each have to
607
+ remember it. A write that leaves the lane alone — a note, a contract, an edit, a claim by
608
+ the holder — appends nothing, because the question the log answers is "how long has this
609
+ been blocked", and `updatedAt` cannot answer it: filing a note against a blocked card
610
+ moves that stamp without moving the card.
611
+ - **Five lanes, and the gates are why.** This board used to say the four lanes were its
612
+ whole vocabulary and a fifth would double the counts and the heads. The flow changed
613
+ that: a card now moves `backlog → plan → execute → done`, and `blocked` is the pool a
614
+ decision parks in. Two of those lanes exist because an agent is a poor judge of itself —
615
+ `plan` holds a contract until the lead, who is not its holder, approves it, and `done` is
616
+ refused until the lead has spot-checked the work — and a WIP limit on `plan` and
617
+ `execute`, counted over the whole board because a repository has one, is what keeps a
618
+ review queue from turning into a shelf. The cost is real (one more tray, one more head)
619
+ and is paid once, in `lib/board/lanes.ts`: the label, the empty word, the colour, the
620
+ ownership rule, and the limit are that table's, and the page receives them in its
621
+ snapshot instead of keeping a copy.
622
+ - **The gates are a label check, and say so.** `approve` and `review` take the lead's name —
623
+ `board.lead`, the page's seat by default — and refuse the card's own holder even when that holder is
624
+ the lead, which is the half of "somebody read this" a file can hold; the board cannot tell a
625
+ person from a session, and does not pretend it can. The name is a team name rather than a
626
+ session, because that is what a pure rule can be handed and what a card file can record: a
627
+ session signs by joining under it, and `board.lead` moving is the board saying who is
628
+ accountable now. The person gets a door of their own — `/board approve` and `/board review`
629
+ write as `you`, a label with no session id — because one session alone would otherwise hold
630
+ a card it could never sign, and because a repository should not need a second process to
631
+ finish its work. The two read differently on the card, which is the honest way to say it:
632
+ `you` is the person who read the plan, the lead is a second pair of eyes. Either sign takes an
633
+ optional `text` note, written with the sign: the signer is the session `note` refuses on a
634
+ held card, so without it a caveat has nowhere to go on the card. The contract is
635
+ three one-line fields written by `contract`, and a changed line or a different plan file
636
+ drops the sign, because the sign named both the wording and the artifact — so a re-worded
637
+ card is signed again, by the lead that signed the last one. A card too small to state an
638
+ acceptance does not belong on the board — it belongs in `todo` — which is the honest answer
639
+ to "this is ceremony for a typo fix".
640
+ - **`status` became `lane` because a card now has two states.** The field, the parameter, and
641
+ the page's attribute were renamed in one step with no behaviour change: once a card can be
642
+ approved and reviewed, "status" answers more than one question, and the docs had always
643
+ said *lane*. `readCard` maps an older file's `status` and `plan` onto `lane` and
644
+ `planPath`, so a published board keeps reading until each card is written again. The log
645
+ needed the same map twice over: a file from before the rename spells a lane `status`, and
646
+ a file from before the *lanes* says `todo` or `doing` — the four-lane board's pool and its
647
+ work in hand — which read as `backlog` and `execute`. A word that is neither a lane nor one
648
+ of those two refuses the record, because a lane guessed from it would put the card
649
+ somewhere its own history disagrees with.
445
650
  - **Rejected:** steps as cards (they flood the board), a shared database (a file per
446
651
  card needs no dependency and no lock), an index file (every write maintains it,
447
652
  every read can find it stale), drag-and-drop (a lane change is a named `move`), a
448
- priority lane (four lanes are the vocabulary), a reverse link field (two copies of
449
- one edge), and an automatic archive past some number of finished cards (a threshold
653
+ reverse link field (two copies of one edge), a separate review lane (a review is a fact
654
+ about a card, not a place), deriving the contract from the plan file (the board never
655
+ reads a plan), and an automatic archive past some number of finished cards (a threshold
450
656
  nobody stated would decide what a read returns, and nothing else here prunes).
451
657
  - **A block says why, and a finished card says with what.** `blocked` exists to ask a
452
- person for a decision, so the move into it records the sentence — `blockedBy`, one
453
- line — and the page draws that rather than the last note somebody wrote, which was a
454
- guess. The reason survives a `release` (the decision is still pending) and is cleared
455
- on the way back into `doing` or `done` (a stale sentence reads as a current one).
658
+ person for a decision, so the move into it records one record — `blocked: { kind, note }`
659
+ — rather than two sibling keys: the kind (`contract`, `dependency`, or `decision`, and
660
+ required by the move) and the sentence a person reads, which is optional. The kind is the
661
+ errand, not the route: where a blocked card may go is decided by what it has, so `blocked`
662
+ offers `plan` whenever that lane has room and `execute` only with the same contract and
663
+ approval the other door asks for. A park frees a slot rather than reserving one, which is
664
+ why a resume can be refused: the lane may have refilled while the card waited, and the
665
+ refusal says so — `wait for a slot, or release it back to the pool` — rather than naming
666
+ the `finish` and `block` exits a parked card does not have. A card parked while it was still being planned comes back to `plan`; one parked
667
+ mid-flight carries both and resumes. A block record is a fact about the `blocked` lane and
668
+ nothing else, so the board's one write deletes it the moment the card is somewhere else — a
669
+ release, a reclaim, and a claim cannot carry a sentence into a lane it was not written for
670
+ — **but the lane log keeps it**: the transition that parked the card carries the same
671
+ `block`, so "why did this sit for two days" outlives the resume. A spot-check is cleared
672
+ only when the card leaves `execute` for `blocked` or `plan` — a park is somebody deciding
673
+ the work is not finished, and re-planning changes what was agreed — so a card that was
674
+ blocked and resumed needs another look before `done`, while a card that lands keeps the
675
+ sign that let it through.
456
676
  `landed` is the commit `HEAD` resolved to at the move to `done`, recorded because
457
- `done` is terminal and nothing later can say which change closed the card. Both
458
- fields are optional and neither is required: a card may be blocked with nothing
459
- recorded, and one finished before the field, or outside a repository, has no commit
460
- to record — finishing never depends on a git read.
677
+ `done` is terminal and nothing later can say which change closed the card. Of the three
678
+ the move writes, only the blocker is required — a card cannot be parked without saying
679
+ what kind of block it is, because a block is what a person is asked to answer. The
680
+ sentence is optional (the page says `blocked, with nothing recorded`), and a card
681
+ finished before the field, or outside a repository, has no commit to record — finishing
682
+ never depends on a git read.
461
683
 
462
684
  ## Files
463
685
 
464
686
  | File | Responsibility |
465
687
  |---|---|
466
- | `index.ts` | The factory: gate on `PI_DISABLED_EXTENSIONS`, build the runtime and the page, register both, close on shutdown |
467
- | `config.ts` | The board's one setting: the port its page is pinned to, clamped, from the `bytefly.json` `board` section |
688
+ | `index.ts` | The factory: gate on the opt-in switch (`PI_ENABLED_EXTENSIONS`) and `PI_DISABLED_EXTENSIONS`, build the runtime and the lazy page, register both, close on shutdown |
468
689
  | `schema.ts` | The tool's parameters |
469
690
  | `types.ts` | The shapes the tool registers and reports — the actions and the fields each acts on, the card an action produced, the cards a list read, the rollup when it was asked for, and the card a refusal carries |
470
691
  | `tools.ts` | Action dispatch and the refusal shape |
471
692
  | `input.ts` | A card body as the tool reads it: the `add` fields normalized in one place |
472
693
  | `format.ts` | The model-facing text |
473
- | `rows.ts` | The transcript row grammar |
474
- | `result.ts` | The transcript block |
475
- | `page.ts` | The page's lifecycle: the port it remembers or is pinned to, the chip that names it, the three closes (`/board stop`, the idle grace, and shutdown — each taking the note with it), and the probe behind `/board status` |
476
- | `snapshot.ts` | The page's snapshot: the cards, the bus's records, and the store's size, handed to `buildView` |
694
+ | `rows.ts` | The transcript row grammar: one card as a line, its lane and its facts painted in the tones `lib/board/tone.ts` gives them, and the accent on the holder's own rows in a `mine` listing |
695
+ | `result.ts` | The transcript block: the collapsed line, and the rows a `list` expands to |
696
+ | `tone.ts` | The terminal's word for a board tone: `TONE_ROLE`, because the page wears a tone as a CSS property and the terminal's roles are its own five words |
697
+ | `lazy-page.ts` | The page behind `lib/lazy`: the command paths load `page.ts` once, and `closeAll` closes only a page that was already opened, so a session that never touches the board imports none of the server |
698
+ | `page.ts` | The page's lifecycle: the `/board` prefix it holds on the session's server, the three closes (`/board stop`, the idle grace, and shutdown — each taking the note with it), and the probe behind `/board status` |
699
+ | `snapshot.ts` | The page's snapshot: the cards, the bus's records, the store's size and path, and the repository's own name, handed to `buildView` |
477
700
  | `commands.ts` | `/board`, its three words and their refusals, and the one line `/board status` prints |
478
- | `seat.ts` | The seat the page is served as: one name, and an id derived from it. The page cannot write, so it is a label now — but the id is still what a card claimed from an older page names |
479
- | `join.ts` | The board's side of the team: the one request that joins for the user, the label it lands on, and how the outcome reads on a `/board` line |
701
+ | `join.ts` | The board's side of the team: when it asks (the page, or the first claimed card), the name it asks for, and how the outcome reads on a `/board` line |
480
702
  | `transport/router.ts` | The five reads — the document, the client script, the theme script, the snapshot, and the frame stream — with the token gate and the guards in front of them |
481
- | `transport/server.ts` | The loopback listener, the frame push, and the close |
482
- | `view/page.ts` | The document: an inert shell the client fills, with no form on it |
703
+ | `transport/server.ts` | The loopback listener, the frame push — one read of the board per frame, and a failed read reported rather than thrown — and the close |
704
+ | `view/page.ts` | The document: an inert shell the client fills, named after the repository it is a board of so the toolbar is not blank before the first frame, with no form on it |
483
705
  | `view/styles.ts` | The page's `board-*` layout rules |
484
- | `view/render.ts` | The client's snapshot → DOM renderer: toolbar, lanes, cards, the dialog's contents |
706
+ | `view/render.ts` | The client's snapshot → DOM renderer: the toolbar, the lanes, and the frame that keeps the dialog current |
707
+ | `view/card.ts` | The client's card face: one card's title and facts, the path hints behind `+N`, the note count, what it holds up, and the read/current marks |
485
708
  | `view/names.ts` | The page's class vocabulary: one table the document and the bundle both read, so what the page wears and what its stylesheet draws can be compared |
486
709
  | `view/team.ts` | The client's round: the two tables and the Done line |
487
- | `view/dialog.ts` | The client's card dialog: open one card, close it, and forget it when the kit closes it |
710
+ | `view/empty.ts` | The client's two empty states: no cards at all, and a filter that matches none — one sentence, and the way back |
711
+ | `view/dialog.ts` | The client's card dialog: fill one card's record (a note through the kit's inline Markdown renderer), keep an open one current across frames, record it as read, close it, and forget it when the kit closes it |
488
712
  | `view/keys.ts` | The key table, and the bindings that make it true — including `j`/`k` walking the open dialog through the drawn cards |
489
713
  | `view/live.ts` | The client's tabs, filter, and frame stream |
490
714
  | `view/client.ts` | The one script served at `/app.js` |
491
715
 
492
- The store, the rules, the rollup, and the snapshot are `lib/board/` — see
716
+ The lanes, the contract, the limits, the reason rule, the steps, the store, the rollup, and
717
+ the snapshot, and the rule that a plan path is a plan file are `lib/board/` — see
493
718
  [`lib/README.md`](../../lib/README.md).
494
719
 
495
720
  ## Testing
496
721
 
497
722
  `test/suite-manifest.ts` declares the suites:
498
- `lib/board/{card,links,page-pointer,round,round-text,rules,session,store,view}` for the
499
- record, the dependency graph, the note a page leaves, the rollup and its text, the card's
500
- transitions, the store a call opens and the identity it writes as, the files that store
501
- keeps, and the snapshot, and
502
- `extensions/board/{input,seat,tools,page,commands,config,extension,transport/router,view/pages,view/behaviour}`
723
+ `lib/board/{artifact,card,contract,lanes,limits,links,page-pointer,read,reason,round,round-text,rules,session,skipped,store,view}`
724
+ for the plan a card is executed against, the record, the reader that maps a v1 file onto it, the contract and its boundary, the
725
+ lane table and its transition matrix, the WIP limits and their config, the dependency graph,
726
+ the note a page leaves, the one reason rule, the rollup and its text, the sentence for a
727
+ refused file, the card's transitions, the store a call opens and the identity it writes as,
728
+ the files that store keeps, and the snapshot, and
729
+ `extensions/board/{input,seat,tools,page,commands,extension,transport/router,view/pages,view/behaviour}`
503
730
  for the card body, the seat, every action, the page's lifecycle over a real socket (a page
504
- served, attached to, and taken over, and a write refused on the wire), the command, the
505
- `board.port` setting, the factory, the routes, the document and client bundle, and the
506
- page driven in a browser — a card opened from its title, the filter, the fold, a live
507
- frame, and the dialog's ladder.
731
+ served, attached to, and taken over, a board that stops being readable, and a write refused
732
+ on the wire), the command, the
733
+ factory, the routes, the document and client bundle, and the
734
+ page driven in a browser — a card opened from its title or anywhere on it, the filter and
735
+ the no-match state, the fold, the lane switch, a live frame, the dialog's ladder, and the
736
+ keyboard a dialog keeps.