@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.
- package/CHANGELOG.md +142 -0
- package/README.md +95 -65
- package/bin/leg.mjs +123 -14
- package/docs/DECISIONS.md +10 -0
- package/docs/DEMO.md +20 -14
- package/docs/DEVIATIONS.md +1 -0
- package/docs/ERRORS.md +41 -0
- package/docs/ROADMAP-v2.md +50 -5
- package/docs/VOCABULARY.md +27 -0
- package/docs/board-guide.md +401 -66
- package/docs/cli-contracts.md +199 -5
- package/docs/concepts.md +167 -19
- package/docs/configuration.md +58 -0
- package/docs/faq.md +21 -5
- package/docs/getting-started.md +15 -11
- package/docs/redesign-2026-09-17.md +477 -0
- package/docs/screenshots/background-1280.png +0 -0
- package/docs/screenshots/board-400px.png +0 -0
- package/docs/screenshots/board-details-open.png +0 -0
- package/docs/screenshots/board-drawer.png +0 -0
- package/docs/screenshots/board-handoff.png +0 -0
- package/docs/screenshots/board-running.png +0 -0
- package/docs/screenshots/capacity-drawer-1280.png +0 -0
- package/docs/screenshots/settings-ladder-1280.png +0 -0
- package/docs/screenshots/terminals-1280.png +0 -0
- package/fixtures/limits/claude/claude-fable-limit.json +11 -0
- package/fixtures/limits/claude/claude-model-limit.json +1 -1
- package/fixtures/limits/claude/claude-session-limit.json +1 -1
- package/fixtures/limits/claude/claude-weekly-limit.json +1 -1
- package/fixtures/live/claude/resume-model-probe.json +20 -0
- package/fixtures/live/claude/usage-oauth.json +87 -0
- package/fixtures/verified.json +1 -1
- package/package.json +3 -2
- package/scripts/seed-fake-cards.mjs +59 -6
- package/scripts/seed-wes-board.mjs +81 -12
- package/src/accounts.mjs +6 -1
- package/src/attach.mjs +328 -46
- package/src/audit.mjs +1 -1
- package/src/board/board.css +119 -9
- package/src/board/board.js +482 -106
- package/src/board/index.html +67 -7
- package/src/board/sessions.js +1347 -117
- package/src/buckets.mjs +101 -0
- package/src/cards.mjs +9 -1
- package/src/chain.mjs +13 -0
- package/src/hook.mjs +7 -1
- package/src/ledger.mjs +10 -2
- package/src/orchestrator.mjs +13 -4
- package/src/preferences.mjs +214 -5
- package/src/scheduler.mjs +24 -1
- package/src/server.mjs +546 -47
- package/src/sessions.mjs +17 -1
- package/src/taps/claude-usage.mjs +91 -2
- package/src/taps/claude.mjs +144 -5
- package/src/taps/codex.mjs +23 -3
- 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
|
|
83
|
-
login,
|
|
84
|
-
**
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
|
116
|
-
title reads `1 running`, and the row reads left to
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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`.
|
package/docs/DEVIATIONS.md
CHANGED
|
@@ -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.
|
package/docs/ROADMAP-v2.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# Roadmap v2: the software factory
|
|
2
2
|
|
|
3
|
-
**
|
|
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`;
|
|
45
|
-
|
|
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
|
-
|
|
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.
|
package/docs/VOCABULARY.md
CHANGED
|
@@ -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`.
|