@ucsandman/legcli 0.12.0 → 0.13.1

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 CHANGED
@@ -1,5 +1,93 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.1 (2026-09-18)
4
+
5
+ A one-line fix in the terminal and the marketing site brought up to what 0.13.0 does.
6
+
7
+ - **The terminal's opening line names rungs.** `next: claude/opus →
8
+ claude/sonnet → codex → agy`, through the same `rungLabel` every other
9
+ surface uses; it read `next: claude → claude → codex → agy` on 0.12.0.
10
+ - **The site says what is underneath the handoff.** legcli.com is reorganised
11
+ around the product as it is now: the sample transcript shows a Fable wall
12
+ answered by Opus keeping the conversation and a login wall answered by Codex
13
+ from the bundle; new sections for the ladder, background cards (End as a
14
+ card, Run in the background, Take over), `leg history`, the portable
15
+ harness and custom adapters in one compact group, and a **Never spent
16
+ without you** column beside what is never touched. The structured data's
17
+ `softwareVersion` had been stuck at 0.8.0; the OG image gains Grok; the FAQ
18
+ no longer says a shared board has no TLS.
19
+
20
+ ## 0.13.0 (2026-09-18)
21
+
22
+ The first day on 0.12.0 with three real terminals found five things. Usage
23
+ polling moves out of the terminals into the board, so the endpoint stops
24
+ answering 429 and the timeline stops repeating it. Every rung of a ladder can
25
+ now name a model from a catalog Leg reads off the installed CLIs, and the new
26
+ card form is rebuilt around that. The board holds still under a reader with a
27
+ row expanded. The floor is a page you can start work from. And a page whose
28
+ files are newer than the process serving them says so.
29
+
30
+ - **One usage poll per login, in the board.** Every claude terminal used to
31
+ ask Claude's usage endpoint once a minute on its own; three terminals plus
32
+ Claude Code's own polling meant a 429 every other minute, and the terminal's
33
+ timeline logged `claude usage unavailable: usage endpoint 429: {...}` each
34
+ time the answer flipped. The board process now polls each login once per
35
+ `LEG_USAGE_POLL_MS` (60s), backs off to ten minutes on any refusal and snaps
36
+ back on the first good answer, and writes the two windows onto every active
37
+ session of that login. A refusal is recorded once on the usage record
38
+ (`error`, `error_since`, owner only) with one status line, `claude usage
39
+ unavailable since 9:03 AM: usage endpoint 429: rate_limit_error`, and one
40
+ `claude usage is back`; it never erases the measured buckets. Terminals
41
+ poll nothing, so a terminal started before this release keeps its old
42
+ minute-by-minute poll until it is restarted.
43
+ - **A model catalog, read from the CLIs you have.** `GET /api/models` lists
44
+ what each provider can run today: claude's aliases (fable, opus, sonnet,
45
+ haiku); codex's `models_cache.json` entries with `visibility: list` plus the
46
+ default from `config.toml` (gpt-6-astra, gpt-5.6-sol, gpt-5.6-terra,
47
+ gpt-5.6-luna, gpt-5.5 on the machine this shipped from); `agy models` and
48
+ `grok models`, each run at most once an hour behind the answer and cached
49
+ under `<LEG_HOME>/models/`. A rung's model is validated by shape for every
50
+ provider and by membership for claude only, so next month's codex model is
51
+ not refused and a saved ladder is never silently reset to the default.
52
+ - **The new card form, rebuilt.** Task first, then the repository as a picker
53
+ of known repos or a typed path, the branch to cut from, and **Who runs it**:
54
+ one row per rung with provider, model, permissions, ask before start and max
55
+ turns, reorderable, prefilled from your ladder, with **Save as my default
56
+ ladder** writing it back to Settings. Advanced holds the workflow, scripted
57
+ first agent, leases, merge method, test command and title. The dialog posts
58
+ its chain as one object per rung, so `claude/fable` then `claude/opus` is
59
+ finally two legs and not one. Two columns at 900px, one below; it scrolls on
60
+ a short window instead of hiding Create card below the fold.
61
+ - **The one-line entry row is never empty.** It derives its rungs from
62
+ `handoff_order` when a preferences file predates ladders, the ladder noun
63
+ opens both the rung and the model selects, and with only metered providers
64
+ and spending off it says so instead of "no agent is configured".
65
+ - **The board holds still.** With a row expanded, a text selection live, the
66
+ pointer on a row or focus in the list, the needs-you re-sort is computed but
67
+ not drawn until you come out (hover and focus release after 30s on their
68
+ own). The expansion is never detached from the document, so its selection,
69
+ scroll offsets and open confirm row survive every 3-second poll; the
70
+ region's top is anchored against rows above it changing height; the
71
+ timeline appends new lines above you without moving what you read, and a
72
+ status line repeated word for word within a minute folds into one with
73
+ `×N`. `scripts/board-jump-probe.mjs` is the regression harness: it printed a
74
+ 205px drift on 0.12.0 and 0px now.
75
+ - **The floor is a page you start work from.** The four login panels move
76
+ behind the same **Capacity and models** disclosure as on the board, under
77
+ the same one-line strip; the **Run in the background** entry sits under it
78
+ and posts exactly what the board's does; Running, Waiting on you, Queued,
79
+ Backlog and Done today are Background-style rows with counts in their
80
+ headings, a queued row says its position and what it waits for, and j/k and
81
+ Enter work as on the board. `More settings` carries the typed task to the
82
+ board's dialog through `/#new-card=`. The strip and the entry row live once,
83
+ in `src/board/strip.js` and `src/board/entry.js`, loaded by both pages.
84
+ - **A page newer than its process says so.** `/api/health` answers with the
85
+ process version; the page files carry theirs. When they differ the board and
86
+ the floor print `This board process runs leg 0.10.0 and the page files are
87
+ 0.13.0. Restart it to match: leg down && leg up`, which is the state a board
88
+ left running across an `npm i -g` lands in, and the state that hid every
89
+ 0.12.0 feature on the first morning.
90
+
3
91
  ## 0.12.0 (2026-09-17)
4
92
 
5
93
  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.12.0; see
717
+ and remain available. The version in this source tree is 0.13.1; 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
@@ -3,6 +3,21 @@
3
3
  What broke, why, and what fixed it. One entry per failure, newest first. A first
4
4
  occurrence has to be written down or a repeat is never countable.
5
5
 
6
+ ## 2026-09-18: the terminal's opening `next:` line named the same agent twice and no model
7
+
8
+ **Fixed in `src/attach.mjs`: the line maps the chain through `rungLabel`, so it
9
+ reads `next: claude/opus → claude/sonnet → codex → agy`.**
10
+
11
+ The ladder made destinations rungs of (agent, account, model), and every other
12
+ surface (the ledger's `handoff` event, the board's chain rail, `leg ladder`)
13
+ moved to `rungLabel`. The one line the human reads first, printed at session
14
+ start, kept its pre-ladder formatter of `agent[/account]`, so a claude/fable
15
+ terminal announced `next: claude → claude → codex → agy`. Found while writing
16
+ the marketing site's transcript from the source lines rather than from memory:
17
+ a transcript that mirrors the code cannot show a line the code prints wrongly.
18
+ The lesson is the same one as the site rule: a sample transcript is written
19
+ from the `say()` calls, and a `say()` that reads wrong there is a bug here.
20
+
6
21
  ## 2026-09-17: a new field on the session view leaked the owner's reset times to a guest, through the guest's own terminal
7
22
 
8
23
  **Fixed in `src/server.mjs` (`sessionsView` decides `guest` before the map and
@@ -583,6 +598,33 @@ it), and check whether a board was listening on 4747 at the time.
583
598
  cache keyed on a directory's mtime sees files added and removed, never a
584
599
  file rewritten in place.
585
600
 
601
+ ## 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)
602
+
603
+ - **What happened.** Wes installed 0.12.0 and opened the board: no Fable
604
+ bucket anywhere, an empty agent select under "Run in the background", and a
605
+ timeline full of `claude usage unavailable: usage endpoint 429`. The board
606
+ process on 4747 had been started the day before from the repo checkout and
607
+ was still 0.10.0 in memory; it served the 0.12.0 page files straight from
608
+ disk, so the page drew controls the process had no data for (`handoff_ladder`
609
+ and `buckets` were simply absent from its payloads). The 429s were a second,
610
+ real defect: every claude terminal polled the usage endpoint once a minute
611
+ on its own, three terminals plus Claude Code's own polling, and each flip
612
+ from success to failure logged the whole JSON body into that terminal's
613
+ timeline.
614
+ - **Fix.** The board and the floor carry the version their files shipped with
615
+ and print `This board process runs leg 0.10.0 and the page files are
616
+ 0.13.0. Restart it to match: leg down && leg up` when `/api/health`
617
+ disagrees (`test/files-version.test.mjs` pins the constant to
618
+ `package.json`). Usage polling moved into the board, one poller per login
619
+ with backoff, one line on failure and one on recovery
620
+ (`src/usage-poll.mjs`, `test/usage-poller.test.mjs`).
621
+ - **The lesson that generalises.** "Installed" is not "running". A release that
622
+ changes a long-lived process is verified by restarting that process and
623
+ reading its `/api/health` version, not by reading the page. And a reported
624
+ symptom that pattern-matches a known cause still gets driven to its own
625
+ evidence: two of the five symptoms this morning were the stale process, the
626
+ third was a polling design defect that the stale process was hiding.
627
+
586
628
  ## The 0.12.0 redesign review confirmed 51 findings before the fix pass, 23 of them high (2026-09-18)
587
629
 
588
630
  - **What happened.** The eight-step redesign (per-model buckets, the ladder,
@@ -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. Picking a lower rung in the ladder select starts the chain there.
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
- | Repo path | required; an absolute path to a git repository |
773
- | Task | required; the prompt every leg gets |
774
- | First agent | required; real agents appear here and Claude is preferred when installed |
799
+ | Task | required, and takes the caret when the dialog opens; the prompt every leg gets |
800
+ | Repository | a picker of every repo this board has seen, plus `Another path, typed below` |
801
+ | Path | required; an absolute path to a git repository root. The picker fills it; you can type any path over it |
802
+ | Branch to cut from | the repo's own default branch, read from the sessions payload, not a hardcoded `main` |
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
- | First-agent controls (Advanced) | allowed permission mode, approval gate, max turns, and scripted behavior when the test/demo override is selected |
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
- | Trunk branch | default `main` |
782
- | Merge method | `ff` or `pr`; relevant to a workflow with a land station |
783
- | Test command | overrides the land station's auto-detected command |
784
- | Title | optional; defaults to the task's first 80 characters |
809
+ | Merge method (Advanced) | `ff` or `pr`; relevant to a workflow with a land station |
810
+ | Test command (Advanced) | overrides the land station's auto-detected command |
811
+ | Card title (Advanced) | optional; defaults to the task's first 80 characters |
812
+
813
+ **Who runs it** is one row per leg, in the order Leg tries them. The first row is
814
+ the agent that starts; every row under it is a fallback, taken only when the row
815
+ above it cannot continue. Each row carries:
816
+
817
+ | control | notes |
818
+ |---------|-------|
819
+ | provider | the installed adapters; a test/demo adapter is labelled as one |
820
+ | model | that provider's own catalog from [`GET /api/models`](cli-contracts.md#get-apimodels), with the provider's default as the first option. `codex` lists `gpt-6-astra`, `gpt-5.6-luna` and the rest of its visible models; `claude` lists Fable, Opus, Sonnet and Haiku |
821
+ | permissions | the adapter's own allowed modes, its default preselected |
822
+ | ask before start | gates that leg behind an approval |
823
+ | max turns | a cap for that leg alone |
824
+ | test behavior | scripted outcome, on a test/demo row only |
825
+ | Up / Down / Remove | reorder or drop the row; the last remaining row cannot be removed |
826
+
827
+ **Add a fallback** appends a row for the first installed agent no row already
828
+ names. Every field is per row, so `claude/fable` then `claude/opus` are two real
829
+ legs with their own models, permissions and limits: the older form keyed those
830
+ by adapter and could not tell one claude row from another.
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. It carries the same instrument head as the board, then polls `/api/floor`
865
- and `/api/trunk?since=1h` every 2 seconds and refreshes on the same server-sent
866
- events as the board. Its top bar has the Leg floor brand, the same connection
867
- word and 24 px rule, a repos list, the scheduler status, running/queued/waiting/
868
- done counts, then a spacer and the **Board** link. Five tables:
869
-
870
- | section | columns |
871
- |---------|---------|
872
- | Running | Card, Station, Agent / leg, Leases, Last event, Elapsed, Actions |
873
- | Waiting on humans | Card, Station, Status, Since, Actions |
874
- | Queued | Card, Station, Leases, Blocked by |
875
- | Leases | Lease, Card, Station, Since |
876
- | Trunk lane | Time, Card, Summary |
877
-
878
- Each region head names its own count, `0 running`, `0 landed in the last hour`,
879
- and an empty table is replaced by a sentence saying what would put a row there.
880
- The Trunk lane lists commits landed in the last hour, newest first. The Leases
881
- table is `src/leases.mjs` `held()`: one row per lease currently claimed by a
882
- running or handing-off card.
910
+ card, and it starts work as well as watching it. It polls `/api/floor` (the
911
+ scheduler's own view: leases, blockers, the repos it knows), `/api/cards` (the
912
+ rows), `/api/trunk?since=1h` and `/api/sessions` every 2 seconds, and refreshes
913
+ on the same server-sent events as the board.
914
+
915
+ Top to bottom:
916
+
917
+ 1. **The masthead.** The Leg floor brand, the connection word and its 24 px
918
+ rule, the scheduler status, the repos list and the **Board** link. The four
919
+ counts that used to sit here are beside the station headings now, with the
920
+ rows they count.
921
+ 2. **The capacity strip**, the same one the board draws and from the same file
922
+ (`src/board/strip.js`): one token per login carrying the bucket that will
923
+ actually stop the work. The four login panels are behind the same
924
+ `Capacity and models >` disclosure they are behind on the board, and whether
925
+ it is open is remembered for both pages. On the floor they used to be the
926
+ whole first screen, above every row the page exists to show.
927
+ 3. **The entry row**, the same one the board carries and from the same file
928
+ (`src/board/entry.js`): `Run in the background: [task] [Start]`, with the
929
+ sentence under it naming the repo, the branch, the ladder and the workflow,
930
+ each a button that opens a select in place. Start posts exactly what the
931
+ board's Start posts (`POST /api/cards`), the new card appears in Queued or
932
+ Backlog within one poll with no reload, and a toast names it. **More
933
+ settings** is the one thing the floor hands over: the New card dialog's
934
+ markup exists once, on the board, so the link carries the sentence you typed
935
+ in the hash (`/#new-card=<task>`) and the board opens the dialog with it
936
+ already in the field.
937
+ 4. **Five stations**, each a question with its count in the heading and, under
938
+ it, either the rows or one sentence saying what would put a row there:
939
+
940
+ | station | what is in it |
941
+ |---------|----------------|
942
+ | Running | `running`, `handing_off` |
943
+ | Waiting on you | `needs_approval`, `waiting_human`, `paused` |
944
+ | Queued | `queued`, each row with its position and what it waits for |
945
+ | Backlog | `backlog` |
946
+ | Done today | `done`, `failed`, `killed`, today only, collapsed behind View |
947
+
948
+ A row is the board's own card row (`.row`: state, station, repo on branch,
949
+ agent/model; the title; the one sentence; the run clock or `idle 12m`; the
950
+ short id; and at most four of its available actions in the fixed order).
951
+ The title is a link to that card on the board (`/#card=<id>`), which is
952
+ where the expansion, the log tail and Take over live: one copy of each.
953
+ **Reassign** opens the board's own picker in place of the row's buttons, an
954
+ adapter and a mode with Apply and Cancel, because the API refuses a reassign
955
+ with no adapter. Every other button posts its action straight away.
956
+ A queued row adds `2 of 3 in the queue` and, when the scheduler has said
957
+ why, its own words for what holds it; with the scheduler stopped it says
958
+ `the scheduler is stopped` rather than blaming a slot that nothing is going
959
+ to free. A row you have focus in is not rebuilt under your finger for 30
960
+ seconds, so a press, a picker or a selection survives the 2-second poll;
961
+ every other row in that station keeps updating meanwhile.
962
+ 5. **Leases** and **Trunk lane**, the two tables the floor alone has:
963
+
964
+ | section | columns |
965
+ |---------|---------|
966
+ | Leases | Lease, Card, Station, Since |
967
+ | Trunk lane | Time, Card, Summary |
968
+
969
+ The Trunk lane lists commits landed in the last hour, newest first. The
970
+ Leases table is `src/leases.mjs` `held()`: one row per lease currently
971
+ claimed by a running or handing-off card. `/api/trunk` is the map of the
972
+ machine and stays with the owner (`src/share.mjs` `mayUseMachine`), so an
973
+ operator sees this one table replaced by a line naming whose it is; the
974
+ stations, the entry row and the leases are theirs as usual.
975
+
976
+ `j` and `k` move a ring down and up the rows, and Enter opens the ringed card on
977
+ the board. Enter belongs to whatever has focus: on a button, a link or the
978
+ disclosure it presses that control, and it only opens the ringed card when
979
+ focus is on the page itself. At 400 px the page is one column: the row's
980
+ actions move under its sentence and nothing scrolls sideways.
883
981
 
884
982
  ## Keyboard and accessibility
885
983
 
@@ -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
@@ -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 an active attach polls its usage source; Claude uses its usage endpoint and Codex uses read-only app-server rate limits | `src/attach.mjs` |
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 one of that agent's names in `src/buckets.mjs` `MODEL_ALIASES` (only claude has any: `fable`, `opus`, `sonnet`, `haiku`). `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
+ | `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).
package/docs/faq.md CHANGED
@@ -135,7 +135,9 @@ paths, no bundle, no events, no logs, and none of the pipeline side. The
135
135
  limit line keeps only the reason and the reset time, never the raw limit
136
136
  text. The one thing they can do on your terminal is ask for a hand-off, which
137
137
  you approve or dismiss on the row. `leg share off` ends it and every link
138
- stops working. There is no TLS, so keep it on Tailscale or a network you trust.
138
+ stops working. TLS is served from a certificate pair you supply (`leg share on
139
+ --tls-cert <file> --tls-key <file>`); Leg issues none, so without one keep it on
140
+ Tailscale or a network you trust.
139
141
 
140
142
  **What if the CLI I want isn't installed?**
141
143
  `node bin/leg.mjs up --dry` prints a preflight table with `ok`/`missing`
Binary file
@@ -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": 830,
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.12.0",
3
+ "version": "0.13.1",
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",