@ucsandman/legcli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +95 -65
  3. package/bin/leg.mjs +123 -14
  4. package/docs/DECISIONS.md +10 -0
  5. package/docs/DEMO.md +20 -14
  6. package/docs/DEVIATIONS.md +1 -0
  7. package/docs/ERRORS.md +41 -0
  8. package/docs/ROADMAP-v2.md +50 -5
  9. package/docs/VOCABULARY.md +27 -0
  10. package/docs/board-guide.md +401 -66
  11. package/docs/cli-contracts.md +199 -5
  12. package/docs/concepts.md +167 -19
  13. package/docs/configuration.md +58 -0
  14. package/docs/faq.md +21 -5
  15. package/docs/getting-started.md +15 -11
  16. package/docs/redesign-2026-09-17.md +477 -0
  17. package/docs/screenshots/background-1280.png +0 -0
  18. package/docs/screenshots/board-400px.png +0 -0
  19. package/docs/screenshots/board-details-open.png +0 -0
  20. package/docs/screenshots/board-drawer.png +0 -0
  21. package/docs/screenshots/board-handoff.png +0 -0
  22. package/docs/screenshots/board-running.png +0 -0
  23. package/docs/screenshots/capacity-drawer-1280.png +0 -0
  24. package/docs/screenshots/settings-ladder-1280.png +0 -0
  25. package/docs/screenshots/terminals-1280.png +0 -0
  26. package/fixtures/limits/claude/claude-fable-limit.json +11 -0
  27. package/fixtures/limits/claude/claude-model-limit.json +1 -1
  28. package/fixtures/limits/claude/claude-session-limit.json +1 -1
  29. package/fixtures/limits/claude/claude-weekly-limit.json +1 -1
  30. package/fixtures/live/claude/resume-model-probe.json +20 -0
  31. package/fixtures/live/claude/usage-oauth.json +87 -0
  32. package/fixtures/verified.json +1 -1
  33. package/package.json +3 -2
  34. package/scripts/seed-fake-cards.mjs +59 -6
  35. package/scripts/seed-wes-board.mjs +81 -12
  36. package/src/accounts.mjs +6 -1
  37. package/src/attach.mjs +328 -46
  38. package/src/audit.mjs +1 -1
  39. package/src/board/board.css +119 -9
  40. package/src/board/board.js +482 -106
  41. package/src/board/index.html +67 -7
  42. package/src/board/sessions.js +1347 -117
  43. package/src/buckets.mjs +101 -0
  44. package/src/cards.mjs +9 -1
  45. package/src/chain.mjs +13 -0
  46. package/src/hook.mjs +7 -1
  47. package/src/ledger.mjs +10 -2
  48. package/src/orchestrator.mjs +13 -4
  49. package/src/preferences.mjs +214 -5
  50. package/src/scheduler.mjs +24 -1
  51. package/src/server.mjs +546 -47
  52. package/src/sessions.mjs +17 -1
  53. package/src/taps/claude-usage.mjs +91 -2
  54. package/src/taps/claude.mjs +144 -5
  55. package/src/taps/codex.mjs +23 -3
  56. package/src/usage.mjs +407 -12
package/docs/DEMO.md CHANGED
@@ -79,16 +79,19 @@ It prints a preflight table, then
79
79
  address in your browser. The board polls every three seconds, so nothing below
80
80
  needs a reload.
81
81
 
82
- The page is one column, read top to bottom: the instrument head with one row per
83
- login, then **Terminals**, **Landed on main**, **Background tasks**, and
84
- **Settings**. This demo happens entirely in **Background tasks**. On a fresh
85
- board home the head reads `no reading` on every rail, because no agent has
86
- reported usage into this home yet.
82
+ The page is one column, read top to bottom: the verdict, the capacity strip
83
+ (one token per login, with the login panels behind its `Capacity and models`
84
+ disclosure), **Terminals**, **Background** (live cards only, hidden while none
85
+ are running), the ledger (finished terminals, landed commits, conversations
86
+ and finished cards, as four counts), and **Settings**. This demo happens
87
+ entirely in **Background**. On a fresh board home the capacity strip reads
88
+ `no reading` for every login, because no agent has reported usage into this
89
+ home yet.
87
90
 
88
91
  ### 3. Queue the card
89
92
 
90
- Click **New card** in the **Background tasks** head. The dialog opens on **New
91
- background card**.
93
+ Click **New card**, in the ledger's finished-cards cell below Terminals and
94
+ Background. The dialog opens on **New background card**.
92
95
 
93
96
  1. **Repo path**: `C:\baton-demo\toy-demo`.
94
97
  2. **Task**: `Add a file greeting.txt containing 'hello from baton'`.
@@ -112,8 +115,9 @@ The recorded chain is `fake-claude>fake-codex`.
112
115
 
113
116
  ### 4. Leg 1, on fake-claude
114
117
 
115
- The card appears as a row in **Background tasks**. The line beside the region
116
- title reads `1 running`, and the row reads left to right:
118
+ The card appears as a row in **Background**, directly under Terminals. The
119
+ line beside the region title reads `1 running`, and the row reads left to
120
+ right:
117
121
 
118
122
  - `fake-claude`, `build`, `running`
119
123
  - the title, and under it the one sentence
@@ -173,11 +177,13 @@ the bundle Leg wrote. The status word is `running` again, the run is `run=2`,
173
177
  and the chain reads `fake-claude · handed off · fake-codex · running`. This is
174
178
  `demo-4-codex-running.png`.
175
179
 
176
- Twenty seconds later the station finishes. The line beside the region title
177
- reads `1 finished`, the status word is `done`, the sentence is
178
- `done after 2 runs, card done: all 1 station(s) complete`, the chain reads
179
- `fake-claude · handed off · fake-codex · done`, and the buttons are **Rerun**
180
- and **Remove**. This is `demo-5-done.png`.
180
+ Twenty seconds later the station finishes. The card leaves **Background**,
181
+ which goes back to empty and hides itself, and the ledger's finished-cards
182
+ cell reads `1 finished card, 1 done, last <time>`. Clicking **View** there
183
+ opens the one line the drawer keeps for it: `done after 2 runs, card done:
184
+ all 1 station(s) complete`. A finished card carries no buttons; Rerun and
185
+ Remove apply to a live row, and this one no longer is one. This is
186
+ `demo-5-done.png`.
181
187
 
182
188
  The worktree named in **Where** now holds the file the fake agent wrote,
183
189
  `hello-fake.txt`, and `.leg/DONE`.
@@ -180,3 +180,4 @@ tests. Rows for the shape changes a later reader would otherwise wonder about.
180
180
  | 2026-09-14 | src/resume.mjs `refreshPointers()` | the board was to skip any checkout with a live terminal, so a live hand-off is never clobbered | it skips only when the pointer's OWN stamped session is live | the case that started this had a hand-written three-day-old `RESUME.md` and a different terminal live in the same checkout; the first rule would have left it exactly as it was |
181
181
  | 2026-09-14 | src/resume.mjs idle pointer | a checkpoint bundle was described as "the last hand-off", and the per-session file was named unconditionally | `lineage.to` decides hand-off vs checkpoint, and the file is named only when it exists on disk | seen in the live pointer for this repo: it claimed a hand-off that never happened and pointed at a `RESUME-<id>.md` that was never written, which is the same class of lie the module exists to stop |
182
182
  | 2026-09-14 | src/board/sessions.js `renderDrawer()` | the whole panel is rebuilt every 3 s poll so relative timestamps stay honest | still rebuilt, but every scrollable box carries a stable `data-scroll-key` and its offset is carried across | the rebuild reset the task box, each message and the timeline to the top every three seconds, so a long message could not be read at all |
183
+ | 2026-09-17 | src/board/sessions.js `verdictLines()` | spec A.5 lists "a model bucket came back and a terminal is still downshifted" below the two branches that print a standing percentage | the came-back branch is tested before them, right after the model-walled branch | as written it can never fire: "one login carries every live terminal" and "several logins carry work" both return for any login that has a figure at all, so the only state change worth telling a downshifted reader about was unreachable |
package/docs/ERRORS.md CHANGED
@@ -582,3 +582,44 @@ it), and check whether a board was listening on 4747 at the time.
582
582
  in the hand-off as in flight, or the next agent ships without it. And a
583
583
  cache keyed on a directory's mtime sees files added and removed, never a
584
584
  file rewritten in place.
585
+
586
+ ## The 0.12.0 redesign review confirmed 51 findings before the fix pass, 23 of them high (2026-09-18)
587
+
588
+ - **What happened.** The eight-step redesign (per-model buckets, the ladder,
589
+ the capacity strip, cards reborn) was reviewed before commit by six finders
590
+ and three refuters per finding: 174 agents, 51 findings confirmed, 23 high.
591
+ The highs were all in code that had passed its own tests: `binding()`
592
+ short-circuited on the first active bucket instead of the one that stops
593
+ the requested model, `summarize()` parsed the ledger once per card per
594
+ refresh, the take-over route handed out a command for a card with no
595
+ checkout, `cardWorkRoot()` fell back to the main checkout, and the guest
596
+ payload leaked buckets, walls and hand-off reasons through the new fields.
597
+ - **Fix.** Three parallel fix passes, one owner per file group, every finding
598
+ closed with a regression test in the same change; the suite went from 783
599
+ to 830 tests. The lockfile's root `repository` field, lost when Playwright
600
+ was installed, was restored so `scripts/npm-publish-gate.mjs` passes.
601
+ - **The lesson that generalises.** A feature's own tests prove the feature's
602
+ own model of itself. The defects a review finds sit where two new pieces
603
+ meet (a new field and an old redaction list, a new route and an old helper),
604
+ and the review has to run on the uncommitted tree, before the commit exists
605
+ to be pushed by someone else. A `npm i` that regenerates the lockfile is a
606
+ release change and gets the publish gate run in the same turn.
607
+
608
+ ## A seeded board's End-as-card button wrote into a real repo (2026-09-17)
609
+
610
+ - **What happened.** Driving the new "End, and keep going as a card" verb in
611
+ a browser against `scripts/seed-wes-board.mjs`'s board, the click on the
612
+ recruiting-tool row hit `POST /api/sessions/:id/end-as-card`, which did
613
+ exactly its job: it wrote a hand-off bundle under
614
+ `C:\Projects\recruiting-tool\.context-handoffs\` and cut a worktree and a
615
+ `leg/card-...` branch there. The seed's rows named real repositories on this
616
+ machine because long real paths were what the layout had to be measured
617
+ against. Nothing ran in the worktree (the server was stopped within a
618
+ minute); the worktree was deregistered and the branch deleted, and the
619
+ directory and bundle were left for a hand delete.
620
+ - **Fix.** The seed's repo paths now live under `C:\Projects-seed\...`, which
621
+ does not exist, so every git-backed action on a seeded row answers 409
622
+ instead of touching a checkout. The row still prints the same length.
623
+ - **The lesson that generalises.** A seeded board is safe to look at and not
624
+ safe to click: any row that names a path that exists is a live control on
625
+ that path. Seed paths must be realistic in shape and impossible in fact.
@@ -1,6 +1,23 @@
1
1
  # Roadmap v2: the software factory
2
2
 
3
- **Unreleased (2026-09-17) opened the chain and finished the share story.** grok is
3
+ **0.12.0 (2026-09-17) added the model dimension and made cards a first-class
4
+ surface.** A hand-off destination is now a rung of (agent, login, model), not
5
+ just an agent, so a Fable wall moves the terminal to opus on the same
6
+ subscription before it moves to another CLI, and for claude that move keeps
7
+ the conversation (`claude --resume <id> --model <alias>`). The **Hand off now
8
+ to** picker lists those rungs with their models, whether each keeps the
9
+ conversation, and why a greyed one buys nothing; the same list is a ladder
10
+ editor in Settings, in a terminal's expansion and behind `leg ladder`. Live
11
+ cards left the drawer and became rows in a **Background** panel directly under
12
+ Terminals, with a one-line entry and the thirteen-field dialog demoted to
13
+ **More settings**; `End, and keep going as a card` and `Take over` are the two
14
+ doors between a terminal and a card. The board top became a capacity strip
15
+ over a **Capacity and models** drawer, a row says `waiting on you` with the
16
+ question when Claude Code's `Notification` hook fires, and a burn-rate figure
17
+ prints only with its sample count. That closes the model dimension, the
18
+ picker's second half, and the cards item below.
19
+
20
+ **0.11.0 (2026-09-17) opened the chain and finished the share story.** grok is
4
21
  a registered card adapter; any other CLI becomes one from a JSON spec
5
22
  (`leg adapter add`, no code); **Hand off now** can name its destination
6
23
  instead of taking the next in the order; `leg share` grew TLS from a
@@ -41,8 +58,8 @@ event names its actor, a board that reads only the ledger, and an auth/bind seam
41
58
 
42
59
  ## Next
43
60
 
44
- 1. ~~**Multi-human network access**~~ — done. 0.3.0 shipped `leg share`; the
45
- unreleased work of 2026-09-17 shipped the three that were left: TLS (`leg share on --tls-cert/--tls-key`,
61
+ 1. ~~**Multi-human network access**~~ — done. 0.3.0 shipped `leg share`; 0.11.0
62
+ shipped the three that were left: TLS (`leg share on --tls-cert/--tls-key`,
46
63
  or `LEG_TLS_CERT`/`LEG_TLS_KEY`, from a pair you supply — Leg issues none),
47
64
  the `operator` role between owner and guest, and the audit trail
48
65
  (`/api/audit`, Settings → Audit trail).
@@ -62,7 +79,35 @@ event names its actor, a board that reads only the ledger, and an auth/bind seam
62
79
  (`plugins.allow`): the verb table in `src/sync/workboard.mjs` is the only
63
80
  thing to check against `openclaw workboard --help`.
64
81
 
82
+ 6. **Probe `codex resume <id> -m <model>`.** The `resume` subcommand and the
83
+ `-m` flag are each verified from `codex --help`; composing them is not, so
84
+ a codex rung ships primed from the bundle and only the claude rungs claim
85
+ to keep the conversation. One real run on a live codex session settles it.
86
+ The same probe shape applies to naming the terminal tab: whether codex, agy
87
+ and grok leave an OSC 2 title alone once the child starts drawing is
88
+ assumed, not known, which is why the tab title is the browser's and not the
89
+ terminal's.
90
+
65
91
  Also on the list: per-station prompt templates editable from the board, lease
66
92
  suggestions from the diff of the previous leg, and a floor view that shows
67
- lease contention over time. (The "hand off now to \<adapter>" picker landed
68
- 2026-09-17, unreleased: Details → **Hand off now to**, or `leg sessions handoff <id> --to`.)
93
+ lease contention over time. (The "hand off now to \<adapter>" picker landed in
94
+ 0.11.0: Details → **Hand off now to**, or `leg sessions handoff <id> --to`.
95
+ 0.12.0 put models on its rows.)
96
+
97
+ ## Ruled out, with the reason
98
+
99
+ - **Phone or push notifications.** They need a relay, which means a server
100
+ that is not this machine holding a token that can reach you. Leg is
101
+ local-first, so the notice surfaces are the ones the machine already owns:
102
+ the browser tab badge (always on, no permission), a browser toast on the
103
+ board (off by default, gated on a secure context), and a terminal toast
104
+ through Claude Code's `Notification` hook (on by default).
105
+ - **Percentages for agy.** Antigravity CLI publishes no usage figure at all,
106
+ so there is nothing to read. agy's token says `no figure`, and its terminals
107
+ are shown by elapsed time instead. A number here could only be invented.
108
+ - **Dollars for subscription sessions.** No transcript Leg reads carries a
109
+ cost field. codex's `credits.balance` can be printed as a measured fact with
110
+ the word `credits`, and is never summed with an estimate.
111
+ - **Per-terminal attribution of a shared login.** Nothing publishes which
112
+ terminal spent which part of a window. The board says it once, at the
113
+ Terminals head (`4 share the claude login`), rather than guessing per row.
@@ -45,6 +45,7 @@ Source: the `appendEvent`/`updateSession` call sites in `src/attach.mjs`,
45
45
  | `limit` | a usage limit was detected; carries the agent's own wording, `(simulated)` when `leg sessions simulate-limit` produced it |
46
46
  | `handoff_requested` | someone pressed Hand off now, ran `leg sessions handoff`, or (on a shared board) asked for or approved a hand-off; `by` names the human (a dismissed request logs as `status` instead) |
47
47
  | `handoff` | the switch happened: from, to, reason, bundle id |
48
+ | `waiting` | a claude `Notification` said a human is being waited on: the type (`permission_prompt`, `idle_prompt`, `agent_needs_input`) and the question verbatim; the next prompt or the end of the turn clears it |
48
49
  | `all_out` | every option is walled; the resets are printed and the terminal waits for the first one (`ended` with "quit while waiting" if Ctrl-C or End cuts the wait short, exit 3) |
49
50
  | `agent_exit` | the agent process exited, with its code |
50
51
  | `continued` | the session was started by `leg history continue` on a conversation the agent's own store holds; `session.json` carries `continued_from: { id, provider, native_id }` |
@@ -61,6 +62,32 @@ Source: the `appendEvent`/`updateSession` call sites in `src/attach.mjs`,
61
62
  | `bounced` | the landing stopped with a [bounce reason](#bounce-reasons-land-station); the full detail is in `body` |
62
63
  | `land_noop` | Land found nothing on the branch beyond its base |
63
64
 
65
+ ## Routing and capacity words
66
+
67
+ The words the ladder, the usage record and the board top use. Source:
68
+ `src/preferences.mjs` (`RUNG_COSTS`, `WHEN_RE`, `defaultLadder`),
69
+ `src/usage.mjs` (`binding`, `burn`, `emptyRecord`), `src/buckets.mjs`
70
+ (`bucketFromWall`) and `src/board/sessions.js`.
71
+
72
+ | word | meaning |
73
+ |------|---------|
74
+ | rung | one destination on the ladder: `{agent, account, model, when, cost}`. `leg ladder` writes `claude/default/opus`; a rung with `model: null` means "whatever that CLI's default is", never a guess |
75
+ | ladder | the ordered list of rungs a terminal falls down, `preferences.handoff_ladder`. `handoff_order` is its older, narrower form and is derived from it, so both stay true |
76
+ | `when` | when a rung may be taken: `always`, `below:N` (only while that login's binding percent is under N), or `walled-only` (only when every rung above it is walled, never when they are merely slow) |
77
+ | `cost` | what a rung spends: `free`, `plan`, `credits` or `metered`. Computed at the moment of choosing, never trusted from disk, because `credits` depends on whether the login has extra usage enabled right now |
78
+ | `may_spend` | off by default. While it is off, an automatic hand-off skips any `credits` or `metered` rung and says so in the ledger. A hand-off a human presses is not automatic and is not gated |
79
+ | `reserve` | a per-login floor, `{agent: percent}`. An automatic hand-off skips a rung past the floor so a background card cannot eat what you kept for yourself; a human pick still takes it and the picker says `past your N% reserve` |
80
+ | `climb_back` | `next-handoff` (the default: the ladder is walked from rung 1 every time, so a reset model is picked up at the next hand-off) or `never` (stay put until **Back to fable**). Leg never interrupts a running turn to climb |
81
+ | bucket | one measured window in a usage record's `buckets[]`: `kind` (`session`, `weekly_all`, `weekly_scoped`), `model` (set only on `weekly_scoped`), `percent`, `resets_at`, `is_active`. A percentage, always measured, never inferred from wording |
82
+ | binding bucket | the bucket that will stop this login first, what `binding(u, model)` returns. The strip, the verdict and the row all print this one rather than whichever window happened to be stored |
83
+ | wall scope | how far a wall reaches, from `bucketFromWall`. `model` walls one model family and writes `walls[model]`, leaving the login open; `account` walls the whole login through `limited_until`. Wording Leg cannot parse is `account`, and the row prints the evidence it failed on |
84
+ | capacity | the phrase a row prints for its own binding bucket: `63% of the fable week`, or nothing when there is no reading. Computed per request, never persisted |
85
+ | forecast | a time figure from `burn()`, and only ever with its sample count: `about 2h 40m of Fable left, from 9 samples over 4h`. Under three samples spanning ten minutes, or on a flat or falling line, or across a reset, it prints nothing at all |
86
+ | waiting on you | a row's status word when `session.waiting` is set by a claude `Notification` hook. It sorts the row first, counts in the Terminals head and puts `(N) Leg` with a dotted favicon in the browser tab. Only claude publishes this signal; other agents say `quiet Nm` and never claim to be waiting |
87
+ | Background | the panel of live cards directly under Terminals. Liveness decides the surface, not kind: `backlog`, `queued`, `running`, `handing_off`, `needs_approval`, `waiting_human` and `paused` are rows here, and `done`, `failed` and `killed` collapse into one ledger line |
88
+ | end as a card | the second verb on a terminal's End confirm row (`POST /api/sessions/:id/end-as-card`): writes the bundle, hands the terminal's own worktree to a card starting at the terminal's current rung, and records the terminal as its `lineage.from` |
89
+ | take over | the reverse, in a card's expansion (`POST /api/cards/:id/take-over`): pauses the card and prints `leg claude --resume-card <id>`, the one command a browser cannot run for you |
90
+
64
91
  ## Harness states (terminal cards, drawer, `leg harness`)
65
92
 
66
93
  Source: `STATES` in `src/harness/index.mjs`; recorded on `session.harness.state`.