@ucsandman/legcli 0.11.0 → 0.13.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 (65) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/README.md +95 -65
  3. package/bin/leg.mjs +123 -14
  4. package/docs/DECISIONS.md +18 -0
  5. package/docs/DEMO.md +20 -14
  6. package/docs/DEVIATIONS.md +1 -0
  7. package/docs/ERRORS.md +68 -0
  8. package/docs/ROADMAP-v2.md +50 -5
  9. package/docs/VOCABULARY.md +27 -0
  10. package/docs/board-guide.md +529 -96
  11. package/docs/cli-contracts.md +241 -5
  12. package/docs/concepts.md +167 -19
  13. package/docs/configuration.md +65 -1
  14. package/docs/faq.md +21 -5
  15. package/docs/getting-started.md +15 -11
  16. package/docs/redesign-2026-09-17.md +477 -0
  17. package/docs/screenshots/background-1280.png +0 -0
  18. package/docs/screenshots/board-400px.png +0 -0
  19. package/docs/screenshots/board-details-open.png +0 -0
  20. package/docs/screenshots/board-drawer.png +0 -0
  21. package/docs/screenshots/board-handoff.png +0 -0
  22. package/docs/screenshots/board-running.png +0 -0
  23. package/docs/screenshots/capacity-drawer-1280.png +0 -0
  24. package/docs/screenshots/floor.png +0 -0
  25. package/docs/screenshots/new-card-dialog.png +0 -0
  26. package/docs/screenshots/settings-ladder-1280.png +0 -0
  27. package/docs/screenshots/terminals-1280.png +0 -0
  28. package/fixtures/limits/claude/claude-fable-limit.json +11 -0
  29. package/fixtures/limits/claude/claude-model-limit.json +1 -1
  30. package/fixtures/limits/claude/claude-session-limit.json +1 -1
  31. package/fixtures/limits/claude/claude-weekly-limit.json +1 -1
  32. package/fixtures/live/claude/resume-model-probe.json +20 -0
  33. package/fixtures/live/claude/usage-oauth.json +87 -0
  34. package/fixtures/verified.json +1 -1
  35. package/package.json +3 -2
  36. package/scripts/board-jump-probe.mjs +335 -0
  37. package/scripts/seed-fake-cards.mjs +59 -6
  38. package/scripts/seed-wes-board.mjs +81 -12
  39. package/src/accounts.mjs +6 -1
  40. package/src/attach.mjs +378 -93
  41. package/src/audit.mjs +1 -1
  42. package/src/board/board.css +203 -11
  43. package/src/board/board.js +664 -200
  44. package/src/board/entry.js +343 -0
  45. package/src/board/floor.html +51 -39
  46. package/src/board/floor.js +585 -73
  47. package/src/board/index.html +122 -45
  48. package/src/board/sessions.js +1569 -141
  49. package/src/board/strip.js +163 -0
  50. package/src/buckets.mjs +101 -0
  51. package/src/cards.mjs +9 -1
  52. package/src/chain.mjs +13 -0
  53. package/src/hook.mjs +7 -1
  54. package/src/ledger.mjs +10 -2
  55. package/src/models.mjs +265 -0
  56. package/src/orchestrator.mjs +13 -4
  57. package/src/preferences.mjs +278 -5
  58. package/src/scheduler.mjs +24 -1
  59. package/src/server.mjs +625 -78
  60. package/src/sessions.mjs +17 -1
  61. package/src/taps/claude-usage.mjs +107 -3
  62. package/src/taps/claude.mjs +144 -5
  63. package/src/taps/codex.mjs +23 -3
  64. package/src/usage-poll.mjs +260 -0
  65. package/src/usage.mjs +439 -12
@@ -2,7 +2,8 @@
2
2
 
3
3
  For anyone using the Leg board day to day: what every element means and when
4
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**
5
+ sentence saying what to do next), **the capacity strip** (one token per login,
6
+ with the login panels behind its disclosure), **Terminals**
6
7
  (one row each), **the ledger** (finished terminals, what landed, background
7
8
  tasks, as three counts that open), then **Settings**. Start a session
8
9
  (`leg claude`) or the background-task board (`npm start`), see
@@ -13,17 +14,22 @@ The visual system, and why it is what it is, is `DESIGN.md` at the repo root.
13
14
  ## Screenshots
14
15
 
15
16
  `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:
17
+ one). Nine were retaken on 2026-09-17 against this build, on a board seeded by
18
+ `scripts/seed-wes-board.mjs` plus `scripts/seed-fake-cards.mjs --count 1
19
+ --finished 10 --live 3`; the rest are from the 2026-09-15 sweep and are noted
20
+ below:
18
21
 
19
22
  ```
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
+ terminals-1280.png the whole board at 1280 px (2026-09-17)
24
+ board-400px.png the same board at 400 px (2026-09-17)
25
+ capacity-drawer-1280.png the strip with Capacity and models open (2026-09-17)
26
+ board-details-open.png a terminal row with its expansion open (2026-09-17)
27
+ board-handoff.png one row waiting on you, with the question (2026-09-17)
28
+ background-1280.png the Background panel and the one-line entry (2026-09-17)
29
+ settings-ladder-1280.png the ladder editor in Settings (2026-09-17)
23
30
  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
31
+ board-running.png one card running its first agent (2026-09-17)
32
+ board-drawer.png that card expanded: where, runs, take over, timeline (2026-09-17)
27
33
  board-done.png the card finished, on the agent that finished it
28
34
  floor.png /floor with nothing queued
29
35
  floor-landing.png /floor with every lane full
@@ -41,6 +47,14 @@ demo-5-done.png done, both legs on the chain
41
47
  The five `demo-*.png` are one run of the sequence in [DEMO.md](DEMO.md), all at
42
48
  1280 px.
43
49
 
50
+ **Still stale, as of 2026-09-17:** `share-owner-1280.png`,
51
+ `share-guest-1280.png`, `board-empty.png`, `board-done.png` and the five
52
+ `demo-*.png` show the old top of the board (a 56px verdict over a column of
53
+ login panels) rather than the capacity strip and its drawer, and the card shots
54
+ among them predate the Background panel. Their terminal rows and their floor
55
+ are still accurate. Retake them with the commands below rather than trusting
56
+ the top band of any of those pictures.
57
+
44
58
  To retake one, seed a board with the shape a real one has and drive it to the
45
59
  state the shot needs:
46
60
 
@@ -48,47 +62,135 @@ state the shot needs:
48
62
  node scripts/seed-wes-board.mjs # 4 live terminals, 5 finished, long paths
49
63
  node scripts/seed-floor-board.mjs # cards in every floor lane, plus a landing
50
64
  node scripts/board-shots.mjs <port> <tag> # shoot 1280 and 400, print the numbers
65
+ node scripts/board-jump-probe.mjs # seeds, serves and drives its own board
51
66
  ```
52
67
 
53
68
  Each seeder writes a throwaway `LEG_HOME` under the OS temp dir; serve it on a
54
69
  spare port with `LEG_TRUST=never`. **Never use port 4747**, that is the live
55
70
  board, with real sessions on it.
56
71
 
72
+ `board-jump-probe.mjs` needs no port and no seeder: it makes its own home, binds
73
+ an ephemeral port, kills everything it started, and prints a table of where the
74
+ expanded region sat in the viewport across thirteen pushes with the list
75
+ re-sorting under it. Run it after any change to `renderSessions` or
76
+ `renderDrawer`. A scroll-hold probe cannot see this class of defect: `scrollY`
77
+ never moves, the content under it does.
78
+
57
79
  Measure against those seeds and never against a board of healthy terminals:
58
80
  styling scoped to live rows and measured on a clean board reported 40px terminal
59
81
  rows while the real screen showed 400px.
60
82
 
61
83
  ## The verdict and the logins
62
84
 
63
- `src/board/sessions.js` draws both, from the `accounts` array of
85
+ `src/board/sessions.js` draws all of it, from the `accounts` array of
64
86
  `GET /api/sessions` and the sessions beside it.
65
87
 
66
88
  **The verdict** is the largest thing on the page and it is a sentence, not a
67
89
  number. It names the one fact that decides what happens next:
68
90
 
69
91
  ```
70
- All 4 terminals are on claude, and claude has 5% left.
92
+ Fable is at 63% of its week, the only login open.
71
93
  ```
72
94
 
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.
95
+ The sentence comes from a branch table with a fixed precedence, and the order
96
+ is what the table is for. A human waiting on a terminal outranks every usage
97
+ figure, because attention is the scarcer thing. Then a login whose whole
98
+ account is nearly spent (`claude has 3% left, shared by every model.`), because
99
+ no model switch can help there. Then a single model at its wall with the login
100
+ still open (`Fable is out until 9:14 PM; opus is open.`), which is the one case
101
+ where a same-login switch is the answer. Then the burn rate, when it is known
102
+ (`About 2h 40m of Fable left.`), because the decision is about the afternoon
103
+ and not about the number. A model bucket that came back while a terminal is
104
+ still downshifted (`Fable is back; leg#7f3a is still on opus.`) sits right after
105
+ the walled-model sentence, since a state change outranks a standing figure.
106
+ Then one login carrying every terminal, several logins carrying work, and the
107
+ quiet states: nothing running and no figure anywhere. Everything walled
108
+ (`Every login is at its limit; codex is back first.`) is decided before any
109
+ per-login sentence, because it is a fact about the whole board. There is
110
+ always a sentence.
111
+
112
+ **Every time figure carries the volume it was drawn from**, always:
113
+ `From 9 samples over 4h. Opus and Sonnet have their own buckets.` The rate is
114
+ measured from the readings Leg has kept for that one bucket, and three rules
115
+ hold it honest. Under **three samples spanning ten minutes** there is no time
116
+ at all: the row prints the percentage and the reset clock instead, because a
117
+ slope drawn through two readings a minute apart is a guess printed in the
118
+ largest type on the page. A rate **never crosses a reset**: the samples are
119
+ cleared when the window rolls over, and the time is capped at the reset even
120
+ when the slope says there is more. A figure that has held flat for an hour is a
121
+ measured zero, not a forecast, and a percentage that fell inside one window is
122
+ a data error rather than a refund; neither prints a time. The second sentence
123
+ names the other models on that login, because a Fable figure is not the login's
124
+ figure and the reader who takes it for one plans the wrong afternoon; on a
125
+ login where every model shares the window it says `Shared by every model.`
126
+
127
+ The headline is capped at **56 characters**, and that number is a measurement,
128
+ not a preference: at 1280 the verdict column is 26ch and 300 sampled sentences
129
+ per length still fit two 52px lines at 60 characters, so 56 is that ceiling
130
+ with slack for a long login name. `test/board-verdict.test.mjs` holds every
131
+ branch under it, including branches fed deliberately long repository and
132
+ account names. A headline that wrapped to three lines is what put a thousand
133
+ pixels between the top of the page and the first terminal row.
78
134
 
79
135
  Under it, one line carrying the age of the reading and which direction it is
80
136
  wrong in:
81
137
 
82
138
  ```
83
- Measured 2h 13m ago. 4 terminals have been running since, so the real figure is
84
- higher than 95 percent, never lower.
139
+ Measured 2h 13m ago. 4 terminals run on it, so the real figure is higher, never
140
+ lower.
85
141
  ```
86
142
 
87
143
  A reading taken two hours ago is a floor, not a measurement, and saying so is
88
144
  the entire reason to print its age. Any login at a wall other than the one in
89
145
  the headline is named on the same line.
90
146
 
91
- **The logins** sit under the verdict, one panel each, and how much surface a
147
+ **The capacity strip** is the band under the verdict: one token per login, a
148
+ dot in its identity colour, its name, a 120 by 6 track and one figure. The
149
+ figure is the **binding** bucket, the one that will actually stop the work:
150
+ the bucket the endpoint itself marks active, else the highest percentage it
151
+ reported, else the hottest of the two legacy windows. That distinction is the
152
+ reason the strip exists. A login whose weekly account window reads 95% and
153
+ whose active Fable bucket reads 63% is stopped by the 63, and the board used to
154
+ print the 95.
155
+
156
+ ```
157
+ claude |=======---| 63% fable week codex |==========| back Sat 10:11 PM
158
+ agy no figure grok no reading
159
+ ```
160
+
161
+ - `63% fable week` is a fresh reading with its bucket word.
162
+ - `100% Tue 9:55 AM` is a reading older than five minutes: the percentage with
163
+ the clock it was taken at, because it describes a moment, not now.
164
+ - `back Sat 10:11 PM` is a login at its wall. The track fills in the wall
165
+ colour and the words carry the state, which is the one place on the board a
166
+ word may wear a severity colour: the word is itself the failure.
167
+ - `no figure` is agy, which publishes no percentage, ever. **No track is
168
+ drawn.** An empty track reads as a measurement of zero.
169
+ - `no reading` is a login that has a window and has never reported it.
170
+ - `not shared` is what a guest sees: usage figures are the owner's.
171
+
172
+ Each track is a `meter` with an `aria-valuetext` carrying the percentage, the
173
+ reset, the source, any wall and the age of the reading, the same sentence the
174
+ gauges carry. A login with no percentage is not a `meter` at all, because
175
+ `aria-valuenow` would have to be a number nobody measured.
176
+
177
+ **`Capacity and models >`**, at the end of the strip, opens a drawer holding
178
+ the login panels unchanged, plus a rail of model chips on each panel head:
179
+ `fable 63%` for a measured bucket, `fable out until 9:14 PM` for a model at its
180
+ wall, and a bare model name where Leg has a name but no figure. A percentage is
181
+ measured and a wall is attributed from wording, so the two are never printed as
182
+ each other. The drawer remembers whether you left it open. The
183
+ `Times are local.` caption lives inside it.
184
+
185
+ An **open chip is a button**: pressing it puts that rung at the top of one
186
+ terminal's ladder, so the next hand-off takes it. The terminal is the one whose
187
+ expansion is open if it is on this login, else the first live row on it, and the
188
+ press is the same `POST /api/sessions/:id/handoff-order` the ladder editor
189
+ makes. A chip stays plain text when it is walled, when it is the model that
190
+ terminal is already running, or when no live terminal on this login can take a
191
+ ladder change: a control that would do nothing is not drawn as one.
192
+
193
+ **The login panels**, in that drawer, are one per login, and how much surface a
92
194
  panel gets is the design saying how much it matters:
93
195
 
94
196
  - The login carrying the terminals gets a wide panel, lit one step brighter than
@@ -113,7 +215,7 @@ one it does not have.
113
215
  Nothing here is on hover. A screen reader gets the whole answer from each
114
216
  track's `aria-valuetext`, including the reset, the source and the staleness; a
115
217
  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.`
218
+ its label. The caption at the foot of the drawer reads `Times are local.`
117
219
 
118
220
  For Codex, the board reads the app-server's read-only
119
221
  `account/rateLimits/read` response every 60 seconds and maps its 300- and
@@ -131,7 +233,11 @@ within a second of the agent's first turn.`
131
233
 
132
234
  The region head carries one verdict with its volume: `3 running, 2 waiting on
133
235
  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.
236
+ `, 3 share the claude login` when two or more live rows are on one login, and
237
+ `, last landed 11:02 PM` appended when a terminal on the page has landed. The
238
+ share clause is the only place per-login burn is addressed: a figure on a row
239
+ is per model, which is real, and the reader who would add three rows' figures
240
+ together is stopped here, once, at the region.
135
241
 
136
242
  ### Terminal row
137
243
 
@@ -143,11 +249,25 @@ the state is never carried by colour or position alone.
143
249
  Reading across the row: what it is doing, what it is working on, how long it has
144
250
  been at it, and what you can do about it.
145
251
 
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.
252
+ - **The register**, one line of the smallest type on the board, read left to
253
+ right: the status word with its dot, then where the work is (`leg on main`,
254
+ or the folder when it is not a repo), then `dirty 3` and `ahead 2` when the
255
+ git poll has counted them, then the model token `claude/fable`, then
256
+ `quiet 4m` when the terminal has said nothing for two minutes or more. After
257
+ those come the words that are unusual about this terminal: the account when
258
+ it is not `default`, the owner on a shared board, `from <agent>` when it was
259
+ handed off, `own worktree, from main` when it cut its own.
260
+ - `dirty 3` is `files_dirty`, `ahead 2` is the commit count against the base.
261
+ Neither is printed as a zero, and `ahead` is absent on a record written by
262
+ an older Leg rather than shown as `ahead 0`.
263
+ - The model token is the agent and the model this leg resolved to
264
+ (`claude/fable`, `codex/gpt-5.6-sol`). With no model on the record it is the
265
+ agent alone, `claude`: a printed model nobody chose is a wrong number in
266
+ disguise, so there is never a default. For Claude it is refreshed from the
267
+ transcript, so a silent fallback off Fable shows up here.
268
+ - `quiet 4m` is `last_activity`, in the muted tone, for every agent. It is an
269
+ observation, not a demand. A terminal that is waiting on you says that
270
+ instead, and a finished one says nothing: it has stopped, not gone quiet.
151
271
  - **The prompt**, as a button: the first prompt of the session, carried in full
152
272
  in its tooltip and in the expansion. A pasted screenshot arrives as an
153
273
  `<image name=... path=...>` tag with an absolute temp path in it; the tag is
@@ -161,6 +281,34 @@ been at it, and what you can do about it.
161
281
  file another live session is also touching is printed in the warning colour.
162
282
  A terminal that is merely running prints no sentence at all: its own row
163
283
  already says so, and four rows each saying `activity` is four lines of noise.
284
+ - **The capacity phrase**, at the right of the files line: `63% of the fable
285
+ week`, the bucket that will actually stop THIS terminal. It is
286
+ `binding(usage, session.model)`, computed per request, so a row on Fable and a
287
+ row on Sonnet in the same login can print different figures and both are
288
+ true. The figure is per model; the reader who would add three rows' figures
289
+ together is stopped by the share clause at the region head, once. Nothing is
290
+ printed when no bucket is known: a percentage nobody measured is worse than
291
+ no percentage at all.
292
+ - Once that bucket has **three readings spanning ten minutes**, the phrase
293
+ becomes the time instead: `about 2h 40m of fable left, from 9 samples over
294
+ 4h`. The sample count is part of the phrase and is never dropped. Under the
295
+ gate it stays the percentage and the reset clock, and the time never runs
296
+ past the reset: a rate measured in this window says nothing about the next
297
+ one.
298
+ - The strip token above keeps the percentage even while the row says the
299
+ time. The strip is the measurement and the time is the inference, and the
300
+ inference lives where its sample count can sit beside it.
301
+ - **`Back to fable`**, a link on the capacity line beside that figure, on a row
302
+ that Leg dropped to a lower model and can climb again. It appears only when
303
+ both facts are known: the row's model is below the top rung of its own
304
+ login's ladder, and that top rung is open (no wall on the model, no wall on
305
+ the login, and either no bucket for it or a bucket under 100). Pressing it
306
+ replaces the button row with one sentence, `Hands off now. The current turn
307
+ stops and fable continues from the bundle.`, or `from the conversation` when
308
+ that rung is one a `--resume` keeps. Confirming posts the hand-off with the
309
+ model on it; a rung that walled between the draw and the press comes back as
310
+ the server's own 409 sentence in the row. It is a link and not a fifth button:
311
+ the 2x2 grid is the shipped shape and does not reflow.
164
312
  - **The clock**: elapsed since the session started (`4h 24m`), and the session's
165
313
  short id. The id used to print as `claude-7f3a` immediately after the word
166
314
  `claude`; the prefix is the agent name twice and it is gone.
@@ -170,6 +318,16 @@ been at it, and what you can do about it.
170
318
  because a disabled control should not wear the one accent colour in the
171
319
  design. When Land is disabled its reason is printed, never left in a tooltip.
172
320
 
321
+ **A terminal waiting on a human says so, and the tab says it too.** Claude
322
+ Code's Notification hook writes `waiting` on the record when it puts up a
323
+ permission prompt, when it has been idle at the prompt, or when a subagent asks
324
+ for input; the row's status word becomes `waiting on you`, its sentence is the
325
+ question itself (rank 3 below), and it sorts to the top of the region and into
326
+ the region head's count. While any row is waiting the tab title reads
327
+ `(1) Leg` and the favicon carries a dot, always, with no permission and no
328
+ setting. codex, agy and grok publish no such signal, so their rows never say
329
+ `waiting on you`; they say `quiet 4m`.
330
+
173
331
  **A fact true of every row is said once, at the region.** Three rows all reading
174
332
  `this terminal works in the checkout itself: there is no branch of its own to
175
333
  land` is one fact and two lines of noise, so it is hoisted to the region head as
@@ -199,12 +357,13 @@ rest behind `also:`. Rank 1 is worst.
199
357
  |------|--------------|
200
358
  | 1 | `every option is out, first back: codex 10:11 PM`, or `limit: <reason>, back <time>` |
201
359
  | 2 | a bounced Land, or `the landing was cut off (the board restarted); press Land again` |
202
- | 3 | an overlap with another live session |
360
+ | 3 | `waiting on you: permission to run Bash(git push origin HEAD), asked 40s ago`, `waiting on you: idle since 11:04 PM`, or an overlap with another live session |
203
361
  | 4 | `sam asked to take this terminal at 11:04 PM` |
204
362
  | 5 | `waiting for codex at 10:11 PM` |
205
363
  | 6 | `handing off to codex, <reason>, 2m` |
206
364
  | 7 | `landing <branch> onto main: commit, rebase, test, fast-forward` |
207
365
  | 8 | `near the 5h wall, next: codex` |
366
+ | 8.5 | `fable at 92% of its week; Hand off > claude/opus keeps this terminal`, or `claude at 97%, shared by every model; next off claude: codex` |
208
367
  | 9 | a finished Land, `nothing to land`, or the button you last pressed |
209
368
  | 10 | `turn 14, last activity 11:04 PM`, the fallback that is always true |
210
369
 
@@ -246,6 +405,15 @@ is counting down to the first reset; its sentence reads `waiting for <agent> at
246
405
  <time>`, and End quits that terminal with exit 3. `lost` means the runner
247
406
  process that owned that terminal is gone. It is never counted as live.
248
407
 
408
+ A tenth word, `waiting on you`, replaces the status word on any row that needs a
409
+ human. Two different things live on the record's `waiting` key and the board
410
+ tells them apart by `type`, never by which fields are set: `type: reset` is the
411
+ all-out countdown above, where the child is already dead and nobody is being
412
+ waited on, and `permission_prompt`, `idle_prompt`, `agent_needs_input` and
413
+ `quota_auto_resume` are a human being waited on. `quota_auto_resume` is the one
414
+ that prints `Claude Code is waiting at the limit itself; Leg is not handing this
415
+ one off.`, because two waiters on one terminal is the failure to avoid.
416
+
249
417
  ### Terminal buttons
250
418
 
251
419
  The order is fixed, Land, Hand off now, Details, End, and it never reflows by
@@ -257,16 +425,18 @@ availability: a button that does not apply is omitted, never moved.
257
425
  | 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 |
258
426
  | 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 |
259
427
  | 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 |
260
- | 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 |
261
- | 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 |
428
+ | Hand off now | the session is active | asks first in the row's confirm row (`Hands off to the first open rung of this terminal's ladder. The current turn stops.`), then `POST /api/sessions/:id/handoff`: saves the bundle, stops this agent, starts the next option in the same terminal. It stops a working agent mid-turn, so it asks whether the mouse or the `h` key pressed it |
429
+ | Change the ladder | inside the expansion, while the status is `starting`, `running`, `warning`, `limit` or `waiting` | `POST /api/sessions/:id/handoff-order` with the whole `handoff_ladder`; an older wrapper instead saves the machine default and says to restart the terminal |
262
430
  | End | the session is active | `POST /api/sessions/:id/end`: stops the agent, ends the session |
263
431
  | 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 |
264
432
  | 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 |
265
433
 
266
434
  End, Remove and Remove record confirm first: the button row is replaced in place
267
- by one sentence and two buttons, focus moves to Cancel, and Escape cancels.
268
- There is no modal and no browser `confirm()`. Settings has the same order editor
269
- for the default copied by new terminals.
435
+ by one sentence and its buttons, focus moves to Cancel, and Escape cancels. End
436
+ offers a second verb, **End, and keep going as a card**, when the terminal is in
437
+ a repository and the reader may use cards.
438
+ There is no modal and no browser `confirm()`. Settings has the same ladder
439
+ editor for the default copied by new terminals.
270
440
 
271
441
  ### The expansion
272
442
 
@@ -289,11 +459,12 @@ from the right, and the page keeps one scroll container
289
459
  `new` or `committed` when there are no counts against `HEAD`. A row expands
290
460
  to that file's diff, capped at 400 lines.
291
461
  6. **Timeline**: the last 40 events, newest first.
292
- 7. **What happens next**: `now: claude, then codex, then agy`, which fallback is
293
- preferred and which is eligible now, the line `Used after a usage limit or
294
- Hand off now. A normal exit ends this terminal.`, the **Change order**
295
- editor, the current bundle id, and whether `.leg/RESUME.md` still describes
296
- the repository (recomputed from git on every poll).
462
+ 7. **What happens next**: `now: claude / opus, then claude / fable, then
463
+ claude / sonnet, then codex, then agy`, which rung is preferred and which is
464
+ eligible now, the line `Used after a usage limit or Hand off now. A normal
465
+ exit ends this terminal.`, the **Hand off now to** picker, the **Change the
466
+ ladder** editor, the current bundle id, and whether `.leg/RESUME.md` still
467
+ describes the repository (recomputed from git on every poll).
297
468
  8. **Harness** (only when the [portable harness](harness.md) is on): the
298
469
  source client, when it was captured and synced, the policy, one line for
299
470
  the leg now running (`codex harness partial · 8/8 components, 3 dropped ·
@@ -309,11 +480,73 @@ stays live, so the panel's own buttons keep working. Messages and diffs are
309
480
  scrubbed for secrets on the way out, and the whole region is refused for a
310
481
  terminal that belongs to someone else. **Close** or the Escape key closes it.
311
482
 
312
- **Change order** moves the three agents without removing one; same-agent
313
- secondary accounts still come before the other agents. The list is an absolute
314
- priority: an agent moved to the bottom is tried last from every starting agent.
315
- The editor previews the resulting priority, with the agent running now skipped,
316
- before saving.
483
+ **The Hand off now to picker** lists one row per rung of this terminal's ladder,
484
+ each of them three fields separated by a middot:
485
+
486
+ ```
487
+ claude / opus · same terminal, keeps the conversation · ready
488
+ claude / sonnet · new agent, from the bundle · ready
489
+ codex · new agent, from the bundle · at its usage limit until Sat 10:11 PM
490
+ claude / fable · new agent, from the bundle · ready, spends usage credits
491
+ agy · new agent, from the bundle · only when every rung above it is walled
492
+ ```
493
+
494
+ The rung first, then what taking it does to the conversation, then what it costs
495
+ you right now. The state word is `ready` for a rung that is simply open; a rung
496
+ that cannot be taken carries **the server's own reason**, verbatim, and is
497
+ disabled, with `until <clock>` when there is a reset to name. A rung that is
498
+ open but worth a warning keeps its reason as plain text and stays clickable:
499
+ `past your 10% reserve` is the one you will see, because a human pressing this
500
+ button ignores the reserve and a floor you cannot see is a floor you swear at.
501
+ The refusal that reads `shares the window that is out, buys nothing` is a
502
+ same-login rung under an account-wide wall: every model shares that window, so
503
+ switching model would buy nothing. The rung Leg would take on its own is not in
504
+ the list twice; the terminal's current rung is not in it at all. Picking one
505
+ replaces the row's button grid with a sentence naming the destination and
506
+ whether the conversation survives, and the hand-off carries the model. The
507
+ choice survives the expansion's 3-second refetch: it is held as a rung, so the
508
+ row it names is still selected after the list is rebuilt, or reordered, under
509
+ it, and the poll stands down entirely while the select has focus.
510
+
511
+ **Change the ladder** edits this terminal's own copy. Each rung is a numbered
512
+ row: the agent and model with the login's dot, a `when` select (`always`,
513
+ `below N%` with a number from 0 to 100, which is the range the server itself
514
+ accepts, or `walled only`), the cost word, and **Up**, **Down**, **Remove**.
515
+ Clearing the number box puts the rung back to `always` rather than inventing a
516
+ percentage; a stored `below:0` or `below:100` is shown as the number that is
517
+ stored, with a line saying what it does (`below 0% is never true: this rung is
518
+ never taken`), and is never quietly rewritten. `+ Add a rung` takes an agent and
519
+ one of its models, or `default` for the model the CLI picks itself; a rung the
520
+ ladder already carries is refused in the status line by name and position
521
+ (`claude / opus is already rung 2.`), never silently. There is no drag: a
522
+ list this short is faster with two buttons and a drag has no keyboard. Saving
523
+ posts the whole ladder; a rung the server will not accept comes back as its own
524
+ sentence (`claude has no model "gpt-5"`) in the status line, and nothing is
525
+ saved. The draft line under the list previews what this terminal would try
526
+ next, with the rung it is on skipped.
527
+
528
+ **Settings** carries the same editor for the machine default, under
529
+ `New terminal handoff ladder`, plus the three rules that are machine-wide:
530
+
531
+ - **Let an automatic hand-off spend**, with the sentence `A rung that spends
532
+ usage credits or metered balance may be taken by an automatic hand-off.` Off
533
+ by default. While it is off, a `credits` or `metered` rung is skipped by an
534
+ unattended hand-off and the ledger says which and why; a human pressing Hand
535
+ off can still take it. When the claude login reports credits off, the panel
536
+ adds `Usage credits are off, so there is nothing to spend through the wall.`
537
+ and offers no button to turn them on, because that cannot be done from here.
538
+ - **Climbing back**, two radios: `Leg climbs back to the top rung at the next
539
+ hand-off.` (the default) or `Stay on the lower rung until you press Back to
540
+ fable.` Under them, the rule that is not a choice:
541
+ `Leg never interrupts a running turn to climb.`
542
+ - **Keep N% of `<login>` for your own terminals**, one number per login on the
543
+ ladder. 0 keeps nothing back. An automatic hand-off skips a rung whose login
544
+ is past the floor; a hand-off you press yourself still takes it, and the
545
+ picker prints `past your 10% reserve` on that row rather than hiding it.
546
+
547
+ Saving Settings writes `handoff_ladder`, `climb_back`, `may_spend` and
548
+ `reserve` in one `PATCH /api/settings`, and the status line reports the
549
+ `handoff_order` the ladder derives, which is what older terminals still read.
317
550
 
318
551
  ## The ledger
319
552
 
@@ -353,9 +586,10 @@ heading, a line of detail and a button that opens the detail below the row.
353
586
  or `N uncommitted`. Nothing here is pushed over the live stream; the count
354
587
  re-reads itself once a minute while the drawer is closed. On a shared board
355
588
  a guest sees `The owner of this machine sees them.` and nothing else.
356
- - **N background tasks**, the card runtime below, as a count with **View N
357
- cards**, plus **New card**. With none it reads `Nothing is queued. Leg starts
358
- the next login only when a terminal hands off.`
589
+ - **N finished cards**, `3 finished cards, 2 done, 1 failed, last 11:02 PM`,
590
+ with **View** opening them newest first. Live cards are not here: they are
591
+ rows in the Background panel under Terminals, with the one-line entry under
592
+ it. With none finished it reads `No finished cards`.
359
593
 
360
594
  ## A shared board (more than one human)
361
595
 
@@ -378,9 +612,14 @@ link: that side belongs to the owner of the machine, and the API answers 403.
378
612
 
379
613
  ## Background tasks: page layout
380
614
 
381
- Everything from here down is the v0.1 card runtime. Each card runs separately
382
- from the interactive terminal conversations, in its own git worktree. It is the
383
- third ledger cell, and its rows open in a drawer under the ledger.
615
+ Everything from here down is the card runtime. Each card runs separately from
616
+ the interactive terminal conversations, in its own git worktree. Liveness, not
617
+ kind, decides where a card appears: a live card (`backlog`, `queued`,
618
+ `running`, `handing_off`, `needs_approval`, `waiting_human`, `paused`) is a row
619
+ in the **Background** panel directly under Terminals, in the terminal row's own
620
+ shape; a finished one (`done`, `failed`, `killed`) collapses into the ledger
621
+ cell, `10 finished cards, 5 done, 5 failed, last 8:15 PM`, whose **View** opens
622
+ them newest first. Ten finished cards are one row, not ten.
384
623
 
385
624
  The masthead (`src/board/index.html`) has the Leg wordmark on the left and, on
386
625
  the right, the connection word (`connecting` / `live` / `reconnecting…`) with its
@@ -389,15 +628,55 @@ dot, the scheduler status (`scheduler running, 2 max`), and the **Floor** link.
389
628
  last region of the page, in flow. Nothing on the board is sticky: the verdict is
390
629
  what you came for and it is at the top, so there is nothing to pin.
391
630
 
392
- With no cards the region prints `No background task. New card queues one. They
393
- run headless in their own worktree and report on the floor.` With cards the
394
- region head prints the counts instead: `2 running, 1 queued, 3 finished`.
631
+ With no live cards the Background panel's rows are gone and only its entry line
632
+ is left; with live cards the region head prints the same counts a terminals
633
+ head does, `1 running, 1 waiting on you`. Under the rows sits the entry: a
634
+ single field, `Run in the background:`, a **Start** button, and an inferred
635
+ sentence whose nouns are buttons, `in leg on main, with claude/fable then
636
+ claude/opus then codex, build only`, plus **More settings** for the full form.
637
+ Start with an empty task is disabled and says why.
638
+
639
+ The sentence describes exactly what Start posts, noun by noun. The branch is
640
+ the repo's own default (the server reads `origin/HEAD`, then `main`, `master`
641
+ and `trunk`, then the branch the checkout is on) and it is sent with the card,
642
+ so a repo whose default is `master` or `develop` starts one instead of being
643
+ refused by a trunk that does not exist. The ladder clause is one leg per rung
644
+ carrying that rung's model: `claude/fable then claude/opus then codex` posts
645
+ three legs, the first two on one login and two different models, which is the
646
+ hand-off the ladder exists for. Two rungs that name the same agent AND the same
647
+ model are one leg, not two, so the sentence never lists a hand-off to a leg's
648
+ own twin. Pressing the ladder noun opens two selects: the rung the chain starts
649
+ at, and the model that first leg runs on, taken from that provider's catalog
650
+ ([`GET /api/models`](cli-contracts.md#get-apimodels)). The model picked there
651
+ applies to the leg that starts and nothing else; the fallbacks under it keep the
652
+ models the ladder gave them, and the saved ladder is not touched. **More
653
+ settings** below the line opens the full form with the same rows in it.
654
+
655
+ A ladder Leg cannot read is never silently empty. An install whose
656
+ `preferences.json` predates the ladder carries only `handoff_order`, and the
657
+ line derives one rung per agent from it; a board that never got an answer from
658
+ `/api/settings` at all falls back to the adapters that are installed. The
659
+ sentence says `no agent is configured` only when there is genuinely nothing
660
+ installed, and says `every agent here bills by the token, and spending is off`
661
+ when there is, but the spend rule is holding it back.
395
662
 
396
663
  **Settings** holds the **API token** field (only needed when the server is bound
397
664
  off loopback; see [configuration.md](configuration.md#network-exposure)), the
398
- **New terminal handoff order** editor used by terminals started after you save,
399
- and the board's own facts: version and bind address, who you are signed in as,
400
- whether share is on, the scheduler, and the board home. Its region head says
665
+ **New terminal handoff ladder** editor used by terminals started after you save
666
+ (the rungs, the spend rule, the climb-back policy and the per-login reserve),
667
+ **Notifications**, and the board's own facts: version and bind address, who you are signed in as,
668
+ whether share is on, the scheduler, and the board home. **Notifications** holds
669
+ two toggles, both about a terminal that is waiting on you: *Terminal toast when
670
+ a terminal waits on you* (on by default, Claude Code only: an OSC 9 sequence
671
+ returned by the Notification hook, which is the toast Windows Terminal renders
672
+ in the window the agent is already running in) and *Browser notification when a
673
+ terminal waits on you* (off by default; the browser asks for permission the
674
+ first time you turn it on, and it fires on the transition into waiting, never on
675
+ every refresh). The board toggle reads `window.isSecureContext` when it renders;
676
+ where that is false it is disabled and says `This page is not a secure context.
677
+ Open the board at http://localhost:<port> to turn toasts on.` The tab title
678
+ badge has no toggle: it needs no permission, so it is always on. Its region head
679
+ says
401
680
  `No API token set, requests reach 127.0.0.1:4747 unauthenticated`, or `API token
402
681
  set, sent to 127.0.0.1:4747 as a bearer token` once a token is stored.
403
682
 
@@ -428,12 +707,21 @@ Each card row reads in the same four registers as a terminal panel:
428
707
  - **What**: the title (the card's `title`, or the first 60 characters of the
429
708
  task) as a button that opens the [expansion](#card-expansion), and one
430
709
  sentence under it.
431
- - **Where**: `repo@trunk`, the worktree path shortened to its tail with the full
432
- path in its tooltip, the chain rail, and the lease chips, one per claimed
433
- lease or `**` (the whole repo) when none were set, plus `blocked by lease`
434
- when the scheduler says so.
435
- - **When**: the elapsed clock, `mm:ss` while a leg is running and `--:--`
436
- otherwise, then the buttons.
710
+ - **Where**: `<repo> on <branch>`, where the branch is the one that checkout is
711
+ actually on: `leg/<card-id>` for a worktree the card cut, and the terminal's
712
+ own branch for one it adopted from "End, and keep going as a card", which is
713
+ not a name anyone can derive from the card id. It is shortened to 32
714
+ characters on the row with the whole line in the tooltip, so what you copy
715
+ into `git checkout` is a branch git has. A card with no checkout yet prints
716
+ the trunk it will branch from. Then the chain rail, and the lease chips, one
717
+ per claimed lease or `**` (the whole repo) when none were set, plus `blocked
718
+ by lease` when the scheduler says so.
719
+ - **When**: the elapsed clock, then the buttons. `mm:ss` (or `hh:mm:ss`) counts
720
+ the leg that is running now, exactly as a terminal row's clock does. A live
721
+ card between runs has no leg to time, so it prints `idle 12m`, the time since
722
+ its last state change, labelled for what it is: the card's own age in that
723
+ column would read `24:05:00` for a card that sat in the backlog for a day. A
724
+ card that has never run prints `--:--`.
437
725
 
438
726
  The log is not on the row. It is in the expansion.
439
727
 
@@ -498,23 +786,48 @@ bottom:
498
786
 
499
787
  ## New card form
500
788
 
501
- The dialog opened by **New card** (`src/board/index.html`):
789
+ The dialog opened by **New card** (`src/board/index.html`,
790
+ `docs/screenshots/new-card-dialog.png`):
791
+
792
+ Two columns on a screen 900px or wider, one on a phone: what the work is on the
793
+ left, who runs it on the right. It opens prefilled with the repo, branch and
794
+ ladder the entry line above it would have used, so the dialog and that line say
795
+ the same thing until you change one.
502
796
 
503
797
  | field | notes |
504
798
  |-------|-------|
505
- | Repo path | required; an absolute path to a git repository |
506
- | Task | required; the prompt every leg gets |
507
- | First agent | required; real agents appear here and Claude is preferred when installed |
799
+ | Task | required, and takes the caret when the dialog opens; the prompt every leg gets |
800
+ | Repository | a picker of every repo this board has seen, plus `Another path, typed below` |
801
+ | Path | required; an absolute path to a git repository root. The picker fills it; you can type any path over it |
802
+ | Branch to cut from | the repo's own default branch, read from the sessions payload, not a hardcoded `main` |
508
803
  | Run now | checked by default; unchecking saves the card as a draft in `backlog` |
804
+ | Who runs it | the ordered list below; at least one row is required |
805
+ | Save as my default ladder | writes the rows back to `preferences.handoff_ladder` through `PATCH /api/settings`, so the entry line and every new terminal use them too |
509
806
  | 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 |
510
- | First-agent controls (Advanced) | allowed permission mode, approval gate, max turns, and scripted behavior when the test/demo override is selected |
511
- | Fallback agents (Advanced) | one understandable row per later adapter, tried in displayed order only if the previous agent cannot continue |
512
- | Scripted first agent (Advanced) | explicitly test/demo only; keeps fake adapters out of the normal first-agent default |
807
+ | Scripted first agent (Advanced) | explicitly test/demo only; it replaces the agent on the first row |
513
808
  | File leases (Advanced) | comma-separated path globs to reserve so overlapping cards wait |
514
- | Trunk branch | default `main` |
515
- | Merge method | `ff` or `pr`; relevant to a workflow with a land station |
516
- | Test command | overrides the land station's auto-detected command |
517
- | Title | optional; defaults to the task's first 80 characters |
809
+ | Merge method (Advanced) | `ff` or `pr`; relevant to a workflow with a land station |
810
+ | Test command (Advanced) | overrides the land station's auto-detected command |
811
+ | Card title (Advanced) | optional; defaults to the task's first 80 characters |
812
+
813
+ **Who runs it** is one row per leg, in the order Leg tries them. The first row is
814
+ the agent that starts; every row under it is a fallback, taken only when the row
815
+ above it cannot continue. Each row carries:
816
+
817
+ | control | notes |
818
+ |---------|-------|
819
+ | provider | the installed adapters; a test/demo adapter is labelled as one |
820
+ | model | that provider's own catalog from [`GET /api/models`](cli-contracts.md#get-apimodels), with the provider's default as the first option. `codex` lists `gpt-6-astra`, `gpt-5.6-luna` and the rest of its visible models; `claude` lists Fable, Opus, Sonnet and Haiku |
821
+ | permissions | the adapter's own allowed modes, its default preselected |
822
+ | ask before start | gates that leg behind an approval |
823
+ | max turns | a cap for that leg alone |
824
+ | test behavior | scripted outcome, on a test/demo row only |
825
+ | Up / Down / Remove | reorder or drop the row; the last remaining row cannot be removed |
826
+
827
+ **Add a fallback** appends a row for the first installed agent no row already
828
+ names. Every field is per row, so `claude/fable` then `claude/opus` are two real
829
+ legs with their own models, permissions and limits: the older form keyed those
830
+ by adapter and could not tell one claude row from another.
518
831
 
519
832
  **Cancel** closes without creating a card; **Create card** posts it and closes on
520
833
  success (errors show inline above the form).
@@ -536,6 +849,33 @@ is fixed: Approve, Run, Resume, Pause, Hand off now, Rerun, Reassign, Kill.
536
849
  | 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 |
537
850
  | Rerun | `done`, `failed`, `killed` | start the station over from the last bundle |
538
851
  | Remove | `done`, `failed`, `killed` | delete the card record, its events and its runs; the worktree is kept (confirms first) |
852
+ | Take over | any non-terminal status, in the [expansion](#card-expansion) | you take the card's checkout yourself |
853
+
854
+ **Take over** is the one place the board hands you a command, because a browser
855
+ tab cannot open a terminal: it prints `leg claude --resume-card 3e1c`, which
856
+ starts an interactive terminal in the card's own worktree, primed from the
857
+ card's bundle. Before it prints anything the card is moved to `paused`,
858
+ whatever it was doing: a running one has its child killed and its bundle
859
+ written, and a `queued` or `handing_off` one is taken out of the set the
860
+ scheduler starts from, so nothing launches a leg into the checkout you are
861
+ about to sit down in. The card's ledger gets one `taken_over` line with your
862
+ name on it. **Resume** puts it back to work when you are done. A card that is
863
+ `done`, `failed` or `killed` has nothing to take over and says so.
864
+
865
+ **End, and keep going as a card** is the same trade in the other direction: it
866
+ is the second verb on a terminal's End confirm row (shown for a terminal in a
867
+ git repository, to the owner or an operator). It writes the terminal's hand-off
868
+ bundle, makes a card whose task is the terminal's prompt plus `Continue from
869
+ the bundle at <path>`, starts its ladder at the rung that terminal was on, and
870
+ then ends the terminal. Where the card works depends on what the terminal had:
871
+ a terminal with a worktree of its own hands it over, and the card waits in the
872
+ backlog until that terminal has really stopped, because `End` is a request its
873
+ runner reads on its own poll and two agents in one working tree is the thing
874
+ this whole feature exists to avoid. A terminal working in the repo itself gets
875
+ the card a checkout of its own, cut from the same branch, with the uncommitted
876
+ work carried into it: the tracked edits as a patch and every file git has not
877
+ seen copied across. If that work cannot be read or cannot be carried, nothing
878
+ is created and the terminal is left running with the reason.
539
879
 
540
880
  ## What a state looks like
541
881
 
@@ -549,35 +889,95 @@ is fixed: Approve, Run, Resume, Pause, Hand off now, Rerun, Reassign, Kill.
549
889
  | needs approval | 0 | needs approval | Approve, Kill, Reassign* |
550
890
  | waiting human | 0 | waiting human (or PR open) | Approve, Kill |
551
891
  | paused | 3 | paused | Resume, Kill, Reassign* |
552
- | done | 6 | done | Rerun, Remove |
553
- | failed | 1 | failed | Rerun, Remove |
554
- | killed | 1 | killed | Rerun, Remove |
892
+ | done | not a row | done | none on the board |
893
+ | failed | not a row | failed | none on the board |
894
+ | killed | not a row | killed | none on the board |
555
895
 
556
896
  \* Reassign only shows when the current station is an `agent` station.
557
897
  † Hand off now only shows when that station has a later chain entry.
558
898
 
899
+ The last three are not rows at all. A finished card is one line in the ledger
900
+ drawer (`finishedLine` in `src/board/board.js`), `done after 3 runs, landed
901
+ 7f3a2c1` or `failed at station build after 2 runs: <last event>`, with no
902
+ buttons beside it. `availableActions` in `src/chain.mjs` still returns `rerun`
903
+ for a terminal status, and Remove is its own `DELETE /api/cards/:id`, so both
904
+ verbs remain reachable through the API and `bin/leg.mjs`; the board simply
905
+ stops spending a row on work that has stopped.
906
+
559
907
  ## Floor view
560
908
 
561
909
  `/floor` (`docs/screenshots/floor.png`) is the scheduler-eye view across every
562
- card. It carries the same instrument head as the board, then polls `/api/floor`
563
- and `/api/trunk?since=1h` every 2 seconds and refreshes on the same server-sent
564
- events as the board. Its top bar has the Leg floor brand, the same connection
565
- word and 24 px rule, a repos list, the scheduler status, running/queued/waiting/
566
- done counts, then a spacer and the **Board** link. Five tables:
567
-
568
- | section | columns |
569
- |---------|---------|
570
- | Running | Card, Station, Agent / leg, Leases, Last event, Elapsed, Actions |
571
- | Waiting on humans | Card, Station, Status, Since, Actions |
572
- | Queued | Card, Station, Leases, Blocked by |
573
- | Leases | Lease, Card, Station, Since |
574
- | Trunk lane | Time, Card, Summary |
575
-
576
- Each region head names its own count, `0 running`, `0 landed in the last hour`,
577
- and an empty table is replaced by a sentence saying what would put a row there.
578
- The Trunk lane lists commits landed in the last hour, newest first. The Leases
579
- table is `src/leases.mjs` `held()`: one row per lease currently claimed by a
580
- running or handing-off card.
910
+ card, and it starts work as well as watching it. It polls `/api/floor` (the
911
+ scheduler's own view: leases, blockers, the repos it knows), `/api/cards` (the
912
+ rows), `/api/trunk?since=1h` and `/api/sessions` every 2 seconds, and refreshes
913
+ on the same server-sent events as the board.
914
+
915
+ Top to bottom:
916
+
917
+ 1. **The masthead.** The Leg floor brand, the connection word and its 24 px
918
+ rule, the scheduler status, the repos list and the **Board** link. The four
919
+ counts that used to sit here are beside the station headings now, with the
920
+ rows they count.
921
+ 2. **The capacity strip**, the same one the board draws and from the same file
922
+ (`src/board/strip.js`): one token per login carrying the bucket that will
923
+ actually stop the work. The four login panels are behind the same
924
+ `Capacity and models >` disclosure they are behind on the board, and whether
925
+ it is open is remembered for both pages. On the floor they used to be the
926
+ whole first screen, above every row the page exists to show.
927
+ 3. **The entry row**, the same one the board carries and from the same file
928
+ (`src/board/entry.js`): `Run in the background: [task] [Start]`, with the
929
+ sentence under it naming the repo, the branch, the ladder and the workflow,
930
+ each a button that opens a select in place. Start posts exactly what the
931
+ board's Start posts (`POST /api/cards`), the new card appears in Queued or
932
+ Backlog within one poll with no reload, and a toast names it. **More
933
+ settings** is the one thing the floor hands over: the New card dialog's
934
+ markup exists once, on the board, so the link carries the sentence you typed
935
+ in the hash (`/#new-card=<task>`) and the board opens the dialog with it
936
+ already in the field.
937
+ 4. **Five stations**, each a question with its count in the heading and, under
938
+ it, either the rows or one sentence saying what would put a row there:
939
+
940
+ | station | what is in it |
941
+ |---------|----------------|
942
+ | Running | `running`, `handing_off` |
943
+ | Waiting on you | `needs_approval`, `waiting_human`, `paused` |
944
+ | Queued | `queued`, each row with its position and what it waits for |
945
+ | Backlog | `backlog` |
946
+ | Done today | `done`, `failed`, `killed`, today only, collapsed behind View |
947
+
948
+ A row is the board's own card row (`.row`: state, station, repo on branch,
949
+ agent/model; the title; the one sentence; the run clock or `idle 12m`; the
950
+ short id; and at most four of its available actions in the fixed order).
951
+ The title is a link to that card on the board (`/#card=<id>`), which is
952
+ where the expansion, the log tail and Take over live: one copy of each.
953
+ **Reassign** opens the board's own picker in place of the row's buttons, an
954
+ adapter and a mode with Apply and Cancel, because the API refuses a reassign
955
+ with no adapter. Every other button posts its action straight away.
956
+ A queued row adds `2 of 3 in the queue` and, when the scheduler has said
957
+ why, its own words for what holds it; with the scheduler stopped it says
958
+ `the scheduler is stopped` rather than blaming a slot that nothing is going
959
+ to free. A row you have focus in is not rebuilt under your finger for 30
960
+ seconds, so a press, a picker or a selection survives the 2-second poll;
961
+ every other row in that station keeps updating meanwhile.
962
+ 5. **Leases** and **Trunk lane**, the two tables the floor alone has:
963
+
964
+ | section | columns |
965
+ |---------|---------|
966
+ | Leases | Lease, Card, Station, Since |
967
+ | Trunk lane | Time, Card, Summary |
968
+
969
+ The Trunk lane lists commits landed in the last hour, newest first. The
970
+ Leases table is `src/leases.mjs` `held()`: one row per lease currently
971
+ claimed by a running or handing-off card. `/api/trunk` is the map of the
972
+ machine and stays with the owner (`src/share.mjs` `mayUseMachine`), so an
973
+ operator sees this one table replaced by a line naming whose it is; the
974
+ stations, the entry row and the leases are theirs as usual.
975
+
976
+ `j` and `k` move a ring down and up the rows, and Enter opens the ringed card on
977
+ the board. Enter belongs to whatever has focus: on a button, a link or the
978
+ disclosure it presses that control, and it only opens the ringed card when
979
+ focus is on the page itself. At 400 px the page is one column: the row's
980
+ actions move under its sentence and nothing scrolls sideways.
581
981
 
582
982
  ## Keyboard and accessibility
583
983
 
@@ -585,9 +985,42 @@ running or handing-off card.
585
985
  summaries) gets a visible focus ring: `outline: 2px solid var(--focus);
586
986
  outline-offset: 2px` on `:focus-visible` only (`src/board/board.css`). There
587
987
  is no bare `:focus` rule, so a mouse click leaves no ring behind it.
588
- - Escape closes the open expansion, the terminal's and the card's. If a confirm
589
- row is open, Escape cancels that first. The floor has neither, so Escape does
590
- nothing there.
988
+ - Escape closes the open expansion, the terminal's and the card's. If the
989
+ keyboard map is open, Escape closes that first, then a confirm row if one is
990
+ open. The floor has neither, so Escape does nothing there.
991
+ - **The keyboard map.** `?` opens and closes a small panel listing every
992
+ binding. Every key CLICKS a button that is already on the row, so no key is a
993
+ second way to do anything:
994
+
995
+ | key | what it does |
996
+ |-----|--------------|
997
+ | `j` | move the ring to the next terminal |
998
+ | `k` | move the ring to the previous terminal |
999
+ | `1` to `9` | move the ring to that terminal |
1000
+ | `h` | press Hand off now on the terminal the ring is on |
1001
+ | `l` | press Land on the terminal the ring is on |
1002
+ | `d` | press Details on the terminal the ring is on |
1003
+ | `e` | press End on the terminal the ring is on |
1004
+ | `?` | open and close the map |
1005
+ | `Escape` | close the map, cancel a confirm row, or close an expansion |
1006
+
1007
+ The ring is a bar down the left edge of the row, and moving it also moves
1008
+ keyboard focus to that row's first button, which is its prompt, so a screen
1009
+ reader announces which terminal the ring landed on rather than a bare `Land`.
1010
+ A disabled button is never clicked, so `l` does nothing on a row that cannot
1011
+ land. While an input, a select or a textarea has focus, every one of these
1012
+ keys is text and nothing else.
1013
+
1014
+ The ring belongs to a TERMINAL, not to a position: the grid re-sorts whenever
1015
+ a row starts waiting on you, and the ring stays on the terminal you put it on.
1016
+ If that terminal ends and moves to the ledger, the ring is cleared rather than
1017
+ inherited by whatever row took its place.
1018
+
1019
+ With no ring set, `h`, `l`, `d` and `e` only set the ring on the first row and
1020
+ paint it; it takes a second press to act, so no key ever acts on a terminal
1021
+ that was not marked on screen first. Every one of these keys stands down while
1022
+ a dialog is open, while the keyboard map is open (`?` and Escape still close
1023
+ it), and while a confirm row is waiting for its answer.
591
1024
  - Buttons carry descriptive `aria-label`s (for example "Pause `<card title>`",
592
1025
  "Reassign adapter for `<card title>`") so a screen reader announces which card
593
1026
  an action applies to, not just the button label.