@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.
- package/CHANGELOG.md +213 -0
- package/README.md +95 -65
- package/bin/leg.mjs +123 -14
- package/docs/DECISIONS.md +18 -0
- package/docs/DEMO.md +20 -14
- package/docs/DEVIATIONS.md +1 -0
- package/docs/ERRORS.md +68 -0
- package/docs/ROADMAP-v2.md +50 -5
- package/docs/VOCABULARY.md +27 -0
- package/docs/board-guide.md +529 -96
- package/docs/cli-contracts.md +241 -5
- package/docs/concepts.md +167 -19
- package/docs/configuration.md +65 -1
- package/docs/faq.md +21 -5
- package/docs/getting-started.md +15 -11
- package/docs/redesign-2026-09-17.md +477 -0
- package/docs/screenshots/background-1280.png +0 -0
- package/docs/screenshots/board-400px.png +0 -0
- package/docs/screenshots/board-details-open.png +0 -0
- package/docs/screenshots/board-drawer.png +0 -0
- package/docs/screenshots/board-handoff.png +0 -0
- package/docs/screenshots/board-running.png +0 -0
- package/docs/screenshots/capacity-drawer-1280.png +0 -0
- package/docs/screenshots/floor.png +0 -0
- package/docs/screenshots/new-card-dialog.png +0 -0
- package/docs/screenshots/settings-ladder-1280.png +0 -0
- package/docs/screenshots/terminals-1280.png +0 -0
- package/fixtures/limits/claude/claude-fable-limit.json +11 -0
- package/fixtures/limits/claude/claude-model-limit.json +1 -1
- package/fixtures/limits/claude/claude-session-limit.json +1 -1
- package/fixtures/limits/claude/claude-weekly-limit.json +1 -1
- package/fixtures/live/claude/resume-model-probe.json +20 -0
- package/fixtures/live/claude/usage-oauth.json +87 -0
- package/fixtures/verified.json +1 -1
- package/package.json +3 -2
- package/scripts/board-jump-probe.mjs +335 -0
- package/scripts/seed-fake-cards.mjs +59 -6
- package/scripts/seed-wes-board.mjs +81 -12
- package/src/accounts.mjs +6 -1
- package/src/attach.mjs +378 -93
- package/src/audit.mjs +1 -1
- package/src/board/board.css +203 -11
- package/src/board/board.js +664 -200
- package/src/board/entry.js +343 -0
- package/src/board/floor.html +51 -39
- package/src/board/floor.js +585 -73
- package/src/board/index.html +122 -45
- package/src/board/sessions.js +1569 -141
- package/src/board/strip.js +163 -0
- package/src/buckets.mjs +101 -0
- package/src/cards.mjs +9 -1
- package/src/chain.mjs +13 -0
- package/src/hook.mjs +7 -1
- package/src/ledger.mjs +10 -2
- package/src/models.mjs +265 -0
- package/src/orchestrator.mjs +13 -4
- package/src/preferences.mjs +278 -5
- package/src/scheduler.mjs +24 -1
- package/src/server.mjs +625 -78
- package/src/sessions.mjs +17 -1
- package/src/taps/claude-usage.mjs +107 -3
- package/src/taps/claude.mjs +144 -5
- package/src/taps/codex.mjs +23 -3
- package/src/usage-poll.mjs +260 -0
- package/src/usage.mjs +439 -12
package/docs/board-guide.md
CHANGED
|
@@ -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
|
|
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).
|
|
17
|
-
|
|
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
|
|
21
|
-
board-400px.png the same board at 400 px
|
|
22
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
92
|
+
Fable is at 63% of its week, the only login open.
|
|
71
93
|
```
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
84
|
-
|
|
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
|
|
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
|
|
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
|
-
`,
|
|
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
|
|
147
|
-
with its dot, then where the work is (`leg on main`,
|
|
148
|
-
is not a repo), then
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
the
|
|
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
|
-
**
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
the
|
|
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
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
399
|
-
|
|
400
|
-
|
|
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**:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
|
506
|
-
|
|
|
507
|
-
|
|
|
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
|
-
|
|
|
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
|
-
|
|
|
515
|
-
|
|
|
516
|
-
|
|
|
517
|
-
|
|
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 |
|
|
553
|
-
| failed |
|
|
554
|
-
| killed |
|
|
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
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
|
589
|
-
|
|
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.
|