@gavin-hu/my-pi-agent 0.19.0 → 0.21.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 (208) hide show
  1. package/README.md +19 -12
  2. package/README.zh-CN.md +15 -9
  3. package/docs/extensions/README.md +2 -1
  4. package/docs/extensions/board.md +329 -151
  5. package/docs/extensions/bytefly.md +427 -273
  6. package/docs/extensions/file-browse.md +46 -44
  7. package/docs/extensions/job.md +1 -2
  8. package/docs/extensions/plan.md +86 -24
  9. package/docs/extensions/serve.md +129 -0
  10. package/docs/extensions/session-bus.md +23 -10
  11. package/docs/extensions/team.md +56 -28
  12. package/docs/extensions/wechat.md +2 -1
  13. package/docs/extensions/worktree.md +30 -15
  14. package/extensions/board/commands.ts +48 -17
  15. package/extensions/board/format.ts +107 -9
  16. package/extensions/board/index.ts +14 -2
  17. package/extensions/board/join.ts +31 -10
  18. package/extensions/board/page.ts +202 -156
  19. package/extensions/board/result.ts +10 -24
  20. package/extensions/board/rows.ts +1 -1
  21. package/extensions/board/schema.ts +31 -10
  22. package/extensions/board/seat.ts +9 -10
  23. package/extensions/board/snapshot.ts +7 -5
  24. package/extensions/board/tools.ts +211 -34
  25. package/extensions/board/transport/router.ts +27 -110
  26. package/extensions/board/transport/server.ts +76 -56
  27. package/extensions/board/types.ts +57 -0
  28. package/extensions/board/view/client.ts +24 -13
  29. package/extensions/board/view/dialog.ts +43 -0
  30. package/extensions/board/view/keys.ts +28 -24
  31. package/extensions/board/view/live.ts +6 -178
  32. package/extensions/board/view/names.ts +91 -0
  33. package/extensions/board/view/page.ts +60 -89
  34. package/extensions/board/view/render.ts +228 -200
  35. package/extensions/board/view/styles.ts +72 -49
  36. package/extensions/board/view/team.ts +63 -41
  37. package/extensions/bytefly/footer/budget.ts +6 -5
  38. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  39. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  40. package/extensions/bytefly/footer/config.ts +19 -11
  41. package/extensions/bytefly/footer/price/price.ts +30 -8
  42. package/extensions/bytefly/footer/register.ts +5 -5
  43. package/extensions/bytefly/footer/render/footer.ts +18 -4
  44. package/extensions/bytefly/footer/render/format.ts +18 -0
  45. package/extensions/bytefly/footer/render/key.ts +46 -0
  46. package/extensions/bytefly/footer/render/layout.ts +13 -3
  47. package/extensions/bytefly/footer/render/lines.ts +23 -7
  48. package/extensions/bytefly/footer/render/segments.ts +93 -31
  49. package/extensions/bytefly/footer/types.ts +29 -5
  50. package/extensions/bytefly/footer/unpriced.ts +42 -17
  51. package/extensions/bytefly/header/component.ts +4 -20
  52. package/extensions/bytefly/panel/commands.ts +28 -1
  53. package/extensions/bytefly/panel/filter.ts +68 -0
  54. package/extensions/bytefly/panel/format.ts +16 -15
  55. package/extensions/bytefly/panel/join.ts +56 -13
  56. package/extensions/bytefly/panel/prices.ts +43 -0
  57. package/extensions/bytefly/panel/settings.ts +72 -23
  58. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  59. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  60. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  61. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  62. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  63. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  64. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  65. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  66. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  67. package/extensions/bytefly/panel/tui.ts +306 -259
  68. package/extensions/check/commands.ts +2 -1
  69. package/extensions/check/render.ts +10 -1
  70. package/extensions/file-browse/config/config.ts +4 -19
  71. package/extensions/file-browse/files/classify.ts +136 -0
  72. package/extensions/file-browse/files/files.ts +8 -547
  73. package/extensions/file-browse/files/index.ts +90 -0
  74. package/extensions/file-browse/files/list.ts +261 -0
  75. package/extensions/file-browse/files/tree.ts +101 -0
  76. package/extensions/file-browse/index.ts +27 -48
  77. package/extensions/file-browse/live/live.ts +1 -1
  78. package/extensions/file-browse/transport/router.ts +12 -6
  79. package/extensions/file-browse/transport/server.ts +61 -28
  80. package/extensions/file-browse/view/chrome.ts +4 -4
  81. package/extensions/file-browse/view/client.ts +3 -1
  82. package/extensions/file-browse/view/emit.ts +51 -0
  83. package/extensions/file-browse/view/html.ts +78 -46
  84. package/extensions/file-browse/view/live-client.ts +2 -2
  85. package/extensions/file-browse/view/markdown.ts +23 -26
  86. package/extensions/file-browse/view/palette.ts +2 -2
  87. package/extensions/file-browse/view/raw-html.ts +27 -22
  88. package/extensions/file-browse/view/tree.ts +1 -1
  89. package/extensions/file-browse/view/urls.ts +20 -6
  90. package/extensions/goal/tui.ts +12 -2
  91. package/extensions/job/runtime.ts +4 -4
  92. package/extensions/job/tools.ts +19 -7
  93. package/extensions/job/tui.ts +2 -28
  94. package/extensions/plan/commands.ts +2 -2
  95. package/extensions/plan/feedback.ts +88 -18
  96. package/extensions/plan/format.ts +10 -3
  97. package/extensions/plan/index.ts +24 -2
  98. package/extensions/plan/plan-files.ts +88 -0
  99. package/extensions/plan/plan-text.ts +114 -0
  100. package/extensions/plan/plans.ts +19 -23
  101. package/extensions/plan/policy.ts +10 -5
  102. package/extensions/plan/runtime.ts +43 -4
  103. package/extensions/plan/todos.ts +51 -3
  104. package/extensions/plan/tools/exit.ts +10 -1
  105. package/extensions/plan/tools/todo-seed.ts +4 -2
  106. package/extensions/plan/tools/write.ts +12 -1
  107. package/extensions/plan/tools.ts +2 -2
  108. package/extensions/plan/types.ts +15 -0
  109. package/extensions/plan/web-blocks.ts +160 -0
  110. package/extensions/plan/web-chrome.ts +7 -5
  111. package/extensions/plan/web-client.ts +271 -325
  112. package/extensions/plan/web-collab.ts +7 -7
  113. package/extensions/plan/web-comments.ts +57 -16
  114. package/extensions/plan/web-composer.ts +91 -0
  115. package/extensions/plan/web-draft.ts +40 -0
  116. package/extensions/plan/web-keys.ts +258 -0
  117. package/extensions/plan/web-markdown.ts +1 -1
  118. package/extensions/plan/web-outline.ts +212 -0
  119. package/extensions/plan/web-page.ts +59 -86
  120. package/extensions/plan/web-payload.ts +188 -0
  121. package/extensions/plan/web-plan.ts +160 -0
  122. package/extensions/plan/web-router.ts +0 -1
  123. package/extensions/plan/web-settle.ts +201 -0
  124. package/extensions/plan/web-share-session.ts +24 -55
  125. package/extensions/plan/web-share.ts +19 -37
  126. package/extensions/plan/web-write.ts +112 -0
  127. package/extensions/plan/web.ts +77 -130
  128. package/extensions/rewind/commands/review.ts +2 -1
  129. package/extensions/serve/config.ts +37 -0
  130. package/extensions/serve/index.ts +71 -0
  131. package/extensions/serve/registry.ts +251 -0
  132. package/extensions/session-bus/join-request.ts +16 -9
  133. package/extensions/session-bus/result.ts +1 -27
  134. package/extensions/session-bus/rows.ts +7 -3
  135. package/extensions/session-bus/runtime.ts +100 -38
  136. package/extensions/session-bus/store.ts +14 -2
  137. package/extensions/session-bus/tools.ts +5 -4
  138. package/extensions/session-bus/tui.ts +1 -1
  139. package/extensions/session-bus/types.ts +10 -1
  140. package/extensions/subagent/commands.ts +2 -1
  141. package/extensions/team/commands.ts +44 -9
  142. package/extensions/team/format.ts +14 -0
  143. package/extensions/team/index.ts +2 -1
  144. package/extensions/team/tools.ts +30 -9
  145. package/extensions/team/types.ts +17 -0
  146. package/extensions/todo/index.ts +4 -1
  147. package/extensions/todo/nudge.ts +8 -2
  148. package/extensions/todo/tui.ts +21 -3
  149. package/extensions/wechat/bridge.ts +43 -172
  150. package/extensions/wechat/intake.ts +245 -0
  151. package/extensions/worktree/commands.ts +24 -17
  152. package/extensions/worktree/exit.ts +12 -6
  153. package/extensions/worktree/git.ts +22 -14
  154. package/extensions/worktree/guard.ts +15 -285
  155. package/extensions/worktree/handoff.ts +10 -2
  156. package/extensions/worktree/index.ts +2 -1
  157. package/extensions/worktree/lifecycle.ts +2 -0
  158. package/extensions/worktree/registry.ts +20 -11
  159. package/extensions/worktree/root-tools.ts +13 -3
  160. package/extensions/worktree/runtime.ts +8 -2
  161. package/extensions/worktree/screen.ts +15 -8
  162. package/extensions/worktree/shell.ts +303 -0
  163. package/extensions/worktree/state.ts +1 -1
  164. package/extensions/worktree/status.ts +42 -9
  165. package/extensions/worktree/tools.ts +19 -13
  166. package/lib/README.md +9 -7
  167. package/lib/board/card.ts +192 -3
  168. package/lib/board/links.ts +26 -2
  169. package/lib/board/page-pointer.ts +111 -32
  170. package/lib/board/round-text.ts +11 -2
  171. package/lib/board/round.ts +25 -20
  172. package/lib/board/rules.ts +144 -11
  173. package/lib/board/session.ts +84 -18
  174. package/lib/board/store.ts +268 -55
  175. package/lib/board/view.ts +114 -24
  176. package/lib/bus-request.ts +83 -0
  177. package/lib/format.ts +22 -0
  178. package/lib/http/index.ts +11 -7
  179. package/lib/http/mount.ts +142 -0
  180. package/lib/http/port.ts +25 -0
  181. package/lib/http/serve.ts +170 -43
  182. package/lib/http/server.ts +3 -3
  183. package/lib/http/sink.ts +34 -9
  184. package/lib/plan-entry.ts +20 -13
  185. package/lib/policy.ts +14 -1
  186. package/lib/requirements.ts +2 -2
  187. package/lib/session-label.ts +23 -7
  188. package/lib/session-roster.ts +74 -3
  189. package/lib/session-tool.ts +37 -21
  190. package/lib/spawn.ts +37 -3
  191. package/lib/stray-fields.ts +45 -0
  192. package/lib/tool-names.ts +13 -0
  193. package/lib/tui/README.md +6 -3
  194. package/lib/tui/glyphs.ts +3 -6
  195. package/lib/tui/render-cache.ts +29 -0
  196. package/lib/tui/tool.ts +30 -1
  197. package/lib/web/README.md +52 -2
  198. package/lib/web/client-runtime.ts +149 -0
  199. package/lib/web/index.ts +4 -2
  200. package/lib/web/markup.ts +106 -2
  201. package/lib/web/names.ts +42 -0
  202. package/package.json +3 -2
  203. package/extensions/board/config.ts +0 -36
  204. package/extensions/board/view/actions.ts +0 -306
  205. package/extensions/board/writes.ts +0 -99
  206. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  207. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  208. package/extensions/job/process.ts +0 -35
@@ -1,44 +1,53 @@
1
1
  # board — one shared feature board per repository
2
2
 
3
- Every Pi session on a repository reads and writes the same cards, and a repository
4
- has **one** page: the session that starts it serves it under the seat `team-lead`, and
5
- every other session opens that page read-only instead of a second one. A card is one
6
- **feature**: its outcome, who holds it, where the work happens, the plan it is
7
- executing, how urgent it is, what it waits for, and the notes that explain how it got
8
- there.
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
6
+ page is a **view** — it cannot change a card — so what a browser shows and what the
7
+ 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
10
 
10
11
  The steps of the work are deliberately *not* on the board. They live in the
11
12
  owner's plan and in `todo`; the board answers who is doing what, and what needs a
12
13
  decision.
13
14
 
14
- <img src="../../assets/screens/board.png" alt="The board page: a toolbar with the seat the page writes as, a live badge, Board and Team tabs, done-of-total progress, a filter and a New card button; four lane trays, each with a dot, a count and a fold control; and cards carrying an owner chip, path hints, a reason line and their actions" width="100%">
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%">
15
16
 
16
- *`/board`: the four lanes, with a new card one keystroke from its dialog and the round one tab away.*
17
+ *`/board`: the four lanes, with the round one tab away and every card opened by its title.*
18
+
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%">
20
+
21
+ *The card dialog: the record of one card — a ladder of facts, then its notes at body size
22
+ (`screenshot:board --dialog card`).*
17
23
 
18
24
  ## Capabilities
19
25
 
20
26
  | Capability | How | What you get |
21
27
  |---|---|---|
22
- | A shared feature list for one repository | `board` action `list` | Every card in lane order — `todo`, `doing`, `blocked`, `done` — or one card, or one lane |
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 |
23
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 |
24
- | Publish a feature | `board` action `add` | A `todo` card with path hints and a first note |
30
+ | Publish a feature | `board` action `add` | A `todo` card with path hints and a first note (one line, at most 1000 characters) |
25
31
  | Take a feature | `board` action `claim` | The card is `doing` and owned by this session |
26
32
  | Hand it back | `board` action `release` | The card is `todo` again, with no owner |
27
33
  | Change lane | `board` action `move` | Wherever the owner moves it: `doing`, `blocked`, `done` |
28
- | Record what happened | `board` action `note` | A note appended to the card's history, attributed and stamped |
34
+ | Record what happened | `board` action `note` | A note appended to the card's history, attributed and stamped — one line, at most 1000 characters |
29
35
  | Record the plan | `board` action `plan` | The plan file the card is executing, so "unplanned" work is visible |
30
36
  | 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 |
31
37
  | Say how urgent it is | `board` action `add` or `edit` with `priority` | `high` sorts first inside its lane, and the card says so |
32
- | 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 |
33
- | Hand a card to a teammate | `Assign to` in the card dialog | The owner is recorded and the card's history says no handoff was sent — `team`'s `assign` is the one that messages a peer |
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 |
34
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` |
35
- | Find a card without reading every lane | `board` action `list` with `query` | The cards whose title, path hint, or holder matches — the same rule the page's filter uses |
41
+ | 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`) |
36
42
  | See the work you hold | `board` action `list` with `mine` | The cards this session holds, by the same rule a claim is checked against |
37
- | Delete a card | `board` action `drop` | The card and its notes are gone; a `todo` or `done` card is anyone's to drop, one in flight only its holder's, and one that a card waits on cannot leave at all |
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 |
38
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 |
39
- | Watch somebody else's page | `/board` on a session that is not serving | The same page, **read-only**: no **+ New card**, no `Claim`, `Move`, `Edit`, `Drop` or note field, because the note a viewer reads carries a credential that may only read. The viewer joins the team under its own label, so a handoff can still reach it |
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 name, 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 |
40
49
  | 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 |
41
- | 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 watches one — and the first card write joins it too, so nothing has to be armed by hand. `/board status` says which team a `/board` in this session put you on |
50
+ | Join the team | `/board`, or the first card you claim | Opening the page joins this session to the team — as `team-lead` when it serves the page, under its own `team-agent-…` name 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. `/board status` reports the team this session is on, whichever door it used |
42
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 |
43
52
 
44
53
  ## Quickstart
@@ -60,11 +69,11 @@ lead: board list round: true
60
69
 
61
70
  | Command | Who | Effect |
62
71
  |---|---|---|
63
- | `/board` | lead | Serves the page as `team-lead`, or opens the one another session already serves |
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 repository and the address |
64
73
  | `/board team` | lead | The same, opened on the Team tab |
65
- | `/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 |
66
- | `/board stop` | lead | Stops the page this session serves; on a watching session it says who serves it |
67
- | `/team` | both | The round in the terminal: the rollup, then who is live and who has gone quiet |
74
+ | `/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
+ | `/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
+ | `/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 |
68
77
 
69
78
  **Who serves the page.** The first `/board` in a repository serves it; after that the
70
79
  page is found by the note it leaves beside the cards (`page.json`, in the board's own
@@ -72,18 +81,32 @@ directory), so a second session opens the same page rather than a rival one. Tha
72
81
  while a page answers; if nobody is serving, the next `/board` serves — the seat's name
73
82
  does not change, so a card a browser claimed is still the seat's. A note a session left
74
83
  behind when it died is not a dead end either: `/board` says which page it named — with its
75
- URL — takes the note over, and serves.
84
+ URL — takes the note over, and serves. A clean exit is not that case: `/board stop` and a
85
+ session shut down normally both take their note with them, so the note that survives is
86
+ one a crash left. `/board status` probes the note it is watching, and names one that
87
+ answers nobody; `/board stop` then drops it (a token-safe clear, so a page that came back
88
+ keeps its own note) instead of waiting for the next `/board` to trip over it.
76
89
 
77
90
  **Joining.** One thing, done for you: `/board` puts this session on the team — as
78
91
  `team-lead` when it serves the page, under its own name when it watches one — and so does
79
- the first card you work. The join is also the listening: mail is reported at that
80
- session's next turn, and no turn is raised, so being on the team never spends a turn. A
81
- session that has neither opened the board nor touched a card is not on it. `/board
82
- status` reports the join `/board` made itself: a session that joined by writing a card
83
- is on the team, and its status line does not say so.
84
-
85
- The page is still a seat: it writes as `team-lead` with an id of its own, so a card it
86
- claimed comes back with `reclaim` rather than a `release` from the terminal.
92
+ the first card it claims. A read, a new card, a note, a plan, or an edit enlists nobody: a
93
+ teammate is a session that holds work. The join is also the listening: mail is reported at
94
+ that session's next turn, and no turn is raised, so being on the team never spends a turn.
95
+ A session that has neither opened the board nor claimed a card is not on it.
96
+
97
+ **The two names.** A session's team name is the seat — `team-lead` — while it serves the
98
+ page, and `team-agent-` plus the last six characters of its session id otherwise, so
99
+ `billing-core` on this machine is `team-agent-9f21ac` to the team. The name is derived from
100
+ the id and not from Pi's session name, which means a rename cannot strand a card, two
101
+ sessions cannot settle on one name, and nothing here ever renames your session: `/team join
102
+ <label>` is the one door that does. A card records that name as its owner, so the chip, the
103
+ roster, and the bus record are one word — and `/board status` says it whichever door put
104
+ this session on the team, including a claim made with no page open.
105
+
106
+ The page still serves as a seat: `/board status` names `team-lead`, the toolbar chip says
107
+ so, and a card a browser claimed **before** the page stopped writing still names
108
+ `board:team-lead` as its owner — `reclaim` is how a lead takes that back from the
109
+ terminal.
87
110
 
88
111
  ## Tool
89
112
 
@@ -92,72 +115,101 @@ claimed comes back with `reclaim` rather than a `release` from the terminal.
92
115
 
93
116
  | Action | Parameters | Result |
94
117
  |---|---|---|
95
- | `list` | `status?`, `mine?`, `card?`, `query?`, `round?` | the cards in lane order, one card, or what a query or `mine` matches; `round: true` adds the rollup, which always covers the whole board |
118
+ | `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 |
96
119
  | `add` | `title`, `paths?`, `priority?`, `note?` | the new `todo` card |
97
120
  | `claim` | `card` | the card, now `doing` and owned by this session |
98
- | `release` | `card` | the card, back in `todo` |
121
+ | `release` | `card` | the card, back in `todo`, keeping any block it recorded |
99
122
  | `reclaim` | `card` | the card, back in `todo`, whoever held it |
100
- | `move` | `card`, `status` | the card in its new lane |
123
+ | `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 |
101
124
  | `note` | `card`, `text` | the card with the note appended |
102
125
  | `plan` | `card`, `path` | the card with its plan recorded |
103
126
  | `edit` | `card`, `title?`, `paths?`, `priority?`, `dependsOn?` | the card with what it says corrected; an omitted field is left alone, and `dependsOn: []` removes every link |
104
- | `drop` | `card` | the id that was dropped |
127
+ | `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 |
128
+ | `drop` | `card` | the id that was dropped, whose number is now spent |
105
129
 
106
130
  `card` takes `4` or `#4`. A refused call is an error result that still carries the
107
131
  card, so the next attempt starts from the truth.
108
132
 
133
+ The parameters are one flat object, so a field is published to every action at once.
134
+ An action reads only the fields in its row above, and a call carrying one that its
135
+ action does not read is **refused**, naming the action that takes it — not ignored.
136
+ `add` with `dependsOn` used to create the card and drop the link, so a caller could
137
+ not tell a recorded link from an ignored one:
138
+
139
+ ```
140
+ add does not act on dependsOn: `edit` takes it.
141
+ ```
142
+
143
+ To link a new card, add it and then edit it. `ACTION_FIELDS` in `types.ts` is the
144
+ table that answers this, and a test holds every published parameter to an action that
145
+ acts on it.
146
+
109
147
  ## Routes
110
148
 
111
- Every route but three needs a token, and which one a request carries decides what it may
112
- do: the lead's satisfies every route and is in the URL its browser was opened at; the one
113
- the note carries satisfies the reads and gets a 403 on every write. The three that do not
114
- are the page's own furniture — `/favicon.ico`, `/theme.js`, and `/app.js` are the same
115
- bytes to every reader and can neither read nor change a card, so refusing them to a
116
- fetch that has no token would only break the page. A write is made as the page's
117
- seat — `team-lead` — not as the session that opened it.
149
+ Every route but two needs the token, and there is exactly one: 128 random bits minted
150
+ when the page starts, compared in constant time, in the URL the browser was opened at.
151
+ The two that do not are the page's own furniture — `/theme.js` and `/app.js` are the same
152
+ bytes to every reader and can neither read nor change a card, so refusing them to a fetch
153
+ that has no token would only break the page. Everything below a
154
+ token is a **read**: the method list is `GET` and `HEAD`, so a `POST` or a `DELETE` is
155
+ refused by the envelope — `405`, `Allow: GET, HEAD` — before the token is even read.
156
+
157
+ The tab's icon is not a route: the document head links the mark as an SVG data URI, so
158
+ a browser fetches the icon out of the page it already has, and `/favicon.ico` is one more
159
+ path the board does not serve.
118
160
 
119
161
  ```
120
- GET /favicon.ico 204 and no body — no token
121
162
  GET /theme.js the theme script — no token
122
163
  GET /app.js the client script — no token
123
- GET / the document (?view=board|team)
164
+ GET / the document (?view=board|team), whose tab reads
165
+ board · <repo> · <N> open
166
+ GET /index.html the same document, for a link that names it
167
+ GET /api/ping whether the page answers, and nothing else: a session
168
+ deciding whether to attach reads this, not the snapshot
124
169
  GET /api/board the snapshot
125
170
  GET /events the live frame stream
126
- POST /api/cards add a card
127
- POST /api/cards/:id/… claim, release, reclaim, assign, move, note, plan, edit
128
- DELETE /api/cards/:id drop a card
129
171
  ```
130
172
 
173
+ `/api/ping` exists so a probe cannot be queued behind the page's own work: the snapshot
174
+ is a store read and a roster read on the same event loop, and a page that is busy
175
+ answering its browser would otherwise be declared dead and taken over.
176
+
131
177
  ## Layout
132
178
 
133
179
  One page, two tabs, one lifecycle. The kit shell keeps the toolbar and the footer
134
- pinned and makes the main pane the only scroll box; the page adds no second one.
180
+ pinned and makes the main pane the only scroll box; the page adds no second one. The
181
+ footer is one line about the board itself: the repository's name, the store's path, the
182
+ count of cards, and the size they occupy — plus the files this build could not read —
183
+ with the repository root's whole path on the hover title, which is what tells two
184
+ clones of one repository apart. The tab carries the repository's name for the same
185
+ reason: a store key is a hash, and no reader can say it out loud.
135
186
 
136
187
  | Band | What it holds |
137
188
  |---|---|
138
- | Toolbar | the mark, the seat the page writes 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, **+ New card**, the keys button, and the theme toggle |
139
- | New card dialog | the modal the toolbar's button (or `n`) opens: **Title** on its own line, the loudest field on the page, with a `n/120` count once you type; **Paths** under it, space-separated hints; **Priority** as a three-word list; the hint line; and one closer, `Cancel`, beside **Add card**. The title is focused on open, the dialog stays open after an add so a lead can publish several cards, and a draft survives `Esc`, the backdrop, and `Cancel` is the only thing that discards it. The same dialog is the card dialog's **Edit**: prefilled, headed `Edit card #4`, with **Save** in place of Add — and leaving that mode throws its text away, because that text was the card's, not a draft |
140
- | Lanes | trays, not a bare list: four columns 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 |
141
- | Cards | the id and a two-line title (the page's loudest text), an owner chip and an age, a muted priority chip when it is not `normal`, path chips, one reason line in one voice — what a card waits for, a blocked card's last note (or `blocked, with nothing recorded`), `no plan recorded`, `planned`, `✓ done by <owner>`, and the last note of a card that is none of those — clamped, with the whole sentence as its tooltip, and at most three actions — `Claim` or `Move` bordered, then `Note` and `Open`. A card that waits offers no `Claim`, because the claim would be refused: the reason line is what it says instead, and `Move` is drawn on any card still in flight, as the way into its dialog rather than a permission. A viewer's page draws `Open` alone, and no **+ New card**: the document says `data-writes="0"` and every affordance reads it. A hovered card lifts a pixel; a finished one recedes, title and owner chip muted |
142
- | 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, owner chip and `Reclaim` (or `Claim` when nobody holds it) on the first, the reason on the second 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). An unassigned card that waits on unfinished work is a row of its own kind, ranked just under a blocker, and offers no action: the claim the rules would refuse is not on the row at all. 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 |
143
- | Card dialog | the lane as a dot and a word beside the title, a meta line, the owner and its priority, the paths, what it waits for, the plan as monospace text, the notes as a timeline (who and when above the sentence, a rule between them), a note field, and `Move to` every lane but the card's own — a waiting card may be parked in `blocked`, and the rules turn down the two that put it back in flight — then `Assign to` for an unowned card, `Release`, `Edit`, `Drop`, `Close` |
189
+ | 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 |
190
+ | 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 |
191
+ | 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 title and a path chip 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. 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 |
192
+ | 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 |
193
+ | 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 |
194
+ (feat(team): an offer leaves a record, so its age can be read)
144
195
 
145
196
  Colour is a role: `--muted` for `todo`, `--accent` for `doing`, `--danger` for
146
197
  `blocked` and destructive choices, `--success` for `done`. The Team tab's decision
147
198
  rows sit on the kit's `--raised`, the one step above `--surface` that lets a block read
148
- as a plane instead of an outline; the Done lane sits on the page itself, so history
149
- stops competing with the lanes a lead works in. A card takes its colour
199
+ as a plane instead of an outline; every lane, `Done` included, keeps that `--surface`
200
+ tray, so a finished card still sits on something and history recedes through its muted
201
+ title and chip instead of through a hole in the row. A card takes its colour
150
202
  from the lane it sits in — the head's dot, and the reason line's tone — rather than
151
203
  from a bar of its own, so one card does not shout twice. The page states a lane and
152
204
  a card with `data-status`, so a theme change repaints it without a rule of its own.
153
205
 
154
206
  The board as a posting card — the same rules, drawn to be read at half width:
155
207
 
156
- <img src="../../assets/cards/board.png" alt="One board, every session: the page's seat and live badge; four lane trays, each with a dot, a count pill and a fold; one card carrying an id, a title, its age, a priority chip, a path chip, one reason line and its three actions; and the lead's round with two owner lines and the counts unassigned, unplanned, blocked, idle and done" width="100%">
208
+ <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%">
157
209
 
158
210
  *`board`: the lanes, one card's atoms, and the lead's round — three readings of one board, drawn by `screenshot:cards board`.*
159
211
 
160
- <img src="../../assets/cards/board-team.png" alt="The three team verbs as three bands: assign sends a handoff to a named peer, whose claim takes the card; 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%">
212
+ <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%">
161
213
 
162
214
  *`board-team`: `assign`, `offer`, and `reclaim` — the two that reach the bus, and the one that writes the card alone (`screenshot:cards team`).*
163
215
 
@@ -165,9 +217,8 @@ The board as a posting card — the same rules, drawn to be read at half width:
165
217
 
166
218
  | Key | What it does |
167
219
  |---|---|
168
- | `n` | New card: open the dialog and focus the title field |
169
- | `/` | Filter the cards by title, path, or owner |
170
- | `j` / `k` | Next or previous card |
220
+ | `/` | Filter the cards by title, holder, path hint, plan, or the text of a note |
221
+ | `j` / `k` | Next or previous card — or, with a card open, the next or previous card in the dialog |
171
222
  | `Enter` | Open the focused card |
172
223
  | `t` | Switch Board and Team |
173
224
  | `Esc` | Close a dialog, or clear the filter |
@@ -175,93 +226,152 @@ The board as a posting card — the same rules, drawn to be read at half width:
175
226
 
176
227
  A dialog traps `Tab`, closes on `Esc` or a backdrop click, and returns focus to
177
228
  whatever opened it — the kit's `dialogScript`, shared with the review and browse
178
- pages. A field keeps its keys: `n` inside the note input types an `n`.
229
+ pages. A field keeps its keys: `/` inside the filter box types a slash.
179
230
 
180
231
  ## Security
181
232
 
182
- - The page binds loopback only, answers only its own host names, and refuses a
183
- state-changing request another site made.
184
- - There are two credentials, both 128 random bits minted when the page starts and
185
- compared in constant time: the lead's, which can change a card, and a viewer's, which
186
- cannot. They are in the URL, never a cookie, and there is no CORS. The note beside the
187
- cards (`page.json`, owner-only) carries the viewer's and never the lead's.
188
- - A write is made **as the page's seat** — a browser is not an identity — so
189
- the toolbar names the session and claim/reclaim ask first.
190
- - The page writes only the board store under the agent directory. It never writes
191
- your repository, and it reads the session bus's own records read-only — labels,
192
- modes, heartbeats, message counts, and whether each is listening — to draw the Teammates list.
233
+ - The page binds loopback only, answers only its own host names, and has no
234
+ state-changing request to refuse.
235
+ - One credential, 128 random bits minted when the page starts and compared in constant
236
+ time, in the URL and never a cookie, with no CORS. The note beside the cards
237
+ (`page.json`, owner-only) carries it, so a second session can attach — and because the
238
+ page cannot change a card, pasting that URL hands out nothing but a view.
239
+ - The page reads only: the board store under the agent directory, and the session bus's
240
+ own records read-only — labels, modes, heartbeats, message counts, and whether each is
241
+ listening — to draw the Teammates list. It never writes your repository.
193
242
 
194
243
  ## Limitations
195
244
 
196
245
  - **Ownership is advisory.** Two sessions can still edit the same file; only
197
246
  `session-bus`'s path claims are a lock.
247
+ - **A listing is bounded, the rollup is not.** Nothing prunes a finished card by itself, so
248
+ the cards accumulate — most of this repository's own board was finished cards — and
249
+ reading them on every call is a cost that grows with the history rather than with the
250
+ work. A `list` that names no lane therefore leaves `done` out and prints the count, which
251
+ is what keeps the omission from reading as a smaller board; `status: "done"` reads them,
252
+ and `round` counts every card, because a rollup that hid half the board would not be one.
253
+ The page bounds its own View the same way, folding a long `done` lane. `archive` is how a
254
+ board stops paying for its history at all: the card leaves the board and its read.
198
255
  - **A finished card stays finished.** `done` is terminal for every transition — claim,
199
256
  assign, release, move, and plan all refuse it — so a card a dependency was recorded
200
- against cannot un-finish and reopen the order after the fact. A card finished by
201
- mistake is dropped and added again; its notes do not come with it.
257
+ against cannot un-finish and reopen the order after the fact, and `archive` does not
258
+ change that: it moves a `done` card's file, and never its lane. The one exception is
259
+ `edit`: a title, path hints, priority, or `dependsOn` can still be corrected by whoever
260
+ holds it, or by anyone while nobody does, because a title is what a reader reads and a
261
+ finished feature whose title is wrong would stay wrong for as long as the board keeps
262
+ history. A card finished by
263
+ mistake is dropped and added again: the new card takes a new number, and the dropped
264
+ one's title and lane stay in `ended/` under the id nothing will use again. Its notes
265
+ do not come with it.
266
+ - **A card file this build cannot read is reported, not skipped.** A file written by a
267
+ newer build, a half-written one, or a record moved under another name is named by
268
+ `list` with the reason and counted in the page's footer beside the store's size, so a
269
+ board that is not readable never reads as a board that is empty. `done` stays
270
+ terminal until a `reopen` is designed.
202
271
  - **Links are recorded by the tool.** A link names an id, so the page shows what a
203
272
  card waits for but does not edit it; `board edit` records it while the card is being
204
273
  planned, and `board drop` refuses to strand one.
205
- - **The board does not know whether a session is alive.** "Idle" means a `doing`
206
- card nobody has touched for 30 minutes, not that its process is gone; and the
207
- Teammates list reads the bus's records, where `live` is a heartbeat inside ten
208
- minutes, `quiet` is a record whose heartbeat has aged, and `no record` means only
209
- that the bus holds no record of that label — never that a session is dead. With
210
- `session-bus` unloaded, or before any session has joined here, every state reads
211
- `—` and the board works exactly as before.
212
- - **The seat and your terminal are two actors.** The page writes as `team-lead`, so a
213
- card a browser claimed is released from the browser or `reclaim`ed from the tool —
214
- `board release` in a session is a different actor and is refused. The Team tab marks
215
- your own row from the session you are reading in, not from the seat.
216
- - **A viewer's page is read-only, and the note is the only credential it carries.** A
217
- lead who pastes their own URL hands out write access; `/board status` prints the page,
218
- not that URL.
219
- - **Nothing prunes.** A `done` card stays until someone drops it; the page shows
220
- how many bytes the store occupies.
221
- - **A page claim is the seat's claim.** Two browsers on one page are one owner, and every
222
- browser that can reach the lead's URL writes as the same seat.
223
- - **The page stops a minute after its last tab leaves.** Closing the tab to run a
224
- command and coming back is fine, and a teammate's watching tab counts as one, so a
225
- page nobody is reading does not answer a port for the rest of the session. `/board`
226
- starts it again on the same port.
227
- - **The port is found, or pinned.** With no setting the page prefers a free port in
228
- 4780–5779, falls back to whatever free port the OS gives it if that one is taken, and
229
- remembers the answer for the session; `board.port` in `bytefly.json` pins it, so a
230
- bookmarked URL survives a restart. A pinned port that is already taken fails the start —
231
- `port 4780 is in use — change "port" in the bytefly.json board section, or stop what
232
- answers on it` — because a quiet fallback would move the page out from under the
233
- bookmark. A live note still wins: one repository has one page, so this session attaches to
234
- the port that note names rather than serving a second one.
274
+ - **The board knows when a process is gone, not when work is stuck.** The Teammates
275
+ list reads the bus's records, where `live` is a heartbeat inside ten minutes,
276
+ `quiet` is a heartbeat that has aged, `gone` is a record whose process is no longer
277
+ running, and `no record` means only that the bus holds no record of that label. A
278
+ `gone` owner's cards join **Needs a decision** with the reason that they are free, and a
279
+ *hung* session reads `live`, so "idle" still means what a card can know — nobody has
280
+ touched it for 30 minutes — rather than that its owner is stuck. With `session-bus`
281
+ unloaded, or before any session has joined here, every state reads `—` and the board
282
+ works exactly as before.
283
+ - **The page is read-only, deliberately.** A browser is not an identity: every page write
284
+ was made as the seat, and a page `assign` had to record that no handoff was sent. Rather
285
+ than duplicate the card rules in the client — which had already drifted from
286
+ `lib/board/rules.ts` — the write path is gone. What a reader loses is editing from the
287
+ browser; what the product keeps is every one of those verbs as a tool. The one capability
288
+ with no tool-side twin is assigning **without** messaging the peer: `team`'s `assign`
289
+ sends a handoff, and nothing else records an owner silently.
290
+ - **A seat can outlive its writes.** A card claimed from the page before this change still
291
+ names `board:team-lead` as its owner, and `reclaim` is how a lead takes it back.
292
+ - **A lane history is short, and it starts when the field did.** A card keeps the last
293
+ eight lane transitions, dropping the oldest and counting what it dropped, so the record
294
+ is bounded at write time rather than by a janitor — nothing prunes a board. A card
295
+ written before the log existed has no `in lane` row, and `updatedAt` keeps meaning what
296
+ it always meant: when the card was last touched, which is not when it last changed lane.
297
+ - **Nothing prunes itself.** A `done` card stays until someone sets it aside or drops it,
298
+ an archived card stays until someone drops it, and a dropped card leaves its record
299
+ behind for the same reason; the page shows how many bytes the board occupies and how
300
+ many files it could not read, and neither it nor the round counts the archive. Setting
301
+ history aside is a `board` call, not a threshold nobody stated, so what a read returns
302
+ never depends on how much has accumulated.
303
+ - **A page claim is a claim nobody cleans up.** A card a page claimed before this change
304
+ is the seat's until a tool reclaims it; the page itself can no longer add to that pile.
305
+ - **The page stops a minute after its last tab leaves, or two minutes after it starts
306
+ with no tab at all.** Closing the tab to run a command and coming back is fine, and a
307
+ teammate's watching tab counts as one. The second grace covers the page a browser never
308
+ opened — `openInBrowser` swallows a failure, and a prefix held by a page nobody saw is a
309
+ prefix held for the session. Either way `/board` starts it again, on the session's port,
310
+ and prints the address it reopened.
311
+ - **The prefix is `/board` on the session's one server.** The port belongs to `serve`, so
312
+ `/board`, `/browse`, and a browser plan review answer on one origin; `serve.port` in
313
+ `bytefly.json` pins that origin, so a bookmarked URL survives a restart. A live note still
314
+ wins: one repository has one page, so this session attaches to the page that note names
315
+ rather than serving a second one — and a session that attaches binds nothing at all.
235
316
  - **The note is believed only while it answers.** A page that crashed leaves its note
236
317
  behind; the next `/board` probes it, takes it over when nothing replies, and attaches to
237
- the page that won when two sessions start in the same instant. A note it cannot read at
318
+ the page that won when two sessions start in the same instant. A clean exit — `/board
319
+ stop`, or a session shut down normally — clears its own note, so the one that survives
320
+ is the one a crash left. A note it cannot read at
238
321
  all is named with its path — "another session is serving" is only said of a page that was
239
322
  seen answering.
323
+ - **A page that cannot read the board says so once.** A snapshot is built on every tick, and
324
+ a read can fail under a running page — the board's directory removed, or replaced by
325
+ something that is not one. The frame is dropped rather than raised, the session is told
326
+ once, and the next tick tries again, so a board that comes back is followed again without
327
+ a second `/board`; a notice per tick would be a transcript nobody reads.
240
328
 
241
329
  ## Pi integration
242
330
 
243
- - **`session-bus`**: a card's owner is the bus label, so a card names a session a
331
+ - **`session-bus`**: a card's owner is the session's team name, so a card names a session a
244
332
  peer can be addressed by. The board calls one thing on it — the join, through
245
333
  `lib/session-tool.ts`, because a command context cannot call a tool and the request
246
334
  therefore travels on `pi.events` — and reads its records otherwise, which is why the
247
- page works with the bus disabled. A failed join is a clause on the `/board` line, not
248
- a failed page. The seat is a different actor from the session that serves it, which is
249
- why `/board status` names both.
335
+ page works with the bus disabled. The join asks for the name the board already writes
336
+ and never renames the session, so the footer stays the user's. A failed join is a clause
337
+ on the `/board` line, not a failed page. The seat is the name the page is served and
338
+ reported under, and `/board status` says which session is on the team through it.
250
339
  - **`plan`**: `board plan` records a plan file's path; the plan extension owns the
251
- file. The plan is recorded **after** approval, because plan mode makes mutating
252
- tools unavailable.
340
+ file. The board stays available while planning, and only `list` works: the same tool
341
+ writes, so its read-only hint cannot say that, and plan mode reads the action in the
342
+ call instead — `add`, `claim`, or any other verb is refused with the exit guidance.
343
+ The plan is recorded **after** approval, when the writing verbs work again.
253
344
  - **`todo`**: the approved plan's steps are seeded into `todo`. Steps never become
254
- cards.
345
+ cards. A `list` call is a read for the lag reminder too, so reading the board never
346
+ forces a todo update, while `add` or `claim` counts as work like any other write.
255
347
  - **`worktree`**: the store is keyed by the repository's common git directory, so
256
348
  every worktree shares one board.
257
- - **`bytefly`**: the page's port appears as a `board` status chip, and `doctor`
258
- reports nothing new — the board needs no binary and no per-machine setup; its one
259
- setting, `board.port`, is a `bytefly.json` edit.
349
+ - **`serve`**: the page mounts `/board` on the session's one server, which draws the
350
+ single `◉ <port>` chip. **`bytefly`**: the bar shows that chip, and `doctor` reports
351
+ nothing new — the board needs no binary and no per-machine setup.
260
352
 
261
353
  ## Design notes
262
354
 
263
- - **One JSON file per card**, in `<agentDir>/board/<repo-key>/cards/`. The listing
264
- is the index, so a reader needs no cache and no daemon.
355
+ - **One JSON file per card**, in `<agentDir>/board/<repo-key>/cards/`, one per ended
356
+ card in `ended/`, and one per archived card in `archive/`. The listing is the index, so
357
+ a reader needs no cache and no daemon.
358
+ - **An id is never reused.** The next id is the highest in `cards/`, `archive/` *or*
359
+ `ended/` plus one, so a number cannot be handed to a second feature — which is the only
360
+ thing that lets a note, a handoff, or a link name a card and still mean it. A record
361
+ is written before its card is removed: a crash between the two leaves a card that
362
+ still exists with a record explaining it, never a card that is gone unexplained.
363
+ - **An archive is a second pile, not a second state.** `archive` renames a finished
364
+ card's file into `archive/` and touches nothing inside it, so there is no new field, no
365
+ schema version, and no lane transition: the record is exactly what it was, and the
366
+ directory it lies in is the whole fact. That is why `ended/` keeps its one reason — a
367
+ drop still leaves a record because the card is gone, while an archived card is not gone
368
+ at all — and why a read by id looks in both piles: a card is where it lies. The
369
+ decision is explicit because nothing in this board prunes by itself, and because an
370
+ automatic sweep would make `status: "done"` return something different past a threshold
371
+ nobody could see. The board, the rollup, and the page read `cards/` alone — the only
372
+ look at the archive is the directory listing a lane-less `list` counts, so the omission
373
+ cannot pass for a deleted history — and an archive therefore lowers the board's size
374
+ and the cost of every read. The ids stay spent because the file name is the id.
265
375
  - **Create is exclusive**, by hard-linking a temporary file onto the target name,
266
376
  so two adds cannot share an id. **Update** re-reads, re-checks the operation's
267
377
  own preconditions, then renames over the old file: an operation means "apply when
@@ -270,62 +380,127 @@ pages. A field keeps its keys: `n` inside the note input types an `n`.
270
380
  only when a card's revision, or a teammate's state, mail, or listening flag, moves: a
271
381
  heartbeat every ten seconds is not news, and a frame nobody needs costs the page a
272
382
  parse.
383
+ - **A query reads what a card says, not just what it is called.** A card's reasoning lives
384
+ in its notes and in the plan it is executing, so leaving both out of the search made the
385
+ two fields that hold the argument the two a reader could not find. The order is the
386
+ answer's weight — title, holder, path hints, plan, notes — and it is one body spliced
387
+ into the page from `lib/board/card.ts`, so the filter box and `board list query:` cannot
388
+ disagree about which card a word names.
389
+ - **The record is a ladder, not a paragraph run.** A card's facts are one per row so each
390
+ can be read and copied alone: the meta line it replaces merged id, updated, and who into
391
+ one sentence, and named the last writer as though it were the author. The notes are the
392
+ cargo — they read at body size on a measure, with their attribution under them — and the
393
+ dialog's body is the only scroll, because a second scrollbar inside a 70vh panel hid most
394
+ of what a reader opened it for. A long plan path elides on one line with the whole value
395
+ on its hover title: the plan is the reason to open a card, and it used to shred across
396
+ three lines of `--dim`.
397
+ - **The walk is over the board as drawn.** `j`/`k` step through the cards the page has
398
+ boxes for, so a folded Done lane and a filter are both respected; opening a card does not
399
+ take the keyboard away, it points it at the next record. The position is announced
400
+ because a title that changes inside an open modal is not.
401
+ - **A class is a symbol, not a string.** The document and the bundle cannot import from
402
+ each other, so both read `view/names.ts` — the only place a `board-*` class is spelled.
403
+ A name assembled from a prefix is what made the old contract untestable: it passed on
404
+ the prefix and could not see the whole name, so a rule with nothing behind it (or a
405
+ name with no rule) survived until a capture showed it.
406
+ - **The page is given what it prints.** A card arrives with the priority it acts on and
407
+ the unfinished dependencies that block it (`waits`, as `#2 (doing)`), so the client
408
+ holds no copy of a rule `lib/board` owns — the copies it did hold had already drifted
409
+ from the tool's. The one body that cannot cross as data is `plural`, and it crosses as
410
+ source (`PLURAL_JS`), because a second wording for `1 card` is the same mistake in a
411
+ smaller place.
412
+ - **An offer is a record, not a reservation.** `team offer` broadcasts a card and then
413
+ writes `offeredAt` — the moment, and nothing else — because an event leaves nothing
414
+ behind and a card three sessions ignored read exactly like one nobody had looked at.
415
+ Every answer clears it: a claim, an assignment, and a release each delete the field, so
416
+ its presence means the offer is still open. The page reads it in two places and the
417
+ tool in a third — `offered 1m ago · nobody took it` under the card, `offered 1m ago —
418
+ nobody took it` as the Team tab's reason, `offered: 1m ago, and nobody took it` in
419
+ `list <card>` — and the age is `lib/format.ts`'s one rule, handed to the page's bundle
420
+ as source so the transcript and the page cannot say one age two ways.
421
+ - **`waits on` answers two questions, on purpose.** The page names the *unfinished*
422
+ dependencies in one line — "why can I not start" — and `board`'s result names every
423
+ dependency with its lane, including finished ones — "what is this card tied to". Both
424
+ read `links.ts`, so neither can invent a blocker; the difference is the question, not
425
+ a drift.
426
+ - **A link is stored once, and read both ways.** `dependsOn` lives on the waiting card
427
+ alone, so nothing on disk says what a card is holding up; `openDependentsOf` answers
428
+ it over the board, `list <card>` prints `waited on by N cards: #2 (todo)`, and the
429
+ dialog has the rung the card's own row cannot. Both skip a finished card, unlike the
430
+ drop refusal, which keeps every edge: "what is held up" is about work, and "would this
431
+ drop strand a link" is about the file.
273
432
  - **One page is found, not configured.** A note beside the cards carries the port, the
274
- seat, and a read-only token; it is created exclusively, so two sessions starting at
433
+ seat, and the token; it is created exclusively, so two sessions starting at
275
434
  once cannot both serve, and the loser attaches to the winner. The alternative —
276
435
  a setting that names the lead — was rejected: the role moves, the seat's name does
277
- not. The port is the one thing a file may pin: `board.port` states where the page
278
- listens, never who serves it.
279
- - **The new card is a dialog, not a fixture.** The page opens on the board, and an
280
- empty board leaves its own explanation in place rather than covering it with the
281
- dialog — the empty state's **Add the first card** is what opens it. A card
282
- is something you ask to write. It is the kit's modal, so `Tab` stays inside it and
283
- closing returns focus to the button that opened it; a draft survives `Esc` or a
284
- backdrop click, and `Cancel` is the one way to throw it away.
436
+ not. The port in the note is the session's, never a number this page chose.
437
+ - **The page is a view because a browser is not an identity.** Every page write was made
438
+ as the seat, so a page `assign` had to write down that nobody had been told, and the
439
+ client's copy of the card rules drifted from `lib/board/rules.ts` — it offered a drop
440
+ the server refused. The write path was deleted rather than hidden behind a flag: a
441
+ disabled verb is still a verb the page has to decide. Deleting it took the composer's
442
+ dialog, the confirm dialog, the note field, and 60-odd buttons with it.
285
443
  - **One edge, stored once.** A link is one id in the waiting card's file and the
286
- reverse edge is computed over the board, because a `blockedBy` copy of the same edge
287
- is a copy that drifts; the cycle check and the drop rule are what keep the graph
288
- whole instead.
444
+ reverse edge is computed over the board, because a copy of the same edge on the card
445
+ being waited for is a copy that drifts; the cycle check and the drop rule are what
446
+ keep the graph whole instead.
289
447
  - **A priority is a field, not a lane.** The four lanes are the board's vocabulary and
290
448
  a fifth would double the counts and the heads; inside a lane, priority then age is the
291
449
  order, and a card says its priority only when it is not the ordinary one.
292
450
  - **`edit` leaves an omitted field alone.** It is the one action whose job is "make it
293
- say this", so an absent field must mean "not this field" — otherwise the page, which
294
- shows no links, would clear every link it saved.
451
+ say this", so an absent field must mean "not this field" — a caller that sends only a
452
+ title must not clear the path hints and links it never mentioned.
453
+ - **A lane change is recorded where every transition ends.** `applyOp` decides each
454
+ transition, but `write()` in `rules.ts` is the one place all of them pass through, so
455
+ the log is appended there rather than from six operations that would each have to
456
+ remember it. A write that leaves the lane alone — a note, a plan, an edit, a claim by
457
+ the holder — appends nothing, because the question the log answers is "how long has
458
+ this been blocked", and `updatedAt` cannot answer it: filing a note against a blocked
459
+ card moves that stamp without moving the card.
295
460
  - **Rejected:** steps as cards (they flood the board), a shared database (a file per
296
461
  card needs no dependency and no lock), an index file (every write maintains it,
297
462
  every read can find it stale), drag-and-drop (a lane change is a named `move`), a
298
- priority lane (four lanes are the vocabulary), and a reverse `blockedBy` field (two
299
- copies of one edge).
463
+ priority lane (four lanes are the vocabulary), a reverse link field (two copies of
464
+ one edge), and an automatic archive past some number of finished cards (a threshold
465
+ nobody stated would decide what a read returns, and nothing else here prunes).
466
+ - **A block says why, and a finished card says with what.** `blocked` exists to ask a
467
+ person for a decision, so the move into it records the sentence — `blockedBy`, one
468
+ line — and the page draws that rather than the last note somebody wrote, which was a
469
+ guess. The reason survives a `release` (the decision is still pending) and is cleared
470
+ on the way back into `doing` or `done` (a stale sentence reads as a current one).
471
+ `landed` is the commit `HEAD` resolved to at the move to `done`, recorded because
472
+ `done` is terminal and nothing later can say which change closed the card. Both
473
+ fields are optional and neither is required: a card may be blocked with nothing
474
+ recorded, and one finished before the field, or outside a repository, has no commit
475
+ to record — finishing never depends on a git read.
300
476
 
301
477
  ## Files
302
478
 
303
479
  | File | Responsibility |
304
480
  |---|---|
305
481
  | `index.ts` | The factory: gate on `PI_DISABLED_EXTENSIONS`, build the runtime and the page, register both, close on shutdown |
306
- | `config.ts` | The board's one setting: the port its page is pinned to, clamped, from the `bytefly.json` `board` section |
307
482
  | `schema.ts` | The tool's parameters |
308
- | `types.ts` | The shapes the tool registers and reports: the card an action produced, the cards a list read, the rollup when it was asked for, and the card a refusal carries |
483
+ | `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 |
309
484
  | `tools.ts` | Action dispatch and the refusal shape |
310
- | `input.ts` | A card body as both surfaces read it: the `add` fields normalized once, so the tool and the page cannot disagree |
485
+ | `input.ts` | A card body as the tool reads it: the `add` fields normalized in one place |
311
486
  | `format.ts` | The model-facing text |
312
487
  | `rows.ts` | The transcript row grammar |
313
488
  | `result.ts` | The transcript block |
314
- | `page.ts` | The page's lifecycle: the port it remembers or is pinned to, the chip that names it, and the three closes — `/board stop`, the idle grace, and shutdown |
315
- | `snapshot.ts` | The page's snapshot: the cards, the bus's records, and the store's size, handed to `buildView` |
316
- | `writes.ts` | The page's write path: one request to one store call, 422 for a field and 409 for a card somebody else moved |
317
- | `commands.ts` | `/board`, its three words, and the one line `/board status` prints |
318
- | `seat.ts` | The seat the page serves as: one name, and an id derived from it, so the page is one actor across restarts |
319
- | `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 |
320
- | `transport/router.ts` | The routes, what a credential may do, and the guards in front of them |
321
- | `transport/server.ts` | The loopback listener, the frame push, and the close |
322
- | `view/page.ts` | The document: an inert shell the client fills |
489
+ | `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` |
490
+ | `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` |
491
+ | `commands.ts` | `/board`, its three words and their refusals, and the one line `/board status` prints |
492
+ | `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 |
493
+ | `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 |
494
+ | `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 |
495
+ | `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 |
496
+ | `view/page.ts` | The document: an inert shell the client fills, with no form on it |
323
497
  | `view/styles.ts` | The page's `board-*` layout rules |
324
498
  | `view/render.ts` | The client's snapshot → DOM renderer: toolbar, lanes, cards, the dialog's contents |
499
+ | `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 |
325
500
  | `view/team.ts` | The client's round: the two tables and the Done line |
326
- | `view/actions.ts` | The client's writes and its two kit dialogs |
327
- | `view/keys.ts` | The key table, and the bindings that make it true |
328
- | `view/live.ts` | The client's tabs, new-card dialog, filter, and frame stream |
501
+ | `view/dialog.ts` | The client's card dialog: open one card, close it, and forget it when the kit closes it |
502
+ | `view/keys.ts` | The key table, and the bindings that make it true — including `j`/`k` walking the open dialog through the drawn cards |
503
+ | `view/live.ts` | The client's tabs, filter, and frame stream |
329
504
  | `view/client.ts` | The one script served at `/app.js` |
330
505
 
331
506
  The store, the rules, the rollup, and the snapshot are `lib/board/` — see
@@ -338,7 +513,10 @@ The store, the rules, the rollup, and the snapshot are `lib/board/` — see
338
513
  record, the dependency graph, the note a page leaves, the rollup and its text, the card's
339
514
  transitions, the store a call opens and the identity it writes as, the files that store
340
515
  keeps, and the snapshot, and
341
- `extensions/board/{input,seat,tools,page,commands,config,extension,transport/router,view/pages}`
516
+ `extensions/board/{input,seat,tools,page,commands,extension,transport/router,view/pages,view/behaviour}`
342
517
  for the card body, the seat, every action, the page's lifecycle over a real socket (a page
343
- served, attached to, taken over, and refused to a viewer), the command, the `board.port`
344
- setting, the factory, the routes, and the document and client bundle.
518
+ served, attached to, and taken over, a board that stops being readable, and a write refused
519
+ on the wire), the command, the
520
+ factory, the routes, the document and client bundle, and the
521
+ page driven in a browser — a card opened from its title, the filter, the fold, a live
522
+ frame, and the dialog's ladder.