@ucsandman/legcli 0.7.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 (184) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/LICENSE +58 -0
  3. package/NOTICE +4 -0
  4. package/README.md +558 -0
  5. package/bin/fake-agent.mjs +143 -0
  6. package/bin/leg.mjs +449 -0
  7. package/docs/DECISIONS.md +119 -0
  8. package/docs/DEMO.md +214 -0
  9. package/docs/DEVIATIONS.md +182 -0
  10. package/docs/ERRORS.md +326 -0
  11. package/docs/README.md +40 -0
  12. package/docs/REUSE.md +289 -0
  13. package/docs/ROADMAP-v2.md +55 -0
  14. package/docs/VOCABULARY.md +243 -0
  15. package/docs/adapters.md +300 -0
  16. package/docs/board-guide.md +579 -0
  17. package/docs/cli-contracts.md +401 -0
  18. package/docs/concepts.md +426 -0
  19. package/docs/configuration.md +220 -0
  20. package/docs/faq.md +206 -0
  21. package/docs/getting-started.md +251 -0
  22. package/docs/real-run.md +80 -0
  23. package/docs/review-2026-09-14.md +72 -0
  24. package/docs/screenshots/board-400px.png +0 -0
  25. package/docs/screenshots/board-details-open.png +0 -0
  26. package/docs/screenshots/board-done.png +0 -0
  27. package/docs/screenshots/board-drawer.png +0 -0
  28. package/docs/screenshots/board-empty.png +0 -0
  29. package/docs/screenshots/board-handoff.png +0 -0
  30. package/docs/screenshots/board-running.png +0 -0
  31. package/docs/screenshots/demo-1-claude-running.png +0 -0
  32. package/docs/screenshots/demo-2-limit-hit.png +0 -0
  33. package/docs/screenshots/demo-3-handoff-bundle.png +0 -0
  34. package/docs/screenshots/demo-4-codex-running.png +0 -0
  35. package/docs/screenshots/demo-5-done.png +0 -0
  36. package/docs/screenshots/floor-final-1280.png +0 -0
  37. package/docs/screenshots/floor-final-400.png +0 -0
  38. package/docs/screenshots/floor-landing.png +0 -0
  39. package/docs/screenshots/floor.png +0 -0
  40. package/docs/screenshots/share-guest-1280.png +0 -0
  41. package/docs/screenshots/share-owner-1280.png +0 -0
  42. package/docs/screenshots/terminals-1280.png +0 -0
  43. package/docs/screenshots/thumb-done.jpg +0 -0
  44. package/docs/screenshots/thumb-handoff-details.jpg +0 -0
  45. package/docs/screenshots/thumb-limit-hit.jpg +0 -0
  46. package/docs/screenshots/youtube-thumbnail.jpg +0 -0
  47. package/fixtures/limits/agy/agy-resource-exhausted.json +11 -0
  48. package/fixtures/limits/claude/claude-budget-limit.json +11 -0
  49. package/fixtures/limits/claude/claude-max-turns.json +11 -0
  50. package/fixtures/limits/claude/claude-model-limit.json +11 -0
  51. package/fixtures/limits/claude/claude-session-limit.json +11 -0
  52. package/fixtures/limits/claude/claude-weekly-limit.json +11 -0
  53. package/fixtures/limits/codex/codex-quota-exceeded.json +11 -0
  54. package/fixtures/limits/codex/codex-rate-limit-exceeded.json +11 -0
  55. package/fixtures/limits/codex/codex-skills-notice.json +11 -0
  56. package/fixtures/limits/codex/codex-usage-limit-reached.json +11 -0
  57. package/fixtures/limits/codex/codex-usage-limit.json +11 -0
  58. package/fixtures/limits/generic/generic-429.json +11 -0
  59. package/fixtures/limits/generic/generic-overloaded.json +11 -0
  60. package/fixtures/limits/generic/generic-quota.json +11 -0
  61. package/fixtures/limits/generic/generic-rate-limit.json +11 -0
  62. package/fixtures/limits/generic/generic-resource-exhausted.json +11 -0
  63. package/fixtures/limits/generic/generic-usage-limit.json +11 -0
  64. package/fixtures/limits/grok/grok-not-logged-in.json +11 -0
  65. package/fixtures/limits/negative/auth-source-set.json +11 -0
  66. package/fixtures/limits/negative/compile-error.json +11 -0
  67. package/fixtures/limits/negative/empty-stdout-exit-0.json +11 -0
  68. package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +1 -0
  69. package/fixtures/live/agy/cmd.txt +1 -0
  70. package/fixtures/live/agy/err.log +0 -0
  71. package/fixtures/live/agy/out.log +1 -0
  72. package/fixtures/live/agy/parsed.json +5 -0
  73. package/fixtures/live/agy/run.json +18 -0
  74. package/fixtures/live/agy/supervisor.log +2 -0
  75. package/fixtures/live/claude/cmd.txt +1 -0
  76. package/fixtures/live/claude/err.log +0 -0
  77. package/fixtures/live/claude/limit-rate_limit.json +21 -0
  78. package/fixtures/live/claude/out.log +1 -0
  79. package/fixtures/live/claude/parsed.json +11 -0
  80. package/fixtures/live/claude/run.json +18 -0
  81. package/fixtures/live/claude/supervisor.log +2 -0
  82. package/fixtures/live/codex/cmd.txt +1 -0
  83. package/fixtures/live/codex/err.log +1 -0
  84. package/fixtures/live/codex/last.md +2 -0
  85. package/fixtures/live/codex/limit-usage_limit_exceeded.json +25 -0
  86. package/fixtures/live/codex/out.log +8 -0
  87. package/fixtures/live/codex/parsed.json +15 -0
  88. package/fixtures/live/codex/run.json +18 -0
  89. package/fixtures/live/codex/supervisor.log +2 -0
  90. package/fixtures/live/grok/cmd.txt +1 -0
  91. package/fixtures/live/grok/err.log +32 -0
  92. package/fixtures/live/grok/out.log +7 -0
  93. package/fixtures/live/grok/parsed.json +5 -0
  94. package/fixtures/live/grok/run.json +18 -0
  95. package/fixtures/live/grok/supervisor.log +2 -0
  96. package/fixtures/verified.json +34 -0
  97. package/package.json +68 -0
  98. package/scripts/board-shots.mjs +69 -0
  99. package/scripts/build-docs-site.mjs +308 -0
  100. package/scripts/check-claims.mjs +150 -0
  101. package/scripts/clean-clone-check.sh +42 -0
  102. package/scripts/license-sign.mjs +23 -0
  103. package/scripts/limits-table.mjs +38 -0
  104. package/scripts/live-limits.mjs +36 -0
  105. package/scripts/privacy-check.mjs +63 -0
  106. package/scripts/probe.mjs +92 -0
  107. package/scripts/seed-fake-cards.mjs +34 -0
  108. package/scripts/seed-floor-board.mjs +69 -0
  109. package/scripts/seed-wes-board.mjs +91 -0
  110. package/scripts/stripe-setup.mjs +99 -0
  111. package/scripts/vercel-env.mjs +40 -0
  112. package/src/accounts.mjs +119 -0
  113. package/src/adapters/agy.mjs +56 -0
  114. package/src/adapters/claude.mjs +54 -0
  115. package/src/adapters/codex.mjs +73 -0
  116. package/src/adapters/common.mjs +32 -0
  117. package/src/adapters/fake.mjs +48 -0
  118. package/src/adapters/grok.mjs +51 -0
  119. package/src/adapters/index.mjs +27 -0
  120. package/src/adapters/resolve.mjs +40 -0
  121. package/src/attach.mjs +592 -0
  122. package/src/auth.mjs +85 -0
  123. package/src/board/board.css +543 -0
  124. package/src/board/board.js +1307 -0
  125. package/src/board/favicon.svg +1 -0
  126. package/src/board/floor.html +127 -0
  127. package/src/board/floor.js +670 -0
  128. package/src/board/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
  129. package/src/board/fonts/OFL-azeret-mono.txt +94 -0
  130. package/src/board/fonts/atkinson-board.woff2 +0 -0
  131. package/src/board/fonts/azeret-board.woff2 +0 -0
  132. package/src/board/index.html +251 -0
  133. package/src/board/sessions.js +1451 -0
  134. package/src/bundle.mjs +103 -0
  135. package/src/cards.mjs +114 -0
  136. package/src/chain.mjs +264 -0
  137. package/src/commands.mjs +79 -0
  138. package/src/contract.mjs +73 -0
  139. package/src/env.mjs +19 -0
  140. package/src/fsx.mjs +72 -0
  141. package/src/git-snapshot.mjs +152 -0
  142. package/src/handoff.mjs +171 -0
  143. package/src/hook.mjs +49 -0
  144. package/src/land.mjs +106 -0
  145. package/src/launcher.mjs +272 -0
  146. package/src/leases.mjs +86 -0
  147. package/src/ledger.mjs +369 -0
  148. package/src/license.mjs +168 -0
  149. package/src/limits.mjs +97 -0
  150. package/src/live-capture.mjs +69 -0
  151. package/src/mergequeue.mjs +225 -0
  152. package/src/orchestrator.mjs +365 -0
  153. package/src/pipeline.mjs +100 -0
  154. package/src/preferences.mjs +47 -0
  155. package/src/presets.mjs +23 -0
  156. package/src/ratelimit.mjs +49 -0
  157. package/src/redact.mjs +47 -0
  158. package/src/resume.mjs +354 -0
  159. package/src/runner.mjs +438 -0
  160. package/src/scheduler.mjs +118 -0
  161. package/src/server.mjs +845 -0
  162. package/src/session-detail.mjs +129 -0
  163. package/src/sessions.mjs +202 -0
  164. package/src/share.mjs +163 -0
  165. package/src/stations/agent.mjs +42 -0
  166. package/src/stations/build.mjs +9 -0
  167. package/src/stations/human.mjs +5 -0
  168. package/src/stations/land.mjs +6 -0
  169. package/src/stations/plan.mjs +8 -0
  170. package/src/stations/pr.mjs +31 -0
  171. package/src/stations/review.mjs +8 -0
  172. package/src/stations/test.mjs +27 -0
  173. package/src/store.mjs +90 -0
  174. package/src/sync/dashclaw.mjs +70 -0
  175. package/src/sync/index.mjs +54 -0
  176. package/src/sync/workboard.mjs +54 -0
  177. package/src/taps/agy.mjs +64 -0
  178. package/src/taps/claude-usage.mjs +67 -0
  179. package/src/taps/claude.mjs +170 -0
  180. package/src/taps/codex.mjs +286 -0
  181. package/src/trust.mjs +323 -0
  182. package/src/usage.mjs +179 -0
  183. package/src/wait.mjs +30 -0
  184. package/src/worktree.mjs +207 -0
@@ -0,0 +1,579 @@
1
+ # Board guide
2
+
3
+ For anyone using the Leg board day to day: what every element means and when
4
+ it shows up. The board is one page, read top to bottom: **the verdict** (one
5
+ sentence saying what to do next), **the logins** (one panel each), **Terminals**
6
+ (one row each), **the ledger** (finished terminals, what landed, background
7
+ tasks, as three counts that open), then **Settings**. Start a session
8
+ (`leg claude`) or the background-task board (`npm start`), see
9
+ [getting-started.md](getting-started.md), then use this as a reference.
10
+
11
+ The visual system, and why it is what it is, is `DESIGN.md` at the repo root.
12
+
13
+ ## Screenshots
14
+
15
+ `docs/screenshots/` (listed here so you know what exists before you look for
16
+ one). All nineteen were retaken on 2026-09-15 against the current build, after
17
+ the dark-product-surface redesign and the card-row port that followed it:
18
+
19
+ ```
20
+ terminals-1280.png the board at 1280 px, four terminals on one login
21
+ board-400px.png the same board at 400 px
22
+ board-details-open.png a terminal with its expansion open
23
+ board-empty.png no background tasks at all
24
+ board-running.png one card running its first agent
25
+ board-handoff.png the limit hit, the card waiting on you
26
+ board-drawer.png that card expanded: bundle, runs, timeline
27
+ board-done.png the card finished, on the agent that finished it
28
+ floor.png /floor with nothing queued
29
+ floor-landing.png /floor with every lane full
30
+ floor-final-1280.png /floor after a card finished
31
+ floor-final-400.png the same at 400 px
32
+ share-owner-1280.png a shared board as its owner
33
+ share-guest-1280.png the same board as a guest: prompts and repos redacted
34
+ demo-1-claude-running.png leg 1 running on fake-claude
35
+ demo-2-limit-hit.png limit hit, bundle written, the row waiting on you
36
+ demo-3-handoff-bundle.png the row expanded: bundle path, run signal, timeline
37
+ demo-4-codex-running.png leg 2 running on fake-codex
38
+ demo-5-done.png done, both legs on the chain
39
+ ```
40
+
41
+ The five `demo-*.png` are one run of the sequence in [DEMO.md](DEMO.md), all at
42
+ 1280 px.
43
+
44
+ To retake one, seed a board with the shape a real one has and drive it to the
45
+ state the shot needs:
46
+
47
+ ```
48
+ node scripts/seed-wes-board.mjs # 4 live terminals, 5 finished, long paths
49
+ node scripts/seed-floor-board.mjs # cards in every floor lane, plus a landing
50
+ node scripts/board-shots.mjs <port> <tag> # shoot 1280 and 400, print the numbers
51
+ ```
52
+
53
+ Each seeder writes a throwaway `LEG_HOME` under the OS temp dir; serve it on a
54
+ spare port with `LEG_TRUST=never`. **Never use port 4747**, that is the live
55
+ board, with real sessions on it.
56
+
57
+ Measure against those seeds and never against a board of healthy terminals:
58
+ styling scoped to live rows and measured on a clean board reported 40px terminal
59
+ rows while the real screen showed 400px.
60
+
61
+ ## The verdict and the logins
62
+
63
+ `src/board/sessions.js` draws both, from the `accounts` array of
64
+ `GET /api/sessions` and the sessions beside it.
65
+
66
+ **The verdict** is the largest thing on the page and it is a sentence, not a
67
+ number. It names the one fact that decides what happens next:
68
+
69
+ ```
70
+ All 4 terminals are on claude, and claude has 5% left.
71
+ ```
72
+
73
+ One login carrying every terminal is one point of failure, so that is what the
74
+ sentence says. With the terminals spread across logins it names the one closest
75
+ to a wall instead; with nothing running it says so, and names any login that is
76
+ walled. The number it prints is what is **left**, because that is the quantity
77
+ you are deciding against.
78
+
79
+ Under it, one line carrying the age of the reading and which direction it is
80
+ wrong in:
81
+
82
+ ```
83
+ Measured 2h 13m ago. 4 terminals have been running since, so the real figure is
84
+ higher than 95 percent, never lower.
85
+ ```
86
+
87
+ A reading taken two hours ago is a floor, not a measurement, and saying so is
88
+ the entire reason to print its age. Any login at a wall other than the one in
89
+ the headline is named on the same line.
90
+
91
+ **The logins** sit under the verdict, one panel each, and how much surface a
92
+ panel gets is the design saying how much it matters:
93
+
94
+ - The login carrying the terminals gets a wide panel, lit one step brighter than
95
+ the rest, with both of its windows drawn. The window closest to a wall is the
96
+ full-size instrument; the other is the same instrument at half height with a
97
+ smaller numeral, so which one to read is not a question.
98
+ - A login with a single fact to report gets a half panel: its gauge if it has a
99
+ reading, then the fact. A walled login reads `At the wall` and
100
+ `Back Saturday 10:11 PM. Nothing runs on codex until then.`
101
+ - A login that publishes no figure draws no instrument at all. agy's panel says
102
+ `agy publishes no usage figure, ever. Leg shows its terminals and their
103
+ elapsed time instead.` An empty track reads as a measurement of zero to anyone
104
+ glancing at it, so none is drawn.
105
+
106
+ **The gauge** is a track, a fill and a numeral. The fill runs in the login's own
107
+ identity colour up to 85 percent and in the over colour past it, and a 2px notch
108
+ is cut through the bar at 85 at all times, including at zero fill: you can see
109
+ the reserve before you reach it. A window that has never been read draws no fill
110
+ and prints `no reading`, because a zero is a reading and Leg does not print
111
+ one it does not have.
112
+
113
+ Nothing here is on hover. A screen reader gets the whole answer from each
114
+ track's `aria-valuetext`, including the reset, the source and the staleness; a
115
+ window with no value is not a `meter` at all and carries the same sentence as
116
+ its label. The caption under the logins reads `Times are local.`
117
+
118
+ For Codex, the board reads the app-server's read-only
119
+ `account/rateLimits/read` response every 60 seconds and maps its 300- and
120
+ 10080-minute windows to 5h and 7d. The active `leg codex` attach also polls
121
+ that response. A percentage alone does not clear a limit wall: Leg waits for
122
+ an explicit available answer from the backend.
123
+
124
+ ## Terminals
125
+
126
+ `src/board/sessions.js` renders the region from `GET /api/sessions`, refreshed
127
+ by the server-sent `sessions` event, re-sorted every 15 s, with the elapsed
128
+ clocks ticking every second. With no sessions it prints `No terminal is running.
129
+ Start one in any repo: leg claude, leg codex or leg agy. It appears here
130
+ within a second of the agent's first turn.`
131
+
132
+ The region head carries one verdict with its volume: `3 running, 2 waiting on
133
+ you`, or `3 running, nothing is waiting on you`, or `nothing is running`, with
134
+ `, last landed 11:02 PM` appended when a terminal on the page has landed.
135
+
136
+ ### Terminal row
137
+
138
+ One row per live session, inside a single panel, separated from its siblings by
139
+ a hairline. Rows that need an answer come first; the rest follow in start order.
140
+ A row that needs you says `waiting on you` where its status word would be, so
141
+ the state is never carried by colour or position alone.
142
+
143
+ Reading across the row: what it is doing, what it is working on, how long it has
144
+ been at it, and what you can do about it.
145
+
146
+ - **The register**, one line of the smallest type on the board: the status word
147
+ with its dot, then where the work is (`leg on main`, or the folder when it
148
+ is not a repo), then anything unusual about this terminal as plain words, the
149
+ account when it is not `default`, the owner on a shared board, `from <agent>`
150
+ when it was handed off, `own worktree, from main` when it cut its own.
151
+ - **The prompt**, as a button: the first prompt of the session, carried in full
152
+ in its tooltip and in the expansion. A pasted screenshot arrives as an
153
+ `<image name=... path=...>` tag with an absolute temp path in it; the tag is
154
+ replaced by `(image)` and the text, so the operator's home directory is not on
155
+ screen and the sentence starts where the sentence starts.
156
+ - **Under the prompt**, exactly one sentence, the highest-ranked thing true
157
+ about this terminal, then `also: <names>` naming every sentence it is holding
158
+ back, then the files this session is touching as comma-separated **basenames**
159
+ up to six, then `, and N more`. The whole path stays on the title and in the
160
+ expansion; printed in full it was ninety characters of temp path per file. A
161
+ file another live session is also touching is printed in the warning colour.
162
+ A terminal that is merely running prints no sentence at all: its own row
163
+ already says so, and four rows each saying `activity` is four lines of noise.
164
+ - **The clock**: elapsed since the session started (`4h 24m`), and the session's
165
+ short id. The id used to print as `claude-7f3a` immediately after the word
166
+ `claude`; the prefix is the agent name twice and it is gone.
167
+ - **The buttons**, in a fixed 2x2 grid so every row's controls sit in the same
168
+ place: Land, Hand off now, Details, End. Land is the primary action only when
169
+ it can actually run, when it is blocked the accent moves to Hand off now,
170
+ because a disabled control should not wear the one accent colour in the
171
+ design. When Land is disabled its reason is printed, never left in a tooltip.
172
+
173
+ **A fact true of every row is said once, at the region.** Three rows all reading
174
+ `this terminal works in the checkout itself: there is no branch of its own to
175
+ land` is one fact and two lines of noise, so it is hoisted to the region head as
176
+ `every terminal here works in the checkout itself...`. The per-row copy stays in
177
+ the DOM, visually hidden, so each Land button's `aria-describedby` still resolves
178
+ to its own reason. The same applies to any sentence two or more terminals would
179
+ print identically, except muted ones: `turn 12, last activity 1:04 AM` is shared
180
+ by coincidence, not a fact about the board.
181
+
182
+ **Finished terminals leave this panel.** A terminal that has ended or been lost
183
+ is history, and after a day's work it is most of the list. It moves to the
184
+ ledger (below) as part of a count that opens. A finished terminal that still
185
+ needs you, or whose expansion you have open, stays in place.
186
+
187
+ ### The one sentence
188
+
189
+ `rankedNotes` in `src/board/sessions.js` is the single source of every sentence
190
+ a panel prints. It builds the list, sorts it, prints the first, and names the
191
+ rest behind `also:`. Rank 1 is worst.
192
+
193
+ | rank | what it says |
194
+ |------|--------------|
195
+ | 1 | `every option is out, first back: codex 10:11 PM`, or `limit: <reason>, back <time>` |
196
+ | 2 | a bounced Land, or `the landing was cut off (the board restarted); press Land again` |
197
+ | 3 | an overlap with another live session |
198
+ | 4 | `sam asked to take this terminal at 11:04 PM` |
199
+ | 5 | `waiting for codex at 10:11 PM` |
200
+ | 6 | `handing off to codex, <reason>, 2m` |
201
+ | 7 | `landing <branch> onto main: commit, rebase, test, fast-forward` |
202
+ | 8 | `near the 5h wall, next: codex` |
203
+ | 9 | a finished Land, `nothing to land`, or the button you last pressed |
204
+ | 10 | `turn 14, last activity 11:04 PM`, the fallback that is always true |
205
+
206
+ A sentence at rank 4 or better is what raises the panel, sorts it to the top and
207
+ counts it in the region head, so those three cannot disagree.
208
+
209
+ Two of them in full, because they are what the board exists to say:
210
+
211
+ - **Overlap**, two live sessions in one repo whose `files_touched` or
212
+ `files_dirty` sets intersect (`sessions.overlaps`), so it catches a file one
213
+ agent has edited and another has only made dirty. In separate checkouts:
214
+ `codex (codex-99ab) is changing src/server.mjs in another checkout; whoever
215
+ lands second rebases`. In the same checkout: `codex (codex-99ab) is editing
216
+ src/server.mjs too`. Three file names, then `, and N more`.
217
+ - **A Land**. Bounced: `Land was attempted at 6:40 PM onto main and bounced:
218
+ CONFLICT (content): Merge conflict in src/server.mjs. The branch still holds
219
+ every commit; nothing was lost.` Finished: `landed on <base>, <7-char sha>,
220
+ <n> files, +<added>/-<removed>`, with `, untested` when the land recorded no
221
+ test run. A finished Land stops being printed ten minutes after it happened.
222
+ Nothing to land: `nothing to land: <branch> has no changes beyond main`.
223
+
224
+ ### Status words
225
+
226
+ | status | word |
227
+ |--------|------|
228
+ | `starting` | starting |
229
+ | `running` | running |
230
+ | `warning` | near limit |
231
+ | `limit` | limit hit |
232
+ | `handing_off` | handing off |
233
+ | `waiting` | waiting for reset |
234
+ | `handed_off` | handed off |
235
+ | `ended` | ended |
236
+ | `lost` | lost |
237
+
238
+ The word carries the meaning. The 7 px square beside it is `aria-hidden` and
239
+ only repeats the tone. `waiting` means every option is walled and the terminal
240
+ is counting down to the first reset; its sentence reads `waiting for <agent> at
241
+ <time>`, and End quits that terminal with exit 3. `lost` means the runner
242
+ process that owned that terminal is gone. It is never counted as live.
243
+
244
+ ### Terminal buttons
245
+
246
+ The order is fixed, Land, Hand off now, Details, End, and it never reflows by
247
+ availability: a button that does not apply is omitted, never moved.
248
+
249
+ | button | shown when | what it does |
250
+ |--------|------------|--------------|
251
+ | Land | always, disabled with the reason printed under it when the session has no worktree of its own, while it is landing, or when the worktree is gone | `POST /api/sessions/:id/land` (202): commits what the agent left on `leg/<id>`, rebases it onto its base, runs the tests, fast-forwards the base or bounces; the panel's sentence shows the result |
252
+ | Request handoff | the board is shared and this terminal is someone else's | `POST /api/sessions/:id/request-handoff` (202): asks the owner; nothing happens until they approve |
253
+ | Approve `<name>` / Dismiss `<name>` | the board is shared and someone asked for a hand-off on your terminal | `POST /api/sessions/:id/requests/<name>/approve` (or `/dismiss`): approving hands the terminal off, and the event says who it was for |
254
+ | Details | any terminal of yours | `GET /api/sessions/:id/detail`: the transcript, the files changed with their line counts, the timeline and the current bundle; `GET /api/sessions/:id/diff?file=<path>` for one file, capped at 400 lines, refused for any path outside that terminal's own tree |
255
+ | Hand off now | the session is active | `POST /api/sessions/:id/handoff`: saves the bundle, stops this agent, starts the next option in the same terminal |
256
+ | Change order | inside the expansion, while the status is `starting`, `running`, `warning`, `limit` or `waiting` | saves its validated claude/codex/agy priority; an older wrapper instead saves the machine default and says to restart the terminal |
257
+ | End | the session is active | `POST /api/sessions/:id/end`: stops the agent, ends the session |
258
+ | Remove | the session is not active | `DELETE /api/sessions/:id`: safely prunes the session record, worktree, and merged branch only when the worktree is clean and the branch is already on its base; otherwise it leaves them and explains why |
259
+ | Remove record | the session is not active and has an own worktree | `DELETE /api/sessions/:id?force=1&keep_worktree=1`: removes only Leg's record and keeps the worktree, branch, unmerged commits, and dirty files |
260
+
261
+ End, Remove and Remove record confirm first: the button row is replaced in place
262
+ by one sentence and two buttons, focus moves to Cancel, and Escape cancels.
263
+ There is no modal and no browser `confirm()`. Settings has the same order editor
264
+ for the default copied by new terminals.
265
+
266
+ ### The expansion
267
+
268
+ **Details**, or a click on the prompt, expands a region in flow directly under
269
+ that panel; the page grows and the panel stays where it is. Nothing slides in
270
+ from the right, and the page keeps one scroll container
271
+ (`docs/screenshots/board-details-open.png`). Sections, top to bottom:
272
+
273
+ 1. A header with the agent, the id tail, **Pause updates** and **Close**.
274
+ 2. **Now**: the status word, the turn count and the last activity time, the last
275
+ thing the agent said, then `path`, `branch`, `cut from`, `transcript` and
276
+ `head` in full, never shortened. `usage unknown (<why>); the limit still
277
+ hands off` sits here when the endpoint had no numbers for this login: no
278
+ login in that config directory, a 404, a 429, a body that is not JSON, or a
279
+ shape Leg does not recognise.
280
+ 3. **Task**: the prompt in full.
281
+ 4. **Conversation**: the last 8 turns, newest first, headed `showing 8 of 34
282
+ turns`; **show 40 more** raises the cap.
283
+ 5. **Files**: every file changed, with `uncommitted`, `+<adds>`, `-<dels>`, or
284
+ `new` or `committed` when there are no counts against `HEAD`. A row expands
285
+ to that file's diff, capped at 400 lines.
286
+ 6. **Timeline**: the last 40 events, newest first.
287
+ 7. **What happens next**: `now: claude, then codex, then agy`, which fallback is
288
+ preferred and which is eligible now, the line `Used after a usage limit or
289
+ Hand off now. A normal exit ends this terminal.`, the **Change order**
290
+ editor, the current bundle id, and whether `.leg/RESUME.md` still describes
291
+ the repository (recomputed from git on every poll).
292
+
293
+ It refetches every 3 seconds while it is open, and stops on **Pause updates**,
294
+ when the tab is in the background, or when it is closed. The rest of the page
295
+ stays live, so the panel's own buttons keep working. Messages and diffs are
296
+ scrubbed for secrets on the way out, and the whole region is refused for a
297
+ terminal that belongs to someone else. **Close** or the Escape key closes it.
298
+
299
+ **Change order** moves the three agents without removing one; same-agent
300
+ secondary accounts still come before the other agents. The list is an absolute
301
+ priority: an agent moved to the bottom is tried last from every starting agent.
302
+ The editor previews the resulting priority, with the agent running now skipped,
303
+ before saving.
304
+
305
+ ## The ledger
306
+
307
+ Below the terminals, three counts sitting on the ground with no panel, because a
308
+ raised surface here would compete with the terminals that are live. Each is a
309
+ heading, a line of detail and a button that opens the detail below the row.
310
+
311
+ - **N finished**, the terminals that have ended or been lost, `4 lost, 2 ended,
312
+ in leg, costclaw, declick`. **View all N** opens them as full rows. On a real
313
+ board after a day's work this is most of the list, which is exactly why it is a
314
+ count and not the list.
315
+ - **N landed**, what has landed on trunk across every repo the board can see,
316
+ `newest 38 minutes ago, on leg@main, recruiting-tool@main`. **View N
317
+ commits** opens them grouped by repo, newest first: the subject and, for a
318
+ commit a Land put there, `3 hours ago, by claude`, read from
319
+ `~/.leg/landings.jsonl`, which outlives the session; any other commit shows
320
+ its git author. Shown in full it was eighteen rows of `git log` at the same
321
+ visual weight as the live terminals, so the loudest thing on the page was a
322
+ commit from eleven days ago. With nothing landed it reads `Nothing landed yet`
323
+ and explains what Land does.
324
+ - **N background tasks**, the card runtime below, as a count with **View N
325
+ cards**, plus **New card**. With none it reads `Nothing is queued. Leg starts
326
+ the next login only when a terminal hands off.`
327
+
328
+ ## A shared board (more than one human)
329
+
330
+ With `leg share` on (off by default), the Terminals region head carries a chip
331
+ reading `you are wes, 2 on this board`, with `, a guest` after the name for
332
+ anyone who is not the owner, and every row carries an owner word in its
333
+ register: `wes, you` on yours, `wes` on someone else's. There is no colour
334
+ difference between the two.
335
+
336
+ A row that belongs to someone else carries exactly one sentence, `read-only: wes
337
+ owns this terminal`. Its prompt reads `prompt hidden`; it has no file names, no
338
+ path beyond the repo and branch, no bundle and no event log; and its only button
339
+ is **Request handoff**. A guest's login panels say `Usage for this login is not
340
+ shared with guests` and draw no instrument. The owner sees `sam asked to take
341
+ this terminal at 11:04 PM` on their own row, with **Approve sam** and **Dismiss
342
+ sam**.
343
+
344
+ A guest's board has no background tasks cell, no New card button and no Floor
345
+ link: that side belongs to the owner of the machine, and the API answers 403.
346
+
347
+ ## Background tasks: page layout
348
+
349
+ Everything from here down is the v0.1 card runtime. Each card runs separately
350
+ from the interactive terminal conversations, in its own git worktree. It is the
351
+ third ledger cell, and its rows open in a drawer under the ledger.
352
+
353
+ The masthead (`src/board/index.html`) has the Leg wordmark on the left and, on
354
+ the right, the connection word (`connecting` / `live` / `reconnecting…`) with its
355
+ dot, the scheduler status (`scheduler running, 2 max`), and the **Floor** link.
356
+ **New card** is in the ledger cell, not the masthead, and **Settings** is the
357
+ last region of the page, in flow. Nothing on the board is sticky: the verdict is
358
+ what you came for and it is at the top, so there is nothing to pin.
359
+
360
+ With no cards the region prints `No background task. New card queues one. They
361
+ run headless in their own worktree and report on the floor.` With cards the
362
+ region head prints the counts instead: `2 running, 1 queued, 3 finished`.
363
+
364
+ **Settings** holds the **API token** field (only needed when the server is bound
365
+ off loopback; see [configuration.md](configuration.md#network-exposure)), the
366
+ **New terminal handoff order** editor used by terminals started after you save,
367
+ and the board's own facts: version and bind address, who you are signed in as,
368
+ whether share is on, the scheduler, and the board home. Its region head says
369
+ `No API token set, requests reach 127.0.0.1:4747 unauthenticated`, or `API token
370
+ set, sent to 127.0.0.1:4747 as a bearer token` once a token is stored.
371
+
372
+ ## Order
373
+
374
+ There are no columns. Cards are rows in one list, ordered by status (`ROW_RANK`
375
+ in `src/board/board.js`), then by start time within a rank:
376
+
377
+ | rank | statuses |
378
+ |------|----------|
379
+ | 0 | `needs_approval`, `waiting_human` |
380
+ | 1 | `failed`, `killed` |
381
+ | 2 | `running`, `handing_off` |
382
+ | 3 | `paused` |
383
+ | 4 | `queued` |
384
+ | 5 | `backlog` |
385
+ | 6 | `done` |
386
+
387
+ Rows are re-appended only when that order actually changes, so a row does not
388
+ move out from under the cursor.
389
+
390
+ ## Card row
391
+
392
+ Each card row reads in the same four registers as a terminal panel:
393
+
394
+ - **Who**: an adapter chip in that agent's colour (or `no agent`), the station
395
+ name when it is not `-`, and the status word.
396
+ - **What**: the title (the card's `title`, or the first 60 characters of the
397
+ task) as a button that opens the [expansion](#card-expansion), and one
398
+ sentence under it.
399
+ - **Where**: `repo@trunk`, the worktree path shortened to its tail with the full
400
+ path in its tooltip, the chain rail, and the lease chips, one per claimed
401
+ lease or `**` (the whole repo) when none were set, plus `blocked by lease`
402
+ when the scheduler says so.
403
+ - **When**: the elapsed clock, `mm:ss` while a leg is running and `--:--`
404
+ otherwise, then the buttons.
405
+
406
+ The log is not on the row. It is in the expansion.
407
+
408
+ ### The one sentence
409
+
410
+ `cardSentence` in `src/board/board.js`, one sentence, the highest-ranked thing
411
+ true about the card:
412
+
413
+ - A bounced land, at `queued`, `running`, `handing_off` or `needs_approval`:
414
+ `Land bounced on attempt 2: rebase-conflict. The worktree still holds every
415
+ commit; nothing was lost.` The reason is printed in full.
416
+ - Queued and blocked: the scheduler's own `blocked_by` message.
417
+ - `failed` or `killed`: `failed at station build after 2 runs: <last event>`.
418
+ - `waiting_human` or `needs_approval`: `waiting on you at station review since
419
+ 11:04 PM`.
420
+ - `done`: `done after 3 runs, <last event>`.
421
+ - Otherwise the last ledger event, `<type>: <summary>`, truncated to 90
422
+ characters, with its time.
423
+
424
+ ### Status words
425
+
426
+ | status | word | notes |
427
+ |--------|------|-------|
428
+ | `backlog` | backlog | |
429
+ | `queued` | queued | |
430
+ | `running` | running, or **landing** | "landing" when the current station is a `land` station |
431
+ | `handing_off` | handing off | |
432
+ | `waiting_human` | waiting human, or **PR open** | "PR open" when the card carries a `pr_url` (land mode `pr`) |
433
+ | `needs_approval` | needs approval | |
434
+ | `paused` | paused | |
435
+ | `done` | done | |
436
+ | `failed` | failed | |
437
+ | `killed` | killed | |
438
+
439
+ ### Chain rail
440
+
441
+ One pair of tokens per chain entry at the current station: the adapter name in
442
+ its agent colour, then that leg's state as a word, `pending`, `running`, `done`,
443
+ `handed off` or `failed` (`stateGlyph` in `src/board/board.js`). There are no
444
+ glyphs: the subset fonts carry no check, cross or arrow. A gated `approve: true`
445
+ entry is not marked on the rail; **Approve** appears in the row's buttons when
446
+ the card reaches `needs_approval`.
447
+
448
+ ## Card expansion
449
+
450
+ Click a card's title to expand a region in flow under that row. Sections, top to
451
+ bottom:
452
+
453
+ 1. **Title**.
454
+ 2. **Task**: the full task text, headed `card <id>`.
455
+ 3. **Where**: `repo`, `trunk`, `worktree`, `leases` and the bundle path, full
456
+ paths never shortened, with a **Copy worktree path** button.
457
+ 4. **Pipeline**: one row per station, its name and its kind, the current one
458
+ marked `, this station`; the heading says `station 2 of 5`.
459
+ 5. **Runs**: one row per run: run number, adapter, outcome or status, signal,
460
+ exit code.
461
+ 6. **Timeline**: the ledger, newest first.
462
+ 7. **Log**: the tail of the active or most recent run's combined log, up to 2000
463
+ lines.
464
+
465
+ **Close** or the Escape key collapses it, and focus returns to the title.
466
+
467
+ ## New card form
468
+
469
+ The dialog opened by **New card** (`src/board/index.html`):
470
+
471
+ | field | notes |
472
+ |-------|-------|
473
+ | Repo path | required; an absolute path to a git repository |
474
+ | Task | required; the prompt every leg gets |
475
+ | First agent | required; real agents appear here and Claude is preferred when installed |
476
+ | Run now | checked by default; unchecking saves the card as a draft in `backlog` |
477
+ | Workflow (Advanced) | Build only stops unmerged in the worktree; Build-land runs test then land; Factory runs plan, build, review, test, and land; custom JSON reveals a station-array textarea |
478
+ | First-agent controls (Advanced) | allowed permission mode, approval gate, max turns, and scripted behavior when the test/demo override is selected |
479
+ | Fallback agents (Advanced) | one understandable row per later adapter, tried in displayed order only if the previous agent cannot continue |
480
+ | Scripted first agent (Advanced) | explicitly test/demo only; keeps fake adapters out of the normal first-agent default |
481
+ | File leases (Advanced) | comma-separated path globs to reserve so overlapping cards wait |
482
+ | Trunk branch | default `main` |
483
+ | Merge method | `ff` or `pr`; relevant to a workflow with a land station |
484
+ | Test command | overrides the land station's auto-detected command |
485
+ | Title | optional; defaults to the task's first 80 characters |
486
+
487
+ **Cancel** closes without creating a card; **Create card** posts it and closes on
488
+ success (errors show inline above the form).
489
+
490
+ ## Buttons
491
+
492
+ Which buttons a card shows depends on its status (`src/chain.mjs`
493
+ `availableActions`, labels from `src/board/board.js` `ACTION_LABELS`). The order
494
+ is fixed: Approve, Run, Resume, Pause, Hand off now, Rerun, Reassign, Kill.
495
+
496
+ | button | shown when | what it does |
497
+ |--------|------------|--------------|
498
+ | Run | `backlog` | queues the card (`enqueue`) |
499
+ | Pause | `running` | stop after the current leg, child killed, bundle written |
500
+ | Hand off now | `running`, at an `agent` station that has a later chain entry | end the current leg as if it were incomplete, write the bundle, start the next adapter |
501
+ | Resume | `paused` | pick up where it stopped |
502
+ | Approve | `needs_approval` or `waiting_human` | release a gated leg, or clear a human station |
503
+ | Kill | any non-terminal status | stop the running agent; card ends `killed` |
504
+ | Reassign | any non-terminal status, and the current station is an `agent` station | pick a different adapter/mode for the current leg from a picker |
505
+ | Rerun | `done`, `failed`, `killed` | start the station over from the last bundle |
506
+ | Remove | `done`, `failed`, `killed` | delete the card record, its events and its runs; the worktree is kept (confirms first) |
507
+
508
+ ## What a state looks like
509
+
510
+ | state | row rank | status word | typical buttons |
511
+ |-------|----------|-------------|------------------|
512
+ | backlog | 5 | backlog | Run |
513
+ | queued | 4 | queued | Kill, Reassign* |
514
+ | blocked by lease | 4 | queued, with a `blocked by lease` chip | Kill, Reassign* |
515
+ | running | 2 | running (or landing) | Pause, Hand off now†, Kill, Reassign* |
516
+ | handing off | 2 | handing off | Kill, Reassign* |
517
+ | needs approval | 0 | needs approval | Approve, Kill, Reassign* |
518
+ | waiting human | 0 | waiting human (or PR open) | Approve, Kill |
519
+ | paused | 3 | paused | Resume, Kill, Reassign* |
520
+ | done | 6 | done | Rerun, Remove |
521
+ | failed | 1 | failed | Rerun, Remove |
522
+ | killed | 1 | killed | Rerun, Remove |
523
+
524
+ \* Reassign only shows when the current station is an `agent` station.
525
+ † Hand off now only shows when that station has a later chain entry.
526
+
527
+ ## Floor view
528
+
529
+ `/floor` (`docs/screenshots/floor.png`) is the scheduler-eye view across every
530
+ card. It carries the same instrument head as the board, then polls `/api/floor`
531
+ and `/api/trunk?since=1h` every 2 seconds and refreshes on the same server-sent
532
+ events as the board. Its top bar has the Leg floor brand, the same connection
533
+ word and 24 px rule, a repos list, the scheduler status, running/queued/waiting/
534
+ done counts, then a spacer and the **Board** link. Five tables:
535
+
536
+ | section | columns |
537
+ |---------|---------|
538
+ | Running | Card, Station, Agent / leg, Leases, Last event, Elapsed, Actions |
539
+ | Waiting on humans | Card, Station, Status, Since, Actions |
540
+ | Queued | Card, Station, Leases, Blocked by |
541
+ | Leases | Lease, Card, Station, Since |
542
+ | Trunk lane | Time, Card, Summary |
543
+
544
+ Each region head names its own count, `0 running`, `0 landed in the last hour`,
545
+ and an empty table is replaced by a sentence saying what would put a row there.
546
+ The Trunk lane lists commits landed in the last hour, newest first. The Leases
547
+ table is `src/leases.mjs` `held()`: one row per lease currently claimed by a
548
+ running or handing-off card.
549
+
550
+ ## Keyboard and accessibility
551
+
552
+ - Every interactive element (links, buttons, inputs, selects, textareas,
553
+ summaries) gets a visible focus ring: `outline: 2px solid var(--focus);
554
+ outline-offset: 2px` on `:focus-visible` only (`src/board/board.css`). There
555
+ is no bare `:focus` rule, so a mouse click leaves no ring behind it.
556
+ - Escape closes the open expansion, the terminal's and the card's. If a confirm
557
+ row is open, Escape cancels that first. The floor has neither, so Escape does
558
+ nothing there.
559
+ - Buttons carry descriptive `aria-label`s (for example "Pause `<card title>`",
560
+ "Reassign adapter for `<card title>`") so a screen reader announces which card
561
+ an action applies to, not just the button label.
562
+ - A terminal's expansion is a `role="region"` labelled "What this terminal is
563
+ doing", and both expansions set `aria-hidden` and `hidden` on open and close.
564
+ - The 7 px status mark is an `aria-hidden` decoration; the status word beside it
565
+ carries the meaning. The chain rail prints each leg's state as a word, so
566
+ nothing on it is hidden from a screen reader either.
567
+ - No state is carried by colour alone: a panel that needs you also says `waiting
568
+ on you` and gains a 2 px bar, and every usage tier prints its word beside the
569
+ rail.
570
+ - `@media (prefers-reduced-motion: reduce)` is respected in
571
+ `src/board/board.css`: the status mark's annunciation animation and every
572
+ transition on the page are disabled for readers who ask for it.
573
+
574
+ ## See also
575
+
576
+ - [concepts.md](concepts.md): what each status and outcome means underneath
577
+ the board.
578
+ - [configuration.md](configuration.md): `LEG_BIND` / `LEG_TOKEN` (the
579
+ Settings token field), `LEG_PORT`.