@ucsandman/legcli 0.11.0 → 0.12.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 (56) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +95 -65
  3. package/bin/leg.mjs +123 -14
  4. package/docs/DECISIONS.md +10 -0
  5. package/docs/DEMO.md +20 -14
  6. package/docs/DEVIATIONS.md +1 -0
  7. package/docs/ERRORS.md +41 -0
  8. package/docs/ROADMAP-v2.md +50 -5
  9. package/docs/VOCABULARY.md +27 -0
  10. package/docs/board-guide.md +401 -66
  11. package/docs/cli-contracts.md +199 -5
  12. package/docs/concepts.md +167 -19
  13. package/docs/configuration.md +58 -0
  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/settings-ladder-1280.png +0 -0
  25. package/docs/screenshots/terminals-1280.png +0 -0
  26. package/fixtures/limits/claude/claude-fable-limit.json +11 -0
  27. package/fixtures/limits/claude/claude-model-limit.json +1 -1
  28. package/fixtures/limits/claude/claude-session-limit.json +1 -1
  29. package/fixtures/limits/claude/claude-weekly-limit.json +1 -1
  30. package/fixtures/live/claude/resume-model-probe.json +20 -0
  31. package/fixtures/live/claude/usage-oauth.json +87 -0
  32. package/fixtures/verified.json +1 -1
  33. package/package.json +3 -2
  34. package/scripts/seed-fake-cards.mjs +59 -6
  35. package/scripts/seed-wes-board.mjs +81 -12
  36. package/src/accounts.mjs +6 -1
  37. package/src/attach.mjs +328 -46
  38. package/src/audit.mjs +1 -1
  39. package/src/board/board.css +119 -9
  40. package/src/board/board.js +482 -106
  41. package/src/board/index.html +67 -7
  42. package/src/board/sessions.js +1347 -117
  43. package/src/buckets.mjs +101 -0
  44. package/src/cards.mjs +9 -1
  45. package/src/chain.mjs +13 -0
  46. package/src/hook.mjs +7 -1
  47. package/src/ledger.mjs +10 -2
  48. package/src/orchestrator.mjs +13 -4
  49. package/src/preferences.mjs +214 -5
  50. package/src/scheduler.mjs +24 -1
  51. package/src/server.mjs +546 -47
  52. package/src/sessions.mjs +17 -1
  53. package/src/taps/claude-usage.mjs +91 -2
  54. package/src/taps/claude.mjs +144 -5
  55. package/src/taps/codex.mjs +23 -3
  56. package/src/usage.mjs +407 -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
 
@@ -60,35 +74,115 @@ rows while the real screen showed 400px.
60
74
 
61
75
  ## The verdict and the logins
62
76
 
63
- `src/board/sessions.js` draws both, from the `accounts` array of
77
+ `src/board/sessions.js` draws all of it, from the `accounts` array of
64
78
  `GET /api/sessions` and the sessions beside it.
65
79
 
66
80
  **The verdict** is the largest thing on the page and it is a sentence, not a
67
81
  number. It names the one fact that decides what happens next:
68
82
 
69
83
  ```
70
- All 4 terminals are on claude, and claude has 5% left.
84
+ Fable is at 63% of its week, the only login open.
71
85
  ```
72
86
 
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.
87
+ The sentence comes from a branch table with a fixed precedence, and the order
88
+ is what the table is for. A human waiting on a terminal outranks every usage
89
+ figure, because attention is the scarcer thing. Then a login whose whole
90
+ account is nearly spent (`claude has 3% left, shared by every model.`), because
91
+ no model switch can help there. Then a single model at its wall with the login
92
+ still open (`Fable is out until 9:14 PM; opus is open.`), which is the one case
93
+ where a same-login switch is the answer. Then the burn rate, when it is known
94
+ (`About 2h 40m of Fable left.`), because the decision is about the afternoon
95
+ and not about the number. A model bucket that came back while a terminal is
96
+ still downshifted (`Fable is back; leg#7f3a is still on opus.`) sits right after
97
+ the walled-model sentence, since a state change outranks a standing figure.
98
+ Then one login carrying every terminal, several logins carrying work, and the
99
+ quiet states: nothing running and no figure anywhere. Everything walled
100
+ (`Every login is at its limit; codex is back first.`) is decided before any
101
+ per-login sentence, because it is a fact about the whole board. There is
102
+ always a sentence.
103
+
104
+ **Every time figure carries the volume it was drawn from**, always:
105
+ `From 9 samples over 4h. Opus and Sonnet have their own buckets.` The rate is
106
+ measured from the readings Leg has kept for that one bucket, and three rules
107
+ hold it honest. Under **three samples spanning ten minutes** there is no time
108
+ at all: the row prints the percentage and the reset clock instead, because a
109
+ slope drawn through two readings a minute apart is a guess printed in the
110
+ largest type on the page. A rate **never crosses a reset**: the samples are
111
+ cleared when the window rolls over, and the time is capped at the reset even
112
+ when the slope says there is more. A figure that has held flat for an hour is a
113
+ measured zero, not a forecast, and a percentage that fell inside one window is
114
+ a data error rather than a refund; neither prints a time. The second sentence
115
+ names the other models on that login, because a Fable figure is not the login's
116
+ figure and the reader who takes it for one plans the wrong afternoon; on a
117
+ login where every model shares the window it says `Shared by every model.`
118
+
119
+ The headline is capped at **56 characters**, and that number is a measurement,
120
+ not a preference: at 1280 the verdict column is 26ch and 300 sampled sentences
121
+ per length still fit two 52px lines at 60 characters, so 56 is that ceiling
122
+ with slack for a long login name. `test/board-verdict.test.mjs` holds every
123
+ branch under it, including branches fed deliberately long repository and
124
+ account names. A headline that wrapped to three lines is what put a thousand
125
+ pixels between the top of the page and the first terminal row.
78
126
 
79
127
  Under it, one line carrying the age of the reading and which direction it is
80
128
  wrong in:
81
129
 
82
130
  ```
83
- Measured 2h 13m ago. 4 terminals have been running since, so the real figure is
84
- higher than 95 percent, never lower.
131
+ Measured 2h 13m ago. 4 terminals run on it, so the real figure is higher, never
132
+ lower.
85
133
  ```
86
134
 
87
135
  A reading taken two hours ago is a floor, not a measurement, and saying so is
88
136
  the entire reason to print its age. Any login at a wall other than the one in
89
137
  the headline is named on the same line.
90
138
 
91
- **The logins** sit under the verdict, one panel each, and how much surface a
139
+ **The capacity strip** is the band under the verdict: one token per login, a
140
+ dot in its identity colour, its name, a 120 by 6 track and one figure. The
141
+ figure is the **binding** bucket, the one that will actually stop the work:
142
+ the bucket the endpoint itself marks active, else the highest percentage it
143
+ reported, else the hottest of the two legacy windows. That distinction is the
144
+ reason the strip exists. A login whose weekly account window reads 95% and
145
+ whose active Fable bucket reads 63% is stopped by the 63, and the board used to
146
+ print the 95.
147
+
148
+ ```
149
+ claude |=======---| 63% fable week codex |==========| back Sat 10:11 PM
150
+ agy no figure grok no reading
151
+ ```
152
+
153
+ - `63% fable week` is a fresh reading with its bucket word.
154
+ - `100% Tue 9:55 AM` is a reading older than five minutes: the percentage with
155
+ the clock it was taken at, because it describes a moment, not now.
156
+ - `back Sat 10:11 PM` is a login at its wall. The track fills in the wall
157
+ colour and the words carry the state, which is the one place on the board a
158
+ word may wear a severity colour: the word is itself the failure.
159
+ - `no figure` is agy, which publishes no percentage, ever. **No track is
160
+ drawn.** An empty track reads as a measurement of zero.
161
+ - `no reading` is a login that has a window and has never reported it.
162
+ - `not shared` is what a guest sees: usage figures are the owner's.
163
+
164
+ Each track is a `meter` with an `aria-valuetext` carrying the percentage, the
165
+ reset, the source, any wall and the age of the reading, the same sentence the
166
+ gauges carry. A login with no percentage is not a `meter` at all, because
167
+ `aria-valuenow` would have to be a number nobody measured.
168
+
169
+ **`Capacity and models >`**, at the end of the strip, opens a drawer holding
170
+ the login panels unchanged, plus a rail of model chips on each panel head:
171
+ `fable 63%` for a measured bucket, `fable out until 9:14 PM` for a model at its
172
+ wall, and a bare model name where Leg has a name but no figure. A percentage is
173
+ measured and a wall is attributed from wording, so the two are never printed as
174
+ each other. The drawer remembers whether you left it open. The
175
+ `Times are local.` caption lives inside it.
176
+
177
+ An **open chip is a button**: pressing it puts that rung at the top of one
178
+ terminal's ladder, so the next hand-off takes it. The terminal is the one whose
179
+ expansion is open if it is on this login, else the first live row on it, and the
180
+ press is the same `POST /api/sessions/:id/handoff-order` the ladder editor
181
+ makes. A chip stays plain text when it is walled, when it is the model that
182
+ terminal is already running, or when no live terminal on this login can take a
183
+ ladder change: a control that would do nothing is not drawn as one.
184
+
185
+ **The login panels**, in that drawer, are one per login, and how much surface a
92
186
  panel gets is the design saying how much it matters:
93
187
 
94
188
  - The login carrying the terminals gets a wide panel, lit one step brighter than
@@ -113,7 +207,7 @@ one it does not have.
113
207
  Nothing here is on hover. A screen reader gets the whole answer from each
114
208
  track's `aria-valuetext`, including the reset, the source and the staleness; a
115
209
  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.`
210
+ its label. The caption at the foot of the drawer reads `Times are local.`
117
211
 
118
212
  For Codex, the board reads the app-server's read-only
119
213
  `account/rateLimits/read` response every 60 seconds and maps its 300- and
@@ -131,7 +225,11 @@ within a second of the agent's first turn.`
131
225
 
132
226
  The region head carries one verdict with its volume: `3 running, 2 waiting on
133
227
  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.
228
+ `, 3 share the claude login` when two or more live rows are on one login, and
229
+ `, last landed 11:02 PM` appended when a terminal on the page has landed. The
230
+ share clause is the only place per-login burn is addressed: a figure on a row
231
+ is per model, which is real, and the reader who would add three rows' figures
232
+ together is stopped here, once, at the region.
135
233
 
136
234
  ### Terminal row
137
235
 
@@ -143,11 +241,25 @@ the state is never carried by colour or position alone.
143
241
  Reading across the row: what it is doing, what it is working on, how long it has
144
242
  been at it, and what you can do about it.
145
243
 
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.
244
+ - **The register**, one line of the smallest type on the board, read left to
245
+ right: the status word with its dot, then where the work is (`leg on main`,
246
+ or the folder when it is not a repo), then `dirty 3` and `ahead 2` when the
247
+ git poll has counted them, then the model token `claude/fable`, then
248
+ `quiet 4m` when the terminal has said nothing for two minutes or more. After
249
+ those come the words that are unusual about this terminal: the account when
250
+ it is not `default`, the owner on a shared board, `from <agent>` when it was
251
+ handed off, `own worktree, from main` when it cut its own.
252
+ - `dirty 3` is `files_dirty`, `ahead 2` is the commit count against the base.
253
+ Neither is printed as a zero, and `ahead` is absent on a record written by
254
+ an older Leg rather than shown as `ahead 0`.
255
+ - The model token is the agent and the model this leg resolved to
256
+ (`claude/fable`, `codex/gpt-5.6-sol`). With no model on the record it is the
257
+ agent alone, `claude`: a printed model nobody chose is a wrong number in
258
+ disguise, so there is never a default. For Claude it is refreshed from the
259
+ transcript, so a silent fallback off Fable shows up here.
260
+ - `quiet 4m` is `last_activity`, in the muted tone, for every agent. It is an
261
+ observation, not a demand. A terminal that is waiting on you says that
262
+ instead, and a finished one says nothing: it has stopped, not gone quiet.
151
263
  - **The prompt**, as a button: the first prompt of the session, carried in full
152
264
  in its tooltip and in the expansion. A pasted screenshot arrives as an
153
265
  `<image name=... path=...>` tag with an absolute temp path in it; the tag is
@@ -161,6 +273,34 @@ been at it, and what you can do about it.
161
273
  file another live session is also touching is printed in the warning colour.
162
274
  A terminal that is merely running prints no sentence at all: its own row
163
275
  already says so, and four rows each saying `activity` is four lines of noise.
276
+ - **The capacity phrase**, at the right of the files line: `63% of the fable
277
+ week`, the bucket that will actually stop THIS terminal. It is
278
+ `binding(usage, session.model)`, computed per request, so a row on Fable and a
279
+ row on Sonnet in the same login can print different figures and both are
280
+ true. The figure is per model; the reader who would add three rows' figures
281
+ together is stopped by the share clause at the region head, once. Nothing is
282
+ printed when no bucket is known: a percentage nobody measured is worse than
283
+ no percentage at all.
284
+ - Once that bucket has **three readings spanning ten minutes**, the phrase
285
+ becomes the time instead: `about 2h 40m of fable left, from 9 samples over
286
+ 4h`. The sample count is part of the phrase and is never dropped. Under the
287
+ gate it stays the percentage and the reset clock, and the time never runs
288
+ past the reset: a rate measured in this window says nothing about the next
289
+ one.
290
+ - The strip token above keeps the percentage even while the row says the
291
+ time. The strip is the measurement and the time is the inference, and the
292
+ inference lives where its sample count can sit beside it.
293
+ - **`Back to fable`**, a link on the capacity line beside that figure, on a row
294
+ that Leg dropped to a lower model and can climb again. It appears only when
295
+ both facts are known: the row's model is below the top rung of its own
296
+ login's ladder, and that top rung is open (no wall on the model, no wall on
297
+ the login, and either no bucket for it or a bucket under 100). Pressing it
298
+ replaces the button row with one sentence, `Hands off now. The current turn
299
+ stops and fable continues from the bundle.`, or `from the conversation` when
300
+ that rung is one a `--resume` keeps. Confirming posts the hand-off with the
301
+ model on it; a rung that walled between the draw and the press comes back as
302
+ the server's own 409 sentence in the row. It is a link and not a fifth button:
303
+ the 2x2 grid is the shipped shape and does not reflow.
164
304
  - **The clock**: elapsed since the session started (`4h 24m`), and the session's
165
305
  short id. The id used to print as `claude-7f3a` immediately after the word
166
306
  `claude`; the prefix is the agent name twice and it is gone.
@@ -170,6 +310,16 @@ been at it, and what you can do about it.
170
310
  because a disabled control should not wear the one accent colour in the
171
311
  design. When Land is disabled its reason is printed, never left in a tooltip.
172
312
 
313
+ **A terminal waiting on a human says so, and the tab says it too.** Claude
314
+ Code's Notification hook writes `waiting` on the record when it puts up a
315
+ permission prompt, when it has been idle at the prompt, or when a subagent asks
316
+ for input; the row's status word becomes `waiting on you`, its sentence is the
317
+ question itself (rank 3 below), and it sorts to the top of the region and into
318
+ the region head's count. While any row is waiting the tab title reads
319
+ `(1) Leg` and the favicon carries a dot, always, with no permission and no
320
+ setting. codex, agy and grok publish no such signal, so their rows never say
321
+ `waiting on you`; they say `quiet 4m`.
322
+
173
323
  **A fact true of every row is said once, at the region.** Three rows all reading
174
324
  `this terminal works in the checkout itself: there is no branch of its own to
175
325
  land` is one fact and two lines of noise, so it is hoisted to the region head as
@@ -199,12 +349,13 @@ rest behind `also:`. Rank 1 is worst.
199
349
  |------|--------------|
200
350
  | 1 | `every option is out, first back: codex 10:11 PM`, or `limit: <reason>, back <time>` |
201
351
  | 2 | a bounced Land, or `the landing was cut off (the board restarted); press Land again` |
202
- | 3 | an overlap with another live session |
352
+ | 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
353
  | 4 | `sam asked to take this terminal at 11:04 PM` |
204
354
  | 5 | `waiting for codex at 10:11 PM` |
205
355
  | 6 | `handing off to codex, <reason>, 2m` |
206
356
  | 7 | `landing <branch> onto main: commit, rebase, test, fast-forward` |
207
357
  | 8 | `near the 5h wall, next: codex` |
358
+ | 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
359
  | 9 | a finished Land, `nothing to land`, or the button you last pressed |
209
360
  | 10 | `turn 14, last activity 11:04 PM`, the fallback that is always true |
210
361
 
@@ -246,6 +397,15 @@ is counting down to the first reset; its sentence reads `waiting for <agent> at
246
397
  <time>`, and End quits that terminal with exit 3. `lost` means the runner
247
398
  process that owned that terminal is gone. It is never counted as live.
248
399
 
400
+ A tenth word, `waiting on you`, replaces the status word on any row that needs a
401
+ human. Two different things live on the record's `waiting` key and the board
402
+ tells them apart by `type`, never by which fields are set: `type: reset` is the
403
+ all-out countdown above, where the child is already dead and nobody is being
404
+ waited on, and `permission_prompt`, `idle_prompt`, `agent_needs_input` and
405
+ `quota_auto_resume` are a human being waited on. `quota_auto_resume` is the one
406
+ that prints `Claude Code is waiting at the limit itself; Leg is not handing this
407
+ one off.`, because two waiters on one terminal is the failure to avoid.
408
+
249
409
  ### Terminal buttons
250
410
 
251
411
  The order is fixed, Land, Hand off now, Details, End, and it never reflows by
@@ -257,16 +417,18 @@ availability: a button that does not apply is omitted, never moved.
257
417
  | 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
418
  | 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
419
  | 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 |
420
+ | 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 |
421
+ | 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
422
  | End | the session is active | `POST /api/sessions/:id/end`: stops the agent, ends the session |
263
423
  | 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
424
  | 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
425
 
266
426
  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.
427
+ by one sentence and its buttons, focus moves to Cancel, and Escape cancels. End
428
+ offers a second verb, **End, and keep going as a card**, when the terminal is in
429
+ a repository and the reader may use cards.
430
+ There is no modal and no browser `confirm()`. Settings has the same ladder
431
+ editor for the default copied by new terminals.
270
432
 
271
433
  ### The expansion
272
434
 
@@ -289,11 +451,12 @@ from the right, and the page keeps one scroll container
289
451
  `new` or `committed` when there are no counts against `HEAD`. A row expands
290
452
  to that file's diff, capped at 400 lines.
291
453
  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).
454
+ 7. **What happens next**: `now: claude / opus, then claude / fable, then
455
+ claude / sonnet, then codex, then agy`, which rung is preferred and which is
456
+ eligible now, the line `Used after a usage limit or Hand off now. A normal
457
+ exit ends this terminal.`, the **Hand off now to** picker, the **Change the
458
+ ladder** editor, the current bundle id, and whether `.leg/RESUME.md` still
459
+ describes the repository (recomputed from git on every poll).
297
460
  8. **Harness** (only when the [portable harness](harness.md) is on): the
298
461
  source client, when it was captured and synced, the policy, one line for
299
462
  the leg now running (`codex harness partial · 8/8 components, 3 dropped ·
@@ -309,11 +472,73 @@ stays live, so the panel's own buttons keep working. Messages and diffs are
309
472
  scrubbed for secrets on the way out, and the whole region is refused for a
310
473
  terminal that belongs to someone else. **Close** or the Escape key closes it.
311
474
 
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.
475
+ **The Hand off now to picker** lists one row per rung of this terminal's ladder,
476
+ each of them three fields separated by a middot:
477
+
478
+ ```
479
+ claude / opus · same terminal, keeps the conversation · ready
480
+ claude / sonnet · new agent, from the bundle · ready
481
+ codex · new agent, from the bundle · at its usage limit until Sat 10:11 PM
482
+ claude / fable · new agent, from the bundle · ready, spends usage credits
483
+ agy · new agent, from the bundle · only when every rung above it is walled
484
+ ```
485
+
486
+ The rung first, then what taking it does to the conversation, then what it costs
487
+ you right now. The state word is `ready` for a rung that is simply open; a rung
488
+ that cannot be taken carries **the server's own reason**, verbatim, and is
489
+ disabled, with `until <clock>` when there is a reset to name. A rung that is
490
+ open but worth a warning keeps its reason as plain text and stays clickable:
491
+ `past your 10% reserve` is the one you will see, because a human pressing this
492
+ button ignores the reserve and a floor you cannot see is a floor you swear at.
493
+ The refusal that reads `shares the window that is out, buys nothing` is a
494
+ same-login rung under an account-wide wall: every model shares that window, so
495
+ switching model would buy nothing. The rung Leg would take on its own is not in
496
+ the list twice; the terminal's current rung is not in it at all. Picking one
497
+ replaces the row's button grid with a sentence naming the destination and
498
+ whether the conversation survives, and the hand-off carries the model. The
499
+ choice survives the expansion's 3-second refetch: it is held as a rung, so the
500
+ row it names is still selected after the list is rebuilt, or reordered, under
501
+ it, and the poll stands down entirely while the select has focus.
502
+
503
+ **Change the ladder** edits this terminal's own copy. Each rung is a numbered
504
+ row: the agent and model with the login's dot, a `when` select (`always`,
505
+ `below N%` with a number from 0 to 100, which is the range the server itself
506
+ accepts, or `walled only`), the cost word, and **Up**, **Down**, **Remove**.
507
+ Clearing the number box puts the rung back to `always` rather than inventing a
508
+ percentage; a stored `below:0` or `below:100` is shown as the number that is
509
+ stored, with a line saying what it does (`below 0% is never true: this rung is
510
+ never taken`), and is never quietly rewritten. `+ Add a rung` takes an agent and
511
+ one of its models, or `default` for the model the CLI picks itself; a rung the
512
+ ladder already carries is refused in the status line by name and position
513
+ (`claude / opus is already rung 2.`), never silently. There is no drag: a
514
+ list this short is faster with two buttons and a drag has no keyboard. Saving
515
+ posts the whole ladder; a rung the server will not accept comes back as its own
516
+ sentence (`claude has no model "gpt-5"`) in the status line, and nothing is
517
+ saved. The draft line under the list previews what this terminal would try
518
+ next, with the rung it is on skipped.
519
+
520
+ **Settings** carries the same editor for the machine default, under
521
+ `New terminal handoff ladder`, plus the three rules that are machine-wide:
522
+
523
+ - **Let an automatic hand-off spend**, with the sentence `A rung that spends
524
+ usage credits or metered balance may be taken by an automatic hand-off.` Off
525
+ by default. While it is off, a `credits` or `metered` rung is skipped by an
526
+ unattended hand-off and the ledger says which and why; a human pressing Hand
527
+ off can still take it. When the claude login reports credits off, the panel
528
+ adds `Usage credits are off, so there is nothing to spend through the wall.`
529
+ and offers no button to turn them on, because that cannot be done from here.
530
+ - **Climbing back**, two radios: `Leg climbs back to the top rung at the next
531
+ hand-off.` (the default) or `Stay on the lower rung until you press Back to
532
+ fable.` Under them, the rule that is not a choice:
533
+ `Leg never interrupts a running turn to climb.`
534
+ - **Keep N% of `<login>` for your own terminals**, one number per login on the
535
+ ladder. 0 keeps nothing back. An automatic hand-off skips a rung whose login
536
+ is past the floor; a hand-off you press yourself still takes it, and the
537
+ picker prints `past your 10% reserve` on that row rather than hiding it.
538
+
539
+ Saving Settings writes `handoff_ladder`, `climb_back`, `may_spend` and
540
+ `reserve` in one `PATCH /api/settings`, and the status line reports the
541
+ `handoff_order` the ladder derives, which is what older terminals still read.
317
542
 
318
543
  ## The ledger
319
544
 
@@ -353,9 +578,10 @@ heading, a line of detail and a button that opens the detail below the row.
353
578
  or `N uncommitted`. Nothing here is pushed over the live stream; the count
354
579
  re-reads itself once a minute while the drawer is closed. On a shared board
355
580
  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.`
581
+ - **N finished cards**, `3 finished cards, 2 done, 1 failed, last 11:02 PM`,
582
+ with **View** opening them newest first. Live cards are not here: they are
583
+ rows in the Background panel under Terminals, with the one-line entry under
584
+ it. With none finished it reads `No finished cards`.
359
585
 
360
586
  ## A shared board (more than one human)
361
587
 
@@ -378,9 +604,14 @@ link: that side belongs to the owner of the machine, and the API answers 403.
378
604
 
379
605
  ## Background tasks: page layout
380
606
 
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.
607
+ Everything from here down is the card runtime. Each card runs separately from
608
+ the interactive terminal conversations, in its own git worktree. Liveness, not
609
+ kind, decides where a card appears: a live card (`backlog`, `queued`,
610
+ `running`, `handing_off`, `needs_approval`, `waiting_human`, `paused`) is a row
611
+ in the **Background** panel directly under Terminals, in the terminal row's own
612
+ shape; a finished one (`done`, `failed`, `killed`) collapses into the ledger
613
+ cell, `10 finished cards, 5 done, 5 failed, last 8:15 PM`, whose **View** opens
614
+ them newest first. Ten finished cards are one row, not ten.
384
615
 
385
616
  The masthead (`src/board/index.html`) has the Leg wordmark on the left and, on
386
617
  the right, the connection word (`connecting` / `live` / `reconnecting…`) with its
@@ -389,15 +620,42 @@ dot, the scheduler status (`scheduler running, 2 max`), and the **Floor** link.
389
620
  last region of the page, in flow. Nothing on the board is sticky: the verdict is
390
621
  what you came for and it is at the top, so there is nothing to pin.
391
622
 
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`.
623
+ With no live cards the Background panel's rows are gone and only its entry line
624
+ is left; with live cards the region head prints the same counts a terminals
625
+ head does, `1 running, 1 waiting on you`. Under the rows sits the entry: a
626
+ single field, `Run in the background:`, a **Start** button, and an inferred
627
+ sentence whose nouns are buttons, `in leg on main, with claude/fable then
628
+ claude/opus then codex, build only`, plus **More settings** for the full form.
629
+ Start with an empty task is disabled and says why.
630
+
631
+ The sentence describes exactly what Start posts, noun by noun. The branch is
632
+ the repo's own default (the server reads `origin/HEAD`, then `main`, `master`
633
+ and `trunk`, then the branch the checkout is on) and it is sent with the card,
634
+ so a repo whose default is `master` or `develop` starts one instead of being
635
+ refused by a trunk that does not exist. The ladder clause is one leg per rung
636
+ carrying that rung's model: `claude/fable then claude/opus then codex` posts
637
+ three legs, the first two on one login and two different models, which is the
638
+ hand-off the ladder exists for. Two rungs that name the same agent AND the same
639
+ model are one leg, not two, so the sentence never lists a hand-off to a leg's
640
+ own twin. Picking a lower rung in the ladder select starts the chain there.
395
641
 
396
642
  **Settings** holds the **API token** field (only needed when the server is bound
397
643
  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
644
+ **New terminal handoff ladder** editor used by terminals started after you save
645
+ (the rungs, the spend rule, the climb-back policy and the per-login reserve),
646
+ **Notifications**, and the board's own facts: version and bind address, who you are signed in as,
647
+ whether share is on, the scheduler, and the board home. **Notifications** holds
648
+ two toggles, both about a terminal that is waiting on you: *Terminal toast when
649
+ a terminal waits on you* (on by default, Claude Code only: an OSC 9 sequence
650
+ returned by the Notification hook, which is the toast Windows Terminal renders
651
+ in the window the agent is already running in) and *Browser notification when a
652
+ terminal waits on you* (off by default; the browser asks for permission the
653
+ first time you turn it on, and it fires on the transition into waiting, never on
654
+ every refresh). The board toggle reads `window.isSecureContext` when it renders;
655
+ where that is false it is disabled and says `This page is not a secure context.
656
+ Open the board at http://localhost:<port> to turn toasts on.` The tab title
657
+ badge has no toggle: it needs no permission, so it is always on. Its region head
658
+ says
401
659
  `No API token set, requests reach 127.0.0.1:4747 unauthenticated`, or `API token
402
660
  set, sent to 127.0.0.1:4747 as a bearer token` once a token is stored.
403
661
 
@@ -428,12 +686,21 @@ Each card row reads in the same four registers as a terminal panel:
428
686
  - **What**: the title (the card's `title`, or the first 60 characters of the
429
687
  task) as a button that opens the [expansion](#card-expansion), and one
430
688
  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.
689
+ - **Where**: `<repo> on <branch>`, where the branch is the one that checkout is
690
+ actually on: `leg/<card-id>` for a worktree the card cut, and the terminal's
691
+ own branch for one it adopted from "End, and keep going as a card", which is
692
+ not a name anyone can derive from the card id. It is shortened to 32
693
+ characters on the row with the whole line in the tooltip, so what you copy
694
+ into `git checkout` is a branch git has. A card with no checkout yet prints
695
+ the trunk it will branch from. Then the chain rail, and the lease chips, one
696
+ per claimed lease or `**` (the whole repo) when none were set, plus `blocked
697
+ by lease` when the scheduler says so.
698
+ - **When**: the elapsed clock, then the buttons. `mm:ss` (or `hh:mm:ss`) counts
699
+ the leg that is running now, exactly as a terminal row's clock does. A live
700
+ card between runs has no leg to time, so it prints `idle 12m`, the time since
701
+ its last state change, labelled for what it is: the card's own age in that
702
+ column would read `24:05:00` for a card that sat in the backlog for a day. A
703
+ card that has never run prints `--:--`.
437
704
 
438
705
  The log is not on the row. It is in the expansion.
439
706
 
@@ -536,6 +803,33 @@ is fixed: Approve, Run, Resume, Pause, Hand off now, Rerun, Reassign, Kill.
536
803
  | 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
804
  | Rerun | `done`, `failed`, `killed` | start the station over from the last bundle |
538
805
  | Remove | `done`, `failed`, `killed` | delete the card record, its events and its runs; the worktree is kept (confirms first) |
806
+ | Take over | any non-terminal status, in the [expansion](#card-expansion) | you take the card's checkout yourself |
807
+
808
+ **Take over** is the one place the board hands you a command, because a browser
809
+ tab cannot open a terminal: it prints `leg claude --resume-card 3e1c`, which
810
+ starts an interactive terminal in the card's own worktree, primed from the
811
+ card's bundle. Before it prints anything the card is moved to `paused`,
812
+ whatever it was doing: a running one has its child killed and its bundle
813
+ written, and a `queued` or `handing_off` one is taken out of the set the
814
+ scheduler starts from, so nothing launches a leg into the checkout you are
815
+ about to sit down in. The card's ledger gets one `taken_over` line with your
816
+ name on it. **Resume** puts it back to work when you are done. A card that is
817
+ `done`, `failed` or `killed` has nothing to take over and says so.
818
+
819
+ **End, and keep going as a card** is the same trade in the other direction: it
820
+ is the second verb on a terminal's End confirm row (shown for a terminal in a
821
+ git repository, to the owner or an operator). It writes the terminal's hand-off
822
+ bundle, makes a card whose task is the terminal's prompt plus `Continue from
823
+ the bundle at <path>`, starts its ladder at the rung that terminal was on, and
824
+ then ends the terminal. Where the card works depends on what the terminal had:
825
+ a terminal with a worktree of its own hands it over, and the card waits in the
826
+ backlog until that terminal has really stopped, because `End` is a request its
827
+ runner reads on its own poll and two agents in one working tree is the thing
828
+ this whole feature exists to avoid. A terminal working in the repo itself gets
829
+ the card a checkout of its own, cut from the same branch, with the uncommitted
830
+ work carried into it: the tracked edits as a patch and every file git has not
831
+ seen copied across. If that work cannot be read or cannot be carried, nothing
832
+ is created and the terminal is left running with the reason.
539
833
 
540
834
  ## What a state looks like
541
835
 
@@ -549,13 +843,21 @@ is fixed: Approve, Run, Resume, Pause, Hand off now, Rerun, Reassign, Kill.
549
843
  | needs approval | 0 | needs approval | Approve, Kill, Reassign* |
550
844
  | waiting human | 0 | waiting human (or PR open) | Approve, Kill |
551
845
  | paused | 3 | paused | Resume, Kill, Reassign* |
552
- | done | 6 | done | Rerun, Remove |
553
- | failed | 1 | failed | Rerun, Remove |
554
- | killed | 1 | killed | Rerun, Remove |
846
+ | done | not a row | done | none on the board |
847
+ | failed | not a row | failed | none on the board |
848
+ | killed | not a row | killed | none on the board |
555
849
 
556
850
  \* Reassign only shows when the current station is an `agent` station.
557
851
  † Hand off now only shows when that station has a later chain entry.
558
852
 
853
+ The last three are not rows at all. A finished card is one line in the ledger
854
+ drawer (`finishedLine` in `src/board/board.js`), `done after 3 runs, landed
855
+ 7f3a2c1` or `failed at station build after 2 runs: <last event>`, with no
856
+ buttons beside it. `availableActions` in `src/chain.mjs` still returns `rerun`
857
+ for a terminal status, and Remove is its own `DELETE /api/cards/:id`, so both
858
+ verbs remain reachable through the API and `bin/leg.mjs`; the board simply
859
+ stops spending a row on work that has stopped.
860
+
559
861
  ## Floor view
560
862
 
561
863
  `/floor` (`docs/screenshots/floor.png`) is the scheduler-eye view across every
@@ -585,9 +887,42 @@ running or handing-off card.
585
887
  summaries) gets a visible focus ring: `outline: 2px solid var(--focus);
586
888
  outline-offset: 2px` on `:focus-visible` only (`src/board/board.css`). There
587
889
  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.
890
+ - Escape closes the open expansion, the terminal's and the card's. If the
891
+ keyboard map is open, Escape closes that first, then a confirm row if one is
892
+ open. The floor has neither, so Escape does nothing there.
893
+ - **The keyboard map.** `?` opens and closes a small panel listing every
894
+ binding. Every key CLICKS a button that is already on the row, so no key is a
895
+ second way to do anything:
896
+
897
+ | key | what it does |
898
+ |-----|--------------|
899
+ | `j` | move the ring to the next terminal |
900
+ | `k` | move the ring to the previous terminal |
901
+ | `1` to `9` | move the ring to that terminal |
902
+ | `h` | press Hand off now on the terminal the ring is on |
903
+ | `l` | press Land on the terminal the ring is on |
904
+ | `d` | press Details on the terminal the ring is on |
905
+ | `e` | press End on the terminal the ring is on |
906
+ | `?` | open and close the map |
907
+ | `Escape` | close the map, cancel a confirm row, or close an expansion |
908
+
909
+ The ring is a bar down the left edge of the row, and moving it also moves
910
+ keyboard focus to that row's first button, which is its prompt, so a screen
911
+ reader announces which terminal the ring landed on rather than a bare `Land`.
912
+ A disabled button is never clicked, so `l` does nothing on a row that cannot
913
+ land. While an input, a select or a textarea has focus, every one of these
914
+ keys is text and nothing else.
915
+
916
+ The ring belongs to a TERMINAL, not to a position: the grid re-sorts whenever
917
+ a row starts waiting on you, and the ring stays on the terminal you put it on.
918
+ If that terminal ends and moves to the ledger, the ring is cleared rather than
919
+ inherited by whatever row took its place.
920
+
921
+ With no ring set, `h`, `l`, `d` and `e` only set the ring on the first row and
922
+ paint it; it takes a second press to act, so no key ever acts on a terminal
923
+ that was not marked on screen first. Every one of these keys stands down while
924
+ a dialog is open, while the keyboard map is open (`?` and Escape still close
925
+ it), and while a confirm row is waiting for its answer.
591
926
  - Buttons carry descriptive `aria-label`s (for example "Pause `<card title>`",
592
927
  "Reassign adapter for `<card title>`") so a screen reader announces which card
593
928
  an action applies to, not just the button label.