@ucsandman/legcli 0.12.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 +71 -0
- package/README.md +1 -1
- package/docs/DECISIONS.md +8 -0
- package/docs/ERRORS.md +27 -0
- package/docs/board-guide.md +129 -31
- package/docs/cli-contracts.md +42 -0
- package/docs/configuration.md +8 -2
- package/docs/screenshots/board-details-open.png +0 -0
- package/docs/screenshots/floor.png +0 -0
- package/docs/screenshots/new-card-dialog.png +0 -0
- package/fixtures/verified.json +1 -1
- package/package.json +1 -1
- package/scripts/board-jump-probe.mjs +335 -0
- package/src/attach.mjs +59 -56
- package/src/board/board.css +84 -2
- package/src/board/board.js +349 -261
- 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 +55 -38
- package/src/board/sessions.js +342 -144
- package/src/board/strip.js +163 -0
- package/src/models.mjs +265 -0
- package/src/preferences.mjs +69 -5
- package/src/server.mjs +81 -33
- package/src/taps/claude-usage.mjs +16 -1
- package/src/usage-poll.mjs +260 -0
- package/src/usage.mjs +33 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 (2026-09-18)
|
|
4
|
+
|
|
5
|
+
The first day on 0.12.0 with three real terminals found five things. Usage
|
|
6
|
+
polling moves out of the terminals into the board, so the endpoint stops
|
|
7
|
+
answering 429 and the timeline stops repeating it. Every rung of a ladder can
|
|
8
|
+
now name a model from a catalog Leg reads off the installed CLIs, and the new
|
|
9
|
+
card form is rebuilt around that. The board holds still under a reader with a
|
|
10
|
+
row expanded. The floor is a page you can start work from. And a page whose
|
|
11
|
+
files are newer than the process serving them says so.
|
|
12
|
+
|
|
13
|
+
- **One usage poll per login, in the board.** Every claude terminal used to
|
|
14
|
+
ask Claude's usage endpoint once a minute on its own; three terminals plus
|
|
15
|
+
Claude Code's own polling meant a 429 every other minute, and the terminal's
|
|
16
|
+
timeline logged `claude usage unavailable: usage endpoint 429: {...}` each
|
|
17
|
+
time the answer flipped. The board process now polls each login once per
|
|
18
|
+
`LEG_USAGE_POLL_MS` (60s), backs off to ten minutes on any refusal and snaps
|
|
19
|
+
back on the first good answer, and writes the two windows onto every active
|
|
20
|
+
session of that login. A refusal is recorded once on the usage record
|
|
21
|
+
(`error`, `error_since`, owner only) with one status line, `claude usage
|
|
22
|
+
unavailable since 9:03 AM: usage endpoint 429: rate_limit_error`, and one
|
|
23
|
+
`claude usage is back`; it never erases the measured buckets. Terminals
|
|
24
|
+
poll nothing, so a terminal started before this release keeps its old
|
|
25
|
+
minute-by-minute poll until it is restarted.
|
|
26
|
+
- **A model catalog, read from the CLIs you have.** `GET /api/models` lists
|
|
27
|
+
what each provider can run today: claude's aliases (fable, opus, sonnet,
|
|
28
|
+
haiku); codex's `models_cache.json` entries with `visibility: list` plus the
|
|
29
|
+
default from `config.toml` (gpt-6-astra, gpt-5.6-sol, gpt-5.6-terra,
|
|
30
|
+
gpt-5.6-luna, gpt-5.5 on the machine this shipped from); `agy models` and
|
|
31
|
+
`grok models`, each run at most once an hour behind the answer and cached
|
|
32
|
+
under `<LEG_HOME>/models/`. A rung's model is validated by shape for every
|
|
33
|
+
provider and by membership for claude only, so next month's codex model is
|
|
34
|
+
not refused and a saved ladder is never silently reset to the default.
|
|
35
|
+
- **The new card form, rebuilt.** Task first, then the repository as a picker
|
|
36
|
+
of known repos or a typed path, the branch to cut from, and **Who runs it**:
|
|
37
|
+
one row per rung with provider, model, permissions, ask before start and max
|
|
38
|
+
turns, reorderable, prefilled from your ladder, with **Save as my default
|
|
39
|
+
ladder** writing it back to Settings. Advanced holds the workflow, scripted
|
|
40
|
+
first agent, leases, merge method, test command and title. The dialog posts
|
|
41
|
+
its chain as one object per rung, so `claude/fable` then `claude/opus` is
|
|
42
|
+
finally two legs and not one. Two columns at 900px, one below; it scrolls on
|
|
43
|
+
a short window instead of hiding Create card below the fold.
|
|
44
|
+
- **The one-line entry row is never empty.** It derives its rungs from
|
|
45
|
+
`handoff_order` when a preferences file predates ladders, the ladder noun
|
|
46
|
+
opens both the rung and the model selects, and with only metered providers
|
|
47
|
+
and spending off it says so instead of "no agent is configured".
|
|
48
|
+
- **The board holds still.** With a row expanded, a text selection live, the
|
|
49
|
+
pointer on a row or focus in the list, the needs-you re-sort is computed but
|
|
50
|
+
not drawn until you come out (hover and focus release after 30s on their
|
|
51
|
+
own). The expansion is never detached from the document, so its selection,
|
|
52
|
+
scroll offsets and open confirm row survive every 3-second poll; the
|
|
53
|
+
region's top is anchored against rows above it changing height; the
|
|
54
|
+
timeline appends new lines above you without moving what you read, and a
|
|
55
|
+
status line repeated word for word within a minute folds into one with
|
|
56
|
+
`×N`. `scripts/board-jump-probe.mjs` is the regression harness: it printed a
|
|
57
|
+
205px drift on 0.12.0 and 0px now.
|
|
58
|
+
- **The floor is a page you start work from.** The four login panels move
|
|
59
|
+
behind the same **Capacity and models** disclosure as on the board, under
|
|
60
|
+
the same one-line strip; the **Run in the background** entry sits under it
|
|
61
|
+
and posts exactly what the board's does; Running, Waiting on you, Queued,
|
|
62
|
+
Backlog and Done today are Background-style rows with counts in their
|
|
63
|
+
headings, a queued row says its position and what it waits for, and j/k and
|
|
64
|
+
Enter work as on the board. `More settings` carries the typed task to the
|
|
65
|
+
board's dialog through `/#new-card=`. The strip and the entry row live once,
|
|
66
|
+
in `src/board/strip.js` and `src/board/entry.js`, loaded by both pages.
|
|
67
|
+
- **A page newer than its process says so.** `/api/health` answers with the
|
|
68
|
+
process version; the page files carry theirs. When they differ the board and
|
|
69
|
+
the floor print `This board process runs leg 0.10.0 and the page files are
|
|
70
|
+
0.13.0. Restart it to match: leg down && leg up`, which is the state a board
|
|
71
|
+
left running across an `npm i -g` lands in, and the state that hid every
|
|
72
|
+
0.12.0 feature on the first morning.
|
|
73
|
+
|
|
3
74
|
## 0.12.0 (2026-09-17)
|
|
4
75
|
|
|
5
76
|
The board is rebuilt around the two questions you actually open it to answer:
|
package/README.md
CHANGED
|
@@ -714,7 +714,7 @@ Leg is commercial software under the [Leg License Agreement](LICENSE).
|
|
|
714
714
|
It ships as readable JavaScript so you can see what it does on your machine,
|
|
715
715
|
and you may modify it for your own use, but not redistribute it or work
|
|
716
716
|
around the license check. Versions 0.2.0 and 0.3.0 were published under MIT
|
|
717
|
-
and remain available. The version in this source tree is 0.
|
|
717
|
+
and remain available. The version in this source tree is 0.13.0; see
|
|
718
718
|
[npm](https://www.npmjs.com/package/legcli) for published versions and
|
|
719
719
|
[CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
720
720
|
|
package/docs/DECISIONS.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Durable product and design decisions that the code does not explain on its own. One entry per decision, newest first.
|
|
4
4
|
|
|
5
|
+
## 2026-09-18: usage is polled by the board, once per login; a rung's model is a catalog entry validated by shape
|
|
6
|
+
|
|
7
|
+
- **What.** The board process runs one usage poller per login (`src/usage-poll.mjs`): 60s, doubling to ten minutes on any refusal, one status line when a login's reading fails and one when it is back. Terminals poll nothing and receive the windows on their session record. `GET /api/models` lists what each installed CLI can run, read from claude's aliases, codex's `models_cache.json` and `config.toml`, and the `models` commands of agy and grok, cached an hour under `<LEG_HOME>/models/`. A rung's `model` is validated by shape (lower-case id characters) for every provider and by membership for claude only.
|
|
8
|
+
- **Why the board and not the terminal.** The endpoint is per login and the reading is per login; a terminal is the wrong unit. Three terminals meant three polls a minute plus Claude Code's own, and the endpoint answered 429 every other minute. One poller per login is also the only shape that reads usage when no terminal is open, which is when a reader most wants the strip to be right. The cost is that a terminal started before this release keeps its own poll until it is restarted, which the timeline says.
|
|
9
|
+
- **Why the failure is recorded once, on the record.** A 429 is a fact about the login, not an event in a terminal's life; logging it per poll per terminal made the timeline a wall of one sentence. The record carries `error` and `error_since`; the transition, not the state, is what a session sees.
|
|
10
|
+
- **Why not freeze the catalog into validation.** `normalizeHandoffLadder` and `readPreferences` swallow a validation throw and fall back to the default ladder, so a catalog that refused next month's codex model would silently reset a saved ladder to `claude, codex, agy`. Shape validation refuses what would break a command line (`--model`, a space, an upper-case id that the normaliser would lower-case into a different id) and nothing else; the select is where membership lives, and an id that is not in the catalog is visibly absent from it rather than silently mangled.
|
|
11
|
+
- **Why the page carries its own version.** A board started before `npm i -g` serves the new files from disk with the old process behind them; the first morning on 0.12.0 was spent on features the page drew and the process had no routes for. `/api/health` already said the process version; the page now says its own, and the two disagreeing is an error the reader can act on in one command.
|
|
12
|
+
|
|
5
13
|
## 2026-09-17: a hand-off destination is a rung of (agent, account, model), and usage is a property of a row
|
|
6
14
|
|
|
7
15
|
- **What.** `preferences.json` keeps `handoff_order` and derives it from a new `handoff_ladder` of rungs, each `{agent, account, model, when, cost}`. `chooseNext` walks the ladder from rung 1 every time. `spawnSpec` pushes the rung's model flag, so the interactive path finally carries `--model` the way every headless adapter already did. A claude downshift with a known session id starts `claude --resume <id> --model <alias>` and skips the bundle; every other rung is primed from the bundle. Usage stops being a region on the board: the binding bucket is one strip token at the top and one phrase on each row, and the four login panels move intact behind a **Capacity and models** disclosure. Live cards become rows in a **Background** panel directly under Terminals; finished cards collapse into one ledger line.
|
package/docs/ERRORS.md
CHANGED
|
@@ -583,6 +583,33 @@ it), and check whether a board was listening on 4747 at the time.
|
|
|
583
583
|
cache keyed on a directory's mtime sees files added and removed, never a
|
|
584
584
|
file rewritten in place.
|
|
585
585
|
|
|
586
|
+
## The first morning on 0.12.0: a 0.10.0 process served the 0.12.0 page, and three terminals rate-limited the usage endpoint (2026-09-18)
|
|
587
|
+
|
|
588
|
+
- **What happened.** Wes installed 0.12.0 and opened the board: no Fable
|
|
589
|
+
bucket anywhere, an empty agent select under "Run in the background", and a
|
|
590
|
+
timeline full of `claude usage unavailable: usage endpoint 429`. The board
|
|
591
|
+
process on 4747 had been started the day before from the repo checkout and
|
|
592
|
+
was still 0.10.0 in memory; it served the 0.12.0 page files straight from
|
|
593
|
+
disk, so the page drew controls the process had no data for (`handoff_ladder`
|
|
594
|
+
and `buckets` were simply absent from its payloads). The 429s were a second,
|
|
595
|
+
real defect: every claude terminal polled the usage endpoint once a minute
|
|
596
|
+
on its own, three terminals plus Claude Code's own polling, and each flip
|
|
597
|
+
from success to failure logged the whole JSON body into that terminal's
|
|
598
|
+
timeline.
|
|
599
|
+
- **Fix.** The board and the floor carry the version their files shipped with
|
|
600
|
+
and print `This board process runs leg 0.10.0 and the page files are
|
|
601
|
+
0.13.0. Restart it to match: leg down && leg up` when `/api/health`
|
|
602
|
+
disagrees (`test/files-version.test.mjs` pins the constant to
|
|
603
|
+
`package.json`). Usage polling moved into the board, one poller per login
|
|
604
|
+
with backoff, one line on failure and one on recovery
|
|
605
|
+
(`src/usage-poll.mjs`, `test/usage-poller.test.mjs`).
|
|
606
|
+
- **The lesson that generalises.** "Installed" is not "running". A release that
|
|
607
|
+
changes a long-lived process is verified by restarting that process and
|
|
608
|
+
reading its `/api/health` version, not by reading the page. And a reported
|
|
609
|
+
symptom that pattern-matches a known cause still gets driven to its own
|
|
610
|
+
evidence: two of the five symptoms this morning were the stale process, the
|
|
611
|
+
third was a polling design defect that the stale process was hiding.
|
|
612
|
+
|
|
586
613
|
## The 0.12.0 redesign review confirmed 51 findings before the fix pass, 23 of them high (2026-09-18)
|
|
587
614
|
|
|
588
615
|
- **What happened.** The eight-step redesign (per-model buckets, the ladder,
|
package/docs/board-guide.md
CHANGED
|
@@ -62,12 +62,20 @@ state the shot needs:
|
|
|
62
62
|
node scripts/seed-wes-board.mjs # 4 live terminals, 5 finished, long paths
|
|
63
63
|
node scripts/seed-floor-board.mjs # cards in every floor lane, plus a landing
|
|
64
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
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
Each seeder writes a throwaway `LEG_HOME` under the OS temp dir; serve it on a
|
|
68
69
|
spare port with `LEG_TRUST=never`. **Never use port 4747**, that is the live
|
|
69
70
|
board, with real sessions on it.
|
|
70
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
|
+
|
|
71
79
|
Measure against those seeds and never against a board of healthy terminals:
|
|
72
80
|
styling scoped to live rows and measured on a clean board reported 40px terminal
|
|
73
81
|
rows while the real screen showed 400px.
|
|
@@ -637,7 +645,20 @@ carrying that rung's model: `claude/fable then claude/opus then codex` posts
|
|
|
637
645
|
three legs, the first two on one login and two different models, which is the
|
|
638
646
|
hand-off the ladder exists for. Two rungs that name the same agent AND the same
|
|
639
647
|
model are one leg, not two, so the sentence never lists a hand-off to a leg's
|
|
640
|
-
own twin.
|
|
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.
|
|
641
662
|
|
|
642
663
|
**Settings** holds the **API token** field (only needed when the server is bound
|
|
643
664
|
off loopback; see [configuration.md](configuration.md#network-exposure)), the
|
|
@@ -765,23 +786,48 @@ bottom:
|
|
|
765
786
|
|
|
766
787
|
## New card form
|
|
767
788
|
|
|
768
|
-
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.
|
|
769
796
|
|
|
770
797
|
| field | notes |
|
|
771
798
|
|-------|-------|
|
|
772
|
-
|
|
|
773
|
-
|
|
|
774
|
-
|
|
|
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` |
|
|
775
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 |
|
|
776
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 |
|
|
777
|
-
|
|
|
778
|
-
| Fallback agents (Advanced) | one understandable row per later adapter, tried in displayed order only if the previous agent cannot continue |
|
|
779
|
-
| 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 |
|
|
780
808
|
| File leases (Advanced) | comma-separated path globs to reserve so overlapping cards wait |
|
|
781
|
-
|
|
|
782
|
-
|
|
|
783
|
-
|
|
|
784
|
-
|
|
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.
|
|
785
831
|
|
|
786
832
|
**Cancel** closes without creating a card; **Create card** posts it and closes on
|
|
787
833
|
success (errors show inline above the form).
|
|
@@ -861,25 +907,77 @@ stops spending a row on work that has stopped.
|
|
|
861
907
|
## Floor view
|
|
862
908
|
|
|
863
909
|
`/floor` (`docs/screenshots/floor.png`) is the scheduler-eye view across every
|
|
864
|
-
card
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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.
|
|
883
981
|
|
|
884
982
|
## Keyboard and accessibility
|
|
885
983
|
|
package/docs/cli-contracts.md
CHANGED
|
@@ -602,6 +602,48 @@ Board routes: `PATCH /api/settings` (owner only) also accepts
|
|
|
602
602
|
`POST /api/sessions/:id/handoff` accepts `target.model` alongside
|
|
603
603
|
`target.agent` and `target.account` (source: src/server.mjs).
|
|
604
604
|
|
|
605
|
+
### GET /api/models
|
|
606
|
+
|
|
607
|
+
The models this machine can start each agent on, so a rung and a card can name
|
|
608
|
+
one. Owner and operator only; a guest gets the same 403 `/api/adapters` and
|
|
609
|
+
`/api/presets` give, because picking a model is spending the owner's plan
|
|
610
|
+
(source: src/server.mjs, src/models.mjs).
|
|
611
|
+
|
|
612
|
+
```json
|
|
613
|
+
{
|
|
614
|
+
"models": {
|
|
615
|
+
"claude": [{ "id": "fable", "label": "Claude Fable", "default": false }],
|
|
616
|
+
"codex": [{ "id": "gpt-6-astra", "label": "GPT-6-Astra", "default": true }],
|
|
617
|
+
"agy": [{ "id": "gemini-3.8-flash-high", "label": "Gemini 3.8 Flash (High)", "default": false }],
|
|
618
|
+
"grok": [{ "id": "grok-4.6", "label": "grok-4.6", "default": true }]
|
|
619
|
+
},
|
|
620
|
+
"observed_at": "2026-09-18T14:37:01.577Z"
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
Where each list comes from, all observed live on 2026-09-18 and captured under
|
|
625
|
+
`fixtures/models/`:
|
|
626
|
+
|
|
627
|
+
| agent | source | today's ids | default |
|
|
628
|
+
|-------|--------|-------------|---------|
|
|
629
|
+
| claude | `MODEL_ALIASES.claude` in src/buckets.mjs; four aliases Claude Code resolves itself, not service-side ids | `fable`, `opus`, `sonnet`, `haiku` | none: a bare `claude` picks for itself, and naming one would be Leg making a choice nobody made |
|
|
630
|
+
| codex | `<CODEX_HOME>/models_cache.json`, `models[]` where `visibility` is `"list"` (`"hide"` covers `gpt-reserve` and `codex-auto-review`) | `gpt-6-astra`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5` | the top-level `model = "..."` in `<CODEX_HOME>/config.toml`, never a `[profiles.x]` one |
|
|
631
|
+
| agy | `agy models`, one `id<TAB>label` per line after a `Fetching available models...` line | 14, from `gemini-3.8-flash-high` to `gpt-oss-120b-medium`, including `claude-opus-4-6-thinking` | agy publishes none |
|
|
632
|
+
| grok | `grok models`, ` * id (default)` / ` - id` bullets under a `Default model: X` line; it lists them even when it is not logged in | `grok-4.6`, `grok-4.5` | `grok-4.6` |
|
|
633
|
+
|
|
634
|
+
`agy` and `grok` each cost a process, so their answers are cached in
|
|
635
|
+
`<LEG_HOME>/models/<agent>.json` for an hour with a 20s probe timeout. A request
|
|
636
|
+
never waits on one: the cached list is served and the refresh runs behind the
|
|
637
|
+
answer. A probe that fails leaves the cache exactly as it was.
|
|
638
|
+
|
|
639
|
+
An id from any of these lists may go on a ladder rung's `model` and on a card's
|
|
640
|
+
chain entry, and reaches the CLI through `modelFlagFor` in src/buckets.mjs
|
|
641
|
+
(`claude --model`, `agy --model`, `codex -m`, `grok -m`). Rung validation is by
|
|
642
|
+
SHAPE for every agent (`/^[a-z0-9][a-z0-9._:-]{0,63}$/`, so a flag or a path can
|
|
643
|
+
never become an argv element) and by MEMBERSHIP for claude alone, whose list is
|
|
644
|
+
closed; the other three catalogs are live and the CLI itself is the authority on
|
|
645
|
+
its own.
|
|
646
|
+
|
|
605
647
|
### History index
|
|
606
648
|
|
|
607
649
|
`<LEG_HOME>/history/index.json` is the one file `leg history` writes: per
|
package/docs/configuration.md
CHANGED
|
@@ -33,7 +33,8 @@ These apply to `leg claude|codex|agy|grok`.
|
|
|
33
33
|
| `LEG_NO_OPEN` | (unset, opens once) | set to `1` to start the board without opening a browser | `bin/leg.mjs` |
|
|
34
34
|
| `LEG_NO_BOARD` | (unset) | set to `1` to run a session with no board at all (the record under `$LEG_HOME/sessions/` is still kept; the test suite uses this) | `src/attach.mjs` |
|
|
35
35
|
| `LEG_WAIT_TICK_MS` | `1000` | how often the all-out countdown redraws and re-checks Ctrl-C / End while waiting for the first reset | `src/attach.mjs` |
|
|
36
|
-
| `LEG_USAGE_POLL_MS` | `60000` | how often
|
|
36
|
+
| `LEG_USAGE_POLL_MS` | `60000` | how often the board asks each login's usage source (one poller per login, not one per terminal); Claude uses its usage endpoint, Codex read-only app-server rate limits, Grok its billing proxy | `src/usage-poll.mjs` |
|
|
37
|
+
| `LEG_USAGE_POLL_MAX_MS` | `600000` | the longest the board waits between readings for one login: a refusal (429, timeout, no login) doubles the wait up to this, and the first good reading puts it back to `LEG_USAGE_POLL_MS` | `src/usage-poll.mjs` |
|
|
37
38
|
| `LEG_ATTACH_POLL_MS` | `2000` | how often the session loop re-reads the taps; git is re-read every third poll | `src/attach.mjs` |
|
|
38
39
|
| `LEG_CLAUDE_USAGE_URL` | `https://api.anthropic.com/api/oauth/usage` | the usage endpoint, for a test double | `src/taps/claude-usage.mjs` |
|
|
39
40
|
| `LEG_CLAUDE_ARGS`, `LEG_CODEX_ARGS`, `LEG_AGY_ARGS` | (none) | space-separated extra arguments for a leg Leg starts on its own after a hand-off (your own `leg <agent> …` args never apply to the next agent); e.g. `LEG_CODEX_ARGS="-m gpt-5.3-codex-spark"` keeps a test chain on cheap models | `src/attach.mjs` |
|
|
@@ -88,7 +89,7 @@ login stops (`src/preferences.mjs`), alongside the older `handoff_order`:
|
|
|
88
89
|
|
|
89
90
|
| key | default | meaning |
|
|
90
91
|
|-----|---------|---------|
|
|
91
|
-
| `handoff_ladder` | `claude/fable`, `claude/opus`, `claude/sonnet`, then one rung per remaining agent in `handoff_order` with model `null` | the fallback list, rung 1 first; each rung is `{agent, account, model, when, cost}`. `model` is `null` or
|
|
92
|
+
| `handoff_ladder` | `claude/fable`, `claude/opus`, `claude/sonnet`, then one rung per remaining agent in `handoff_order` with model `null` | the fallback list, rung 1 first; each rung is `{agent, account, model, when, cost}`. `model` is `null` or a model id that agent publishes: claude's four aliases are a closed list (`fable`, `opus`, `sonnet`, `haiku`, from `src/buckets.mjs` `MODEL_ALIASES`) and a fifth word is refused by name, while codex, agy and grok take any id from their own live catalog (`src/models.mjs` reads each CLI's, and the board's model picker offers it) as long as it matches the shape gate: lower-case letters, digits, `.`, `_`, `:` and `-`, 64 characters at most. `account` is `default` or a name this machine has for that agent (`leg accounts ls`); anything else is refused with a sentence, because that string becomes the CLI's config dir and the usage record's file name. `when` is `always`, `below:N`, or `walled-only`. `cost` is `free`, `plan`, `credits`, or `metered`, and is a static label; the live cost a rung would spend right now is computed from the agent and the login, never read off this key, so a ladder migrated from an older `handoff_order` still meets the spending gate on `grok` |
|
|
92
93
|
| `climb_back` | `next-handoff` | `next-handoff` picks a recovered higher rung up again at the very next hand-off, with no extra step; `never` keeps a terminal on the rung it downshifted to until a human hands it off there by name |
|
|
93
94
|
| `may_spend` | `false` | while `false`, an automatic hand-off skips any rung whose live cost is `credits` or `metered`, and records why; a human's own pick is not gated by this |
|
|
94
95
|
| `reserve` | `{}` | `{ "<agent>": percent }`; an automatic hand-off will not take a rung on that login once its binding bucket is above `100 - percent`. A human's own pick still reaches it, and the picker names the reserve on that row instead of hiding it |
|
|
@@ -105,6 +106,11 @@ edited. If the file is hand-edited so the two disagree, `handoff_order` wins
|
|
|
105
106
|
and the ladder is rebuilt from it, because the order is the shape a hand
|
|
106
107
|
edit is more likely to have meant.
|
|
107
108
|
|
|
109
|
+
A rung Leg cannot read (a misspelt claude alias, an account this machine no
|
|
110
|
+
longer has) is dropped on its own and the rest of the ladder is kept, and Leg
|
|
111
|
+
never writes that shortened ladder back: the file keeps the rungs you typed
|
|
112
|
+
until you save a ladder yourself, from the board or with `leg ladder set`.
|
|
113
|
+
|
|
108
114
|
`leg ladder` (`leg ladder ls`) prints the ladder with each rung's live state;
|
|
109
115
|
`leg ladder set <n> <agent>[/<account>[/<model>]]`, `leg ladder rm <n>` and
|
|
110
116
|
`leg ladder spend on|off` change it. See [cli-contracts.md](cli-contracts.md).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/fixtures/verified.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"_comment": "The single source of truth for every dated, numbered claim Leg makes in public. scripts/check-claims.mjs asserts that each surface listed here states exactly these values, and --strict additionally compares the pinned CLI versions against what those CLIs currently publish. Re-verify on a real machine, then edit this file and every surface in the same commit.",
|
|
3
3
|
"verifiedOn": "2026-09-11",
|
|
4
4
|
"verifiedOnLong": "11 September 2026",
|
|
5
|
-
"tests":
|
|
5
|
+
"tests": 926,
|
|
6
6
|
"clis": {
|
|
7
7
|
"claude": {
|
|
8
8
|
"label": "Claude Code",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ucsandman/legcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Usage-limit monitor and automatic handoff for Claude Code, Codex, agy and Grok. Type leg claude|codex|agy|grok and get the same interactive agent with a board alongside, auto-approve on by default, usage tracking per agent and account, a live context handoff bundle, and at the limit the next agent continuing in the same terminal. $79 once, 30-day money-back guarantee.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|