@gavin-hu/my-pi-agent 0.21.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 (254) hide show
  1. package/README.md +46 -31
  2. package/README.zh-CN.md +40 -26
  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 -24
  9. package/docs/extensions/board.md +361 -147
  10. package/docs/extensions/bytefly.md +152 -111
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +30 -31
  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 +166 -126
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +34 -18
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +72 -33
  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 +56 -15
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +5 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +4 -40
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +13 -11
  38. package/extensions/board/result.ts +30 -16
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +42 -10
  41. package/extensions/board/snapshot.ts +2 -1
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +131 -44
  44. package/extensions/board/types.ts +36 -32
  45. package/extensions/board/view/card.ts +229 -0
  46. package/extensions/board/view/client.ts +33 -15
  47. package/extensions/board/view/dialog.ts +169 -6
  48. package/extensions/board/view/empty.ts +47 -0
  49. package/extensions/board/view/keys.ts +13 -2
  50. package/extensions/board/view/live.ts +89 -21
  51. package/extensions/board/view/names.ts +13 -4
  52. package/extensions/board/view/page.ts +23 -9
  53. package/extensions/board/view/render.ts +208 -271
  54. package/extensions/board/view/styles.ts +123 -31
  55. package/extensions/board/view/team.ts +297 -76
  56. package/extensions/bytefly/block/fallback.ts +1 -1
  57. package/extensions/bytefly/doctor/runtime.ts +5 -2
  58. package/extensions/bytefly/footer/collect/label.ts +60 -0
  59. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  60. package/extensions/bytefly/footer/config.ts +4 -2
  61. package/extensions/bytefly/footer/register.ts +33 -4
  62. package/extensions/bytefly/footer/render/lines.ts +7 -4
  63. package/extensions/bytefly/footer/render/segments.ts +37 -10
  64. package/extensions/bytefly/footer/render/status.ts +0 -11
  65. package/extensions/bytefly/footer/types.ts +7 -0
  66. package/extensions/bytefly/header/config.ts +1 -1
  67. package/extensions/bytefly/index.ts +32 -6
  68. package/extensions/bytefly/mode/editor.ts +115 -0
  69. package/extensions/bytefly/panel/commands.ts +210 -120
  70. package/extensions/bytefly/panel/format.ts +4 -4
  71. package/extensions/bytefly/panel/settings.ts +73 -6
  72. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  73. package/extensions/bytefly/update/cache.ts +1 -1
  74. package/extensions/bytefly/update/register.ts +4 -4
  75. package/extensions/bytefly/working/config.ts +5 -7
  76. package/extensions/bytefly/working/format.ts +10 -13
  77. package/extensions/bytefly/working/register.ts +22 -27
  78. package/extensions/check/commands.ts +3 -2
  79. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  80. package/extensions/context/commands.ts +2 -2
  81. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  82. package/extensions/file-browse/index.ts +67 -36
  83. package/extensions/file-read/render.ts +4 -4
  84. package/extensions/goal/plan-route.ts +2 -2
  85. package/extensions/job/child.ts +527 -0
  86. package/extensions/job/clock.ts +142 -0
  87. package/extensions/job/commands.ts +4 -2
  88. package/extensions/job/config.ts +8 -1
  89. package/extensions/job/format.ts +22 -1
  90. package/extensions/job/log-writer.ts +13 -2
  91. package/extensions/job/registry.ts +63 -7
  92. package/extensions/job/render.ts +34 -0
  93. package/extensions/job/runtime.ts +296 -655
  94. package/extensions/job/session.ts +132 -19
  95. package/extensions/job/store.ts +7 -7
  96. package/extensions/job/tools.ts +3 -3
  97. package/extensions/job/types.ts +47 -0
  98. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
  99. package/extensions/job/waiters.ts +44 -1
  100. package/extensions/memory/commands.ts +4 -2
  101. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  102. package/extensions/plan/commands.ts +12 -9
  103. package/extensions/plan/format.ts +8 -2
  104. package/extensions/plan/git-bash.ts +115 -16
  105. package/extensions/plan/index.ts +53 -52
  106. package/extensions/plan/plan-index.ts +4 -2
  107. package/extensions/plan/plans.ts +5 -54
  108. package/extensions/plan/policy.ts +8 -10
  109. package/extensions/plan/review-lazy.ts +85 -0
  110. package/extensions/plan/review.ts +2 -2
  111. package/extensions/plan/runtime.ts +49 -39
  112. package/extensions/plan/sections.ts +1 -1
  113. package/extensions/plan/steps.ts +3 -3
  114. package/extensions/plan/todos.ts +1 -1
  115. package/extensions/plan/tools/cancel.ts +27 -62
  116. package/extensions/plan/tools/enter.ts +55 -81
  117. package/extensions/plan/tools/exit.ts +221 -238
  118. package/extensions/plan/tools/refusal.ts +18 -0
  119. package/extensions/plan/tools/steps.ts +75 -92
  120. package/extensions/plan/tools/write.ts +77 -120
  121. package/extensions/plan/tools.ts +188 -30
  122. package/extensions/plan/tui.ts +5 -5
  123. package/extensions/plan/types.ts +48 -5
  124. package/extensions/plan/web-client.ts +3 -0
  125. package/extensions/plan/web-lazy.ts +53 -0
  126. package/extensions/plan/web-markdown.ts +9 -67
  127. package/extensions/plan/web-payload.ts +3 -3
  128. package/extensions/plan/web-router.ts +18 -9
  129. package/extensions/plan/web.ts +1 -12
  130. package/extensions/question/tools.ts +6 -2
  131. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  132. package/extensions/rewind/commands/commands.ts +6 -3
  133. package/extensions/rewind/state/runtime.ts +1 -1
  134. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  135. package/extensions/rewind/view/review-tui.ts +4 -4
  136. package/extensions/serve/config.ts +10 -8
  137. package/extensions/serve/index.ts +17 -3
  138. package/extensions/serve/registry.ts +26 -17
  139. package/extensions/session-bus/commands.ts +11 -6
  140. package/extensions/session-bus/join-request.ts +2 -1
  141. package/extensions/session-bus/mail-watch.ts +26 -17
  142. package/extensions/session-bus/protocol.ts +15 -3
  143. package/extensions/session-bus/resolve.ts +5 -7
  144. package/extensions/session-bus/runtime.ts +40 -12
  145. package/extensions/session-bus/store.ts +101 -19
  146. package/extensions/session-bus/types.ts +19 -0
  147. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  148. package/extensions/subagent/agents.ts +1 -1
  149. package/extensions/subagent/commands.ts +4 -2
  150. package/extensions/subagent/format.ts +15 -15
  151. package/extensions/subagent/index.ts +1 -1
  152. package/extensions/subagent/render.ts +3 -3
  153. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  154. package/extensions/team/commands.ts +24 -7
  155. package/extensions/team/dispatch.ts +18 -4
  156. package/extensions/team/format.ts +17 -2
  157. package/extensions/team/index.ts +9 -8
  158. package/extensions/team/schema.ts +6 -5
  159. package/extensions/team/tools.ts +107 -45
  160. package/extensions/team/types.ts +3 -2
  161. package/extensions/todo/commands.ts +4 -2
  162. package/extensions/todo/format.ts +11 -0
  163. package/extensions/todo/render.ts +57 -0
  164. package/extensions/todo/runtime.ts +1 -1
  165. package/extensions/todo/tools.ts +1 -1
  166. package/extensions/todo/view/todo-screen.ts +122 -0
  167. package/extensions/todo/widget.ts +47 -0
  168. package/extensions/watch/commands.ts +4 -2
  169. package/extensions/watch/format.ts +36 -7
  170. package/extensions/watch/render.ts +57 -0
  171. package/extensions/watch/runtime.ts +27 -22
  172. package/extensions/watch/tools.ts +4 -4
  173. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  174. package/extensions/web-access/fetch/tool.ts +12 -4
  175. package/extensions/wechat/commands/commands.ts +18 -14
  176. package/extensions/wechat/index.ts +72 -37
  177. package/extensions/wechat/state/credentials.ts +1 -1
  178. package/extensions/wechat/state/state.ts +1 -1
  179. package/extensions/wechat/text/status.ts +112 -0
  180. package/extensions/wechat/types.ts +2 -2
  181. package/extensions/wechat/view/status-screen.ts +4 -103
  182. package/extensions/worktree/commands.ts +11 -9
  183. package/extensions/worktree/enter.ts +2 -2
  184. package/extensions/worktree/exit.ts +2 -2
  185. package/extensions/worktree/index.ts +5 -5
  186. package/extensions/worktree/screen.ts +3 -3
  187. package/extensions/worktree/status.ts +2 -2
  188. package/extensions/worktree/tools.ts +113 -116
  189. package/lib/README.md +27 -13
  190. package/lib/board/actions.ts +111 -0
  191. package/lib/board/artifact.ts +149 -0
  192. package/lib/board/card.ts +163 -52
  193. package/lib/board/config.ts +58 -0
  194. package/lib/board/contract.ts +113 -0
  195. package/lib/board/join.ts +38 -0
  196. package/lib/board/lanes.ts +182 -0
  197. package/lib/board/lead.ts +130 -0
  198. package/lib/board/limits.ts +78 -0
  199. package/lib/board/links.ts +31 -10
  200. package/lib/board/page-pointer.ts +1 -1
  201. package/lib/board/read.ts +83 -0
  202. package/lib/board/reason.ts +102 -0
  203. package/lib/board/round-text.ts +68 -6
  204. package/lib/board/round.ts +31 -15
  205. package/lib/board/rules.ts +151 -307
  206. package/lib/board/seat.ts +35 -0
  207. package/lib/board/session.ts +136 -54
  208. package/lib/board/skipped.ts +22 -0
  209. package/lib/board/steps.ts +487 -0
  210. package/lib/board/store.ts +87 -30
  211. package/lib/board/tone.ts +92 -0
  212. package/lib/board/view.ts +70 -58
  213. package/{extensions/watch → lib}/child.ts +40 -21
  214. package/lib/env.ts +65 -7
  215. package/lib/format.ts +13 -0
  216. package/lib/http/index.ts +14 -12
  217. package/lib/http/port.ts +15 -10
  218. package/lib/lazy.ts +45 -0
  219. package/lib/plan-entry.ts +39 -10
  220. package/lib/plans/paths.ts +82 -0
  221. package/lib/policy.ts +9 -6
  222. package/lib/report.ts +3 -2
  223. package/lib/requirements.ts +8 -8
  224. package/lib/session-label.ts +111 -5
  225. package/lib/session-roster.ts +43 -1
  226. package/lib/session-tool.ts +5 -4
  227. package/lib/stray-fields.ts +7 -2
  228. package/lib/tool-names.ts +5 -10
  229. package/lib/tui/README.md +23 -8
  230. package/lib/tui/chrome.ts +16 -15
  231. package/lib/tui/cursor.ts +18 -3
  232. package/lib/tui/glyphs.ts +9 -7
  233. package/lib/tui/tokens.ts +33 -2
  234. package/lib/tui/tool.ts +3 -3
  235. package/lib/web/README.md +21 -0
  236. package/lib/web/components.ts +5 -1
  237. package/lib/web/index.ts +1 -0
  238. package/lib/web/markdown.ts +83 -0
  239. package/lib/web/markup.ts +2 -1
  240. package/lib/web/runtime.ts +9 -10
  241. package/package.json +6 -1
  242. package/skills/init/SKILL.md +5 -5
  243. package/skills/next/SKILL.md +3 -5
  244. package/extensions/board/seat.ts +0 -28
  245. package/extensions/todo/tui.ts +0 -212
  246. package/lib/tui/index.ts +0 -34
  247. package/skills/project-docs/SKILL.md +0 -133
  248. package/skills/project-docs/references/audit.md +0 -131
  249. package/skills/project-docs/references/changelog.md +0 -105
  250. package/skills/project-docs/references/discovery.md +0 -127
  251. package/skills/project-docs/references/readme.md +0 -80
  252. package/skills/project-docs/references/release.md +0 -84
  253. package/skills/project-docs/references/roadmap.md +0 -93
  254. 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
+ ```
51
+
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.
56
+
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.
16
61
 
17
- *`/board`: the four lanes, with the round one tab away and every card opened by its title.*
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%">
18
63
 
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%">
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 (one line, at most 1000 characters) |
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 — one line, at most 1000 characters |
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 |
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 |
46
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 |
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 |
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 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 |
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,20 +106,30 @@ 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 repository and 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 |
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 |
74
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 |
@@ -87,26 +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
166
+ the lead's name when it serves the page, under its own name when it watches one — and so does
92
167
  the first card it claims. A read, a new card, a note, a plan, or an edit enlists nobody: a
93
168
  teammate is a session that holds work. The join is also the listening: mail is reported at
94
169
  that session's next turn, and no turn is raised, so being on the team never spends a turn.
95
170
  A session that has neither opened the board nor claimed a card is not on it.
96
171
 
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.
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.
110
193
 
111
194
  ## Tool
112
195
 
@@ -115,20 +198,25 @@ terminal.
115
198
 
116
199
  | Action | Parameters | Result |
117
200
  |---|---|---|
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 |
119
- | `add` | `title`, `paths?`, `priority?`, `note?` | the new `todo` card |
120
- | `claim` | `card` | the card, now `doing` and owned by this session |
121
- | `release` | `card` | the card, back in `todo`, keeping any block it recorded |
122
- | `reclaim` | `card` | the card, back in `todo`, whoever held it |
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 |
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 |
124
207
  | `note` | `card`, `text` | the card with the note appended |
125
- | `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>` |
126
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 |
127
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 |
128
213
  | `drop` | `card` | the id that was dropped, whose number is now spent |
129
214
 
130
215
  `card` takes `4` or `#4`. A refused call is an error result that still carries the
131
- 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.
132
220
 
133
221
  The parameters are one flat object, so a field is published to every action at once.
134
222
  An action reads only the fields in its row above, and a call carrying one that its
@@ -178,41 +266,53 @@ answering its browser would otherwise be declared dead and taken over.
178
266
 
179
267
  One page, two tabs, one lifecycle. The kit shell keeps the toolbar and the footer
180
268
  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.
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.
186
278
 
187
279
  | Band | What it holds |
188
280
  |---|---|
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)
195
-
196
- Colour is a role: `--muted` for `todo`, `--accent` for `doing`, `--danger` for
197
- `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
198
289
  rows sit on the kit's `--raised`, the one step above `--surface` that lets a block read
199
290
  as a plane instead of an outline; every lane, `Done` included, keeps that `--surface`
200
291
  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
202
- from the lane it sits in — the head's dot, and the reason line's tone — rather than
203
- from a bar of its own, so one card does not shout twice. The page states a lane and
204
- 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.
205
301
 
206
302
  The board as a posting card — the same rules, drawn to be read at half width:
207
303
 
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%">
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%">
209
305
 
210
306
  *`board`: the lanes, one card's atoms, and the lead's round — three readings of one board, drawn by `screenshot:cards board`.*
211
307
 
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%">
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%">
213
309
 
214
310
  *`board-team`: `assign`, `offer`, and `reclaim` — the two that reach the bus, and the one that writes the card alone (`screenshot:cards team`).*
215
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
+
216
316
  ## Keys
217
317
 
218
318
  | Key | What it does |
@@ -224,9 +324,13 @@ The board as a posting card — the same rules, drawn to be read at half width:
224
324
  | `Esc` | Close a dialog, or clear the filter |
225
325
  | `?` | The keyboard sheet |
226
326
 
227
- A dialog traps `Tab`, closes on `Esc` or a backdrop click, and returns focus to
228
- whatever opened it — the kit's `dialogScript`, shared with the review and browse
229
- 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.
230
334
 
231
335
  ## Security
232
336
 
@@ -248,26 +352,35 @@ pages. A field keeps its keys: `/` inside the filter box types a slash.
248
352
  the cards accumulate — most of this repository's own board was finished cards — and
249
353
  reading them on every call is a cost that grows with the history rather than with the
250
354
  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,
355
+ is what keeps the omission from reading as a smaller board; `lane: "done"` reads them,
252
356
  and `round` counts every card, because a rollup that hid half the board would not be one.
253
357
  The page bounds its own View the same way, folding a long `done` lane. `archive` is how a
254
358
  board stops paying for its history at all: the card leaves the board and its read.
255
- - **A finished card stays finished.** `done` is terminal for every transition — claim,
256
- assign, release, move, and plan all refuse it — so a card a dependency was recorded
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.
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.
266
376
  - **A card file this build cannot read is reported, not skipped.** A file written by a
267
377
  newer build, a half-written one, or a record moved under another name is named by
268
378
  `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.
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.
271
384
  - **Links are recorded by the tool.** A link names an id, so the page shows what a
272
385
  card waits for but does not edit it; `board edit` records it while the card is being
273
386
  planned, and `board drop` refuses to strand one.
@@ -288,7 +401,7 @@ pages. A field keeps its keys: `/` inside the filter box types a slash.
288
401
  with no tool-side twin is assigning **without** messaging the peer: `team`'s `assign`
289
402
  sends a handoff, and nothing else records an owner silently.
290
403
  - **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.
404
+ names `board:<lead>` as its owner, and `reclaim` is how a lead takes it back.
292
405
  - **A lane history is short, and it starts when the field did.** A card keeps the last
293
406
  eight lane transitions, dropping the oldest and counting what it dropped, so the record
294
407
  is bounded at write time rather than by a janitor — nothing prunes a board. A card
@@ -309,8 +422,9 @@ it always meant: when the card was last touched, which is not when it last chang
309
422
  prefix held for the session. Either way `/board` starts it again, on the session's port,
310
423
  and prints the address it reopened.
311
424
  - **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
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
314
428
  wins: one repository has one page, so this session attaches to the page that note names
315
429
  rather than serving a second one — and a session that attaches binds nothing at all.
316
430
  - **The note is believed only while it answers.** A page that crashed leaves its note
@@ -336,11 +450,24 @@ it always meant: when the card was last touched, which is not when it last chang
336
450
  and never renames the session, so the footer stays the user's. A failed join is a clause
337
451
  on the `/board` line, not a failed page. The seat is the name the page is served and
338
452
  reported under, and `/board status` says which session is on the team through it.
339
- - **`plan`**: `board plan` records a plan file's path; the plan extension owns the
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.
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.
344
471
  - **`todo`**: the approved plan's steps are seeded into `todo`. Steps never become
345
472
  cards. A `list` call is a read for the lag reminder too, so reading the board never
346
473
  forces a todo update, while `add` or `claim` counts as work like any other write.
@@ -352,9 +479,18 @@ it always meant: when the card was last touched, which is not when it last chang
352
479
 
353
480
  ## Design notes
354
481
 
355
- - **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
356
483
  card in `ended/`, and one per archived card in `archive/`. The listing is the index, so
357
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.
358
494
  - **An id is never reused.** The next id is the highest in `cards/`, `archive/` *or*
359
495
  `ended/` plus one, so a number cannot be handed to a second feature — which is the only
360
496
  thing that lets a note, a handoff, or a link name a card and still mean it. A record
@@ -367,7 +503,7 @@ it always meant: when the card was last touched, which is not when it last chang
367
503
  drop still leaves a record because the card is gone, while an archived card is not gone
368
504
  at all — and why a read by id looks in both piles: a card is where it lies. The
369
505
  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
506
+ automatic sweep would make `lane: "done"` return something different past a threshold
371
507
  nobody could see. The board, the rollup, and the page read `cards/` alone — the only
372
508
  look at the archive is the directory listing a lane-less `list` counts, so the omission
373
509
  cannot pass for a deleted history — and an archive therefore lowers the board's size
@@ -379,7 +515,11 @@ it always meant: when the card was last touched, which is not when it last chang
379
515
  - **The page is a dumb renderer** over one snapshot built on the server, pushed
380
516
  only when a card's revision, or a teammate's state, mail, or listening flag, moves: a
381
517
  heartbeat every ten seconds is not news, and a frame nobody needs costs the page a
382
- 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.
383
523
  - **A query reads what a card says, not just what it is called.** A card's reasoning lives
384
524
  in its notes and in the plan it is executing, so leaving both out of the search made the
385
525
  two fields that hold the argument the two a reader could not find. The order is the
@@ -404,20 +544,25 @@ it always meant: when the card was last touched, which is not when it last chang
404
544
  the prefix and could not see the whole name, so a rule with nothing behind it (or a
405
545
  name with no rule) survived until a capture showed it.
406
546
  - **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.
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.
412
557
  - **An offer is a record, not a reservation.** `team offer` broadcasts a card and then
413
558
  writes `offeredAt` — the moment, and nothing else — because an event leaves nothing
414
559
  behind and a card three sessions ignored read exactly like one nobody had looked at.
415
560
  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.
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.
421
566
  - **`waits on` answers two questions, on purpose.** The page names the *unfinished*
422
567
  dependencies in one line — "why can I not start" — and `board`'s result names every
423
568
  dependency with its lane, including finished ones — "what is this card tied to". Both
@@ -425,15 +570,18 @@ it always meant: when the card was last touched, which is not when it last chang
425
570
  a drift.
426
571
  - **A link is stored once, and read both ways.** `dependsOn` lives on the waiting card
427
572
  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
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
429
575
  dialog has the rung the card's own row cannot. Both skip a finished card, unlike the
430
576
  drop refusal, which keeps every edge: "what is held up" is about work, and "would this
431
577
  drop strand a link" is about the file.
432
578
  - **One page is found, not configured.** A note beside the cards carries the port, the
433
579
  seat, and the token; it is created exclusively, so two sessions starting at
434
580
  once cannot both serve, and the loser attaches to the winner. The alternative —
435
- a setting that names the lead — was rejected: the role moves, the seat's name does
436
- not. The port in the note is the session's, never a number this page chose.
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.
437
585
  - **The page is a view because a browser is not an identity.** Every page write was made
438
586
  as the seat, so a page `assign` had to write down that nobody had been told, and the
439
587
  client's copy of the card rules drifted from `lib/board/rules.ts` — it offered a drop
@@ -444,79 +592,145 @@ it always meant: when the card was last touched, which is not when it last chang
444
592
  reverse edge is computed over the board, because a copy of the same edge on the card
445
593
  being waited for is a copy that drifts; the cycle check and the drop rule are what
446
594
  keep the graph whole instead.
447
- - **A priority is a field, not a lane.** The four lanes are the board's vocabulary and
448
- a fifth would double the counts and the heads; inside a lane, priority then age is the
449
- 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".
450
601
  - **`edit` leaves an omitted field alone.** It is the one action whose job is "make it
451
602
  say this", so an absent field must mean "not this field" — a caller that sends only a
452
603
  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
604
+ - **A lane change is recorded where every transition ends.** `steps.ts` decides each
454
605
  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.
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.
460
650
  - **Rejected:** steps as cards (they flood the board), a shared database (a file per
461
651
  card needs no dependency and no lock), an index file (every write maintains it,
462
652
  every read can find it stale), drag-and-drop (a lane change is a named `move`), a
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
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
465
656
  nobody stated would decide what a read returns, and nothing else here prunes).
466
657
  - **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).
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.
471
676
  `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.
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.
476
683
 
477
684
  ## Files
478
685
 
479
686
  | File | Responsibility |
480
687
  |---|---|
481
- | `index.ts` | The factory: gate on `PI_DISABLED_EXTENSIONS`, build the runtime and the page, register both, close on shutdown |
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 |
482
689
  | `schema.ts` | The tool's parameters |
483
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 |
484
691
  | `tools.ts` | Action dispatch and the refusal shape |
485
692
  | `input.ts` | A card body as the tool reads it: the `add` fields normalized in one place |
486
693
  | `format.ts` | The model-facing text |
487
- | `rows.ts` | The transcript row grammar |
488
- | `result.ts` | The transcript block |
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 |
489
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` |
490
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` |
491
700
  | `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
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 |
494
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 |
495
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 |
496
- | `view/page.ts` | The document: an inert shell the client fills, with no form on it |
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 |
497
705
  | `view/styles.ts` | The page's `board-*` layout rules |
498
- | `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 |
499
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 |
500
709
  | `view/team.ts` | The client's round: the two tables and the Done line |
501
- | `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 |
502
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 |
503
713
  | `view/live.ts` | The client's tabs, filter, and frame stream |
504
714
  | `view/client.ts` | The one script served at `/app.js` |
505
715
 
506
- 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
507
718
  [`lib/README.md`](../../lib/README.md).
508
719
 
509
720
  ## Testing
510
721
 
511
722
  `test/suite-manifest.ts` declares the suites:
512
- `lib/board/{card,links,page-pointer,round,round-text,rules,session,store,view}` for the
513
- record, the dependency graph, the note a page leaves, the rollup and its text, the card's
514
- transitions, the store a call opens and the identity it writes as, the files that store
515
- keeps, and the snapshot, and
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
516
729
  `extensions/board/{input,seat,tools,page,commands,extension,transport/router,view/pages,view/behaviour}`
517
730
  for the card body, the seat, every action, the page's lifecycle over a real socket (a page
518
731
  served, attached to, and taken over, a board that stops being readable, and a write refused
519
732
  on the wire), the command, the
520
733
  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.
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.