@ucsandman/legcli 0.10.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 +212 -0
- package/README.md +158 -67
- package/bin/leg.mjs +168 -18
- package/docs/DECISIONS.md +10 -0
- package/docs/DEMO.md +20 -14
- package/docs/DEVIATIONS.md +1 -0
- package/docs/ERRORS.md +94 -0
- package/docs/ROADMAP-v2.md +69 -11
- package/docs/VOCABULARY.md +27 -0
- package/docs/adapters.md +93 -11
- package/docs/board-guide.md +401 -66
- package/docs/cli-contracts.md +235 -22
- package/docs/concepts.md +167 -19
- package/docs/configuration.md +113 -5
- 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/limits/grok/grok-balance-exhausted.json +11 -0
- package/fixtures/live/claude/resume-model-probe.json +20 -0
- package/fixtures/live/claude/usage-oauth.json +87 -0
- package/fixtures/live/grok/cmd.txt +1 -1
- package/fixtures/live/grok/parsed.json +6 -3
- package/fixtures/live/grok/run.json +22 -10
- package/fixtures/verified.json +8 -1
- package/package.json +3 -2
- package/scripts/build-docs-site.mjs +4 -4
- package/scripts/probe.mjs +2 -1
- package/scripts/seed-fake-cards.mjs +59 -6
- package/scripts/seed-wes-board.mjs +81 -12
- package/src/accounts.mjs +6 -1
- package/src/adapters/cli.mjs +130 -0
- package/src/adapters/custom.mjs +271 -0
- package/src/adapters/grok.mjs +51 -10
- package/src/adapters/index.mjs +34 -7
- package/src/attach.mjs +350 -42
- package/src/audit.mjs +118 -0
- package/src/board/audit.js +123 -0
- package/src/board/board.css +134 -9
- package/src/board/board.js +482 -106
- package/src/board/index.html +89 -7
- package/src/board/sessions.js +1371 -113
- 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 +615 -50
- package/src/sessions.mjs +17 -1
- package/src/share.mjs +66 -6
- package/src/taps/claude-usage.mjs +91 -2
- package/src/taps/claude.mjs +144 -5
- package/src/taps/codex.mjs +23 -3
- package/src/taps/grok.mjs +4 -0
- package/src/usage.mjs +424 -13
package/docs/configuration.md
CHANGED
|
@@ -65,6 +65,64 @@ To opt out and keep standard approval prompts:
|
|
|
65
65
|
2. Environment variable: set `LEG_AUTO_APPROVE=0` or `LEG_NO_AUTO_APPROVE=1` (or legacy `BATON_AUTO_APPROVE=0` / `BATON_NO_AUTO_APPROVE=1`).
|
|
66
66
|
3. Persistent preference: set `"auto_approve": false` in `~/.leg/preferences.json`.
|
|
67
67
|
|
|
68
|
+
### The hand-off ladder
|
|
69
|
+
|
|
70
|
+
`~/.leg/preferences.json` also holds the ladder a terminal falls down when its
|
|
71
|
+
login stops (`src/preferences.mjs`), alongside the older `handoff_order`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"handoff_order": ["claude", "codex", "agy"],
|
|
76
|
+
"handoff_ladder": [
|
|
77
|
+
{ "agent": "claude", "account": "default", "model": "fable", "when": "always", "cost": "plan" },
|
|
78
|
+
{ "agent": "claude", "account": "default", "model": "opus", "when": "always", "cost": "plan" },
|
|
79
|
+
{ "agent": "claude", "account": "default", "model": "sonnet", "when": "always", "cost": "plan" },
|
|
80
|
+
{ "agent": "codex", "account": "default", "model": null, "when": "always", "cost": "plan" },
|
|
81
|
+
{ "agent": "agy", "account": "default", "model": null, "when": "always", "cost": "free" }
|
|
82
|
+
],
|
|
83
|
+
"climb_back": "next-handoff",
|
|
84
|
+
"may_spend": false,
|
|
85
|
+
"reserve": {}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| key | default | meaning |
|
|
90
|
+
|-----|---------|---------|
|
|
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
|
+
| `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
|
+
| `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
|
+
| `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 |
|
|
95
|
+
|
|
96
|
+
A fresh install with no `preferences.json` starts with `claude/fable`,
|
|
97
|
+
`claude/opus`, `claude/sonnet`, then every other installed agent from
|
|
98
|
+
`handoff_order` with `model: null`.
|
|
99
|
+
|
|
100
|
+
Leg writes `handoff_ladder` and `handoff_order` together and keeps them in
|
|
101
|
+
step: saving a ladder rewrites `handoff_order` from its distinct agent order,
|
|
102
|
+
and saving a bare `handoff_order` rewrites the ladder as one `model: null`
|
|
103
|
+
rung per agent, so an older install behaves exactly as it did until a rung is
|
|
104
|
+
edited. If the file is hand-edited so the two disagree, `handoff_order` wins
|
|
105
|
+
and the ladder is rebuilt from it, because the order is the shape a hand
|
|
106
|
+
edit is more likely to have meant.
|
|
107
|
+
|
|
108
|
+
`leg ladder` (`leg ladder ls`) prints the ladder with each rung's live state;
|
|
109
|
+
`leg ladder set <n> <agent>[/<account>[/<model>]]`, `leg ladder rm <n>` and
|
|
110
|
+
`leg ladder spend on|off` change it. See [cli-contracts.md](cli-contracts.md).
|
|
111
|
+
|
|
112
|
+
### Notifications
|
|
113
|
+
|
|
114
|
+
`~/.leg/preferences.json` also holds two toggles for a terminal that is
|
|
115
|
+
waiting on you (`src/preferences.mjs` `defaults()`):
|
|
116
|
+
|
|
117
|
+
| key | default | meaning |
|
|
118
|
+
|-----|---------|---------|
|
|
119
|
+
| `notify_terminal` | `true` | "Terminal toast when a terminal waits on you": on Claude Code's own `Notification` hook (`permission_prompt`, `idle_prompt`, `agent_needs_input`), send an OSC 9 toast to the window the agent is already running in |
|
|
120
|
+
| `notify_board` | `false` | "Browser notification when a terminal waits on you": fire a browser `Notification()` the moment a terminal row becomes `needsYou` and the reader is not looking at the board (a waiting card raises the tab badge and the verdict, not the notification); the board reads `window.isSecureContext` at render time and disables the toggle with an explanatory sentence where that is false |
|
|
121
|
+
|
|
122
|
+
Both are set from Settings on the board, or by writing the key directly. The
|
|
123
|
+
tab title badge (`(1) Leg` plus a favicon dot) has no toggle: it needs no
|
|
124
|
+
permission, so it is always on.
|
|
125
|
+
|
|
68
126
|
## Core
|
|
69
127
|
|
|
70
128
|
| variable | default | meaning | read in |
|
|
@@ -126,7 +184,8 @@ resolves to by default.
|
|
|
126
184
|
| `LEG_CLAUDE_BIN` | claude | `src/adapters/claude.mjs` |
|
|
127
185
|
| `LEG_CODEX_BIN` | codex | `src/adapters/codex.mjs` |
|
|
128
186
|
| `LEG_AGY_BIN` | agy | `src/adapters/agy.mjs` |
|
|
129
|
-
| `LEG_GROK_BIN` | grok
|
|
187
|
+
| `LEG_GROK_BIN` | grok | `src/adapters/grok.mjs` |
|
|
188
|
+
| `LEG_<NAME>_BIN` | a custom adapter called `<name>` (dashes become underscores, so `my-agent` reads `LEG_MY_AGENT_BIN`) | `src/adapters/custom.mjs` |
|
|
130
189
|
| `LEG_GH_BIN` | the `pr` land-mode stub | `src/stations/pr.mjs`; unset, `land_mode: pr` returns an error rather than running a real `gh` |
|
|
131
190
|
| `LEG_CHB_BIN` | `context-handoff-bundle` | `src/handoff.mjs`; unset, Leg tries `context-handoff-bundle` on PATH, then `python -m context_handoff_bundle` |
|
|
132
191
|
|
|
@@ -210,9 +269,10 @@ other address without also setting `LEG_TOKEN` makes the server refuse to
|
|
|
210
269
|
start, exit code `3` (`src/auth.mjs` `checkBind`/`BindRefused`). With a
|
|
211
270
|
token set, every `/api/*` request needs an `Authorization: Bearer <token>`
|
|
212
271
|
header; the event stream (`EventSource`, which cannot set headers) accepts
|
|
213
|
-
the same token as a `?token=` query parameter instead.
|
|
214
|
-
|
|
215
|
-
|
|
272
|
+
the same token as a `?token=` query parameter instead. Keep `LEG_BIND` on
|
|
273
|
+
loopback unless you are using `leg share`, which gives each human their own
|
|
274
|
+
token (see [Share](#share-more-than-one-human)) and can serve the board over
|
|
275
|
+
TLS.
|
|
216
276
|
|
|
217
277
|
## Share (more than one human)
|
|
218
278
|
|
|
@@ -224,7 +284,8 @@ switch and the roster; the env variables below only tune the limits.
|
|
|
224
284
|
| `on` | share is on (it also needs a `bind` and at least one person) |
|
|
225
285
|
| `bind`, `bind_kind`, `port` | where the board listens: the Tailscale address by default, `lan`, or one you named |
|
|
226
286
|
| `owner` | the name a terminal belongs to when nothing else says (`LEG_PERSON`), and the name a loopback browser is treated as |
|
|
227
|
-
| `people[]` | `{ name, role: owner\|guest, token_sha256, created_at }`; the token itself is printed once and never stored |
|
|
287
|
+
| `people[]` | `{ name, role: owner\|operator\|guest, token_sha256, created_at }`; the token itself is printed once and never stored |
|
|
288
|
+
| `tls` | `{ cert, key }`, the paths to a certificate pair; absent means plain http |
|
|
228
289
|
| `loopback_owner` | default `true`: a browser on this machine is the owner without a token. Set it to `false` to ask for a link even here |
|
|
229
290
|
|
|
230
291
|
| variable | default | meaning | read in |
|
|
@@ -234,6 +295,53 @@ switch and the roster; the env variables below only tune the limits.
|
|
|
234
295
|
|
|
235
296
|
`LEG_TOKEN` is the single-token mode and is ignored while share is on.
|
|
236
297
|
|
|
298
|
+
### Roles
|
|
299
|
+
|
|
300
|
+
| role | terminals | cards | this machine |
|
|
301
|
+
|------|-----------|-------|--------------|
|
|
302
|
+
| `owner` | every one | every one | settings, harness policy, the trunk's repo paths, the history index, the worktree map, the audit trail |
|
|
303
|
+
| `operator` | their own; someone else's is read-only and redacted | adds, runs, approves, reassigns, kills | nothing: `/api/settings`, `/api/history`, `/api/worktrees`, `/api/trunk` and `/api/audit` all answer 403, and `/api/health` omits the home path |
|
|
304
|
+
| `guest` | their own; someone else's is read-only and redacted, with **Request handoff** as the only button | nothing: 403 | nothing |
|
|
305
|
+
|
|
306
|
+
A guest and an operator both see their own terminal's hand-off destinations,
|
|
307
|
+
so they can use the picker on it — but never the reset times behind them,
|
|
308
|
+
which are this machine's usage data. `src/share.mjs` `mayUseCards` and
|
|
309
|
+
`mayUseMachine` are the only place a role is turned into permission.
|
|
310
|
+
|
|
311
|
+
### TLS
|
|
312
|
+
|
|
313
|
+
Off unless a certificate pair is configured. Leg never issues one: a
|
|
314
|
+
self-signed pair teaches everyone on the board to click through a warning,
|
|
315
|
+
which is worse than plaintext on a network that is already private. On
|
|
316
|
+
Tailscale, `tailscale cert <machine>.<tailnet>.ts.net` issues a pair browsers
|
|
317
|
+
already trust.
|
|
318
|
+
|
|
319
|
+
| variable | meaning | read in |
|
|
320
|
+
|----------|---------|---------|
|
|
321
|
+
| `LEG_TLS_CERT` | path to the certificate; wins over `share.json`'s `tls.cert` | `src/share.mjs` `readTls` |
|
|
322
|
+
| `LEG_TLS_KEY` | path to the private key; wins over `share.json`'s `tls.key` | `src/share.mjs` `readTls` |
|
|
323
|
+
|
|
324
|
+
`leg share on --tls-cert <file> --tls-key <file>` writes the pair into
|
|
325
|
+
`share.json` and reads it once, so a bad pair fails there rather than at the
|
|
326
|
+
next board start. With a pair configured the shared address serves https and
|
|
327
|
+
every link `leg share` prints says `https://`. The companion listener on
|
|
328
|
+
`127.0.0.1` — the one that lets this machine's own browser in without a token
|
|
329
|
+
— stays plain http, because the certificate is for the shared name and
|
|
330
|
+
loopback traffic never leaves the machine. Half a pair, a missing file, an
|
|
331
|
+
unreadable file or an empty one stops the board with exit `3` rather than
|
|
332
|
+
falling back to plaintext. A renewed pair is picked up by `leg down && leg up`.
|
|
333
|
+
|
|
334
|
+
### The audit trail
|
|
335
|
+
|
|
336
|
+
`GET /api/audit` (owner only) and Settings → **Audit trail** on the board: one
|
|
337
|
+
list across every terminal and every card, newest first, of the actions a
|
|
338
|
+
person or an agent took — hand-offs, landings, approvals, reassignments,
|
|
339
|
+
kills. It reads the events already on disk and stores nothing new. Query
|
|
340
|
+
parameters: `limit` (default 200, max 1000), `since` (an ISO timestamp), `who`
|
|
341
|
+
(a name), `kind` (`human`, `agent` or `leg`). Every answer carries `scanned`
|
|
342
|
+
— how many terminals, cards and events it read — so an empty trail cannot be
|
|
343
|
+
mistaken for a quiet week.
|
|
344
|
+
|
|
237
345
|
## Card-level options
|
|
238
346
|
|
|
239
347
|
These configure one card, not the whole install (set with `card add` flags
|
package/docs/faq.md
CHANGED
|
@@ -57,9 +57,25 @@ same summary). The short version: owning two paid subscriptions is not named
|
|
|
57
57
|
as prohibited by Anthropic or OpenAI, but rotating to a second account of the
|
|
58
58
|
same vendor because the first is rate-limited sits close to OpenAI's
|
|
59
59
|
"circumvent any rate limits" wording and Anthropic's "circumvent product
|
|
60
|
-
guardrails". Leg's default
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
guardrails". Leg's default fallback ladder switches vendors (claude, codex,
|
|
61
|
+
agy) only after it has already tried claude's own weaker models (fable, opus,
|
|
62
|
+
sonnet), which is plainly fine. Same-vendor rotation only happens after you
|
|
63
|
+
run `leg accounts add`; switching models on one login, claude/fable to
|
|
64
|
+
claude/opus, is not a second account at all and none of this applies to it.
|
|
65
|
+
|
|
66
|
+
**Can I switch models mid-session, or does a hand-off always mean a different CLI?**
|
|
67
|
+
Yes. `handoff_ladder` in `preferences.json` names an agent, account and model
|
|
68
|
+
per rung (`src/preferences.mjs`), and a fresh install tries claude/fable, then
|
|
69
|
+
claude/opus, then claude/sonnet before it ever leaves the claude login, then
|
|
70
|
+
the other agents. A downshift to a weaker claude model with a known session id
|
|
71
|
+
runs `claude --resume <id> --model <alias>` and keeps the conversation; every
|
|
72
|
+
other rung takes the bundle as today. `may_spend` (default `false`) keeps an
|
|
73
|
+
automatic hand-off off any rung whose live cost is `credits` or `metered`; a
|
|
74
|
+
hand-off you press yourself can still take it. `climb_back` decides whether
|
|
75
|
+
Leg returns to the top rung at the next hand-off (`next-handoff`, the default)
|
|
76
|
+
or waits for you to press **Back to fable** (`never`). Edit the ladder with
|
|
77
|
+
`leg ladder` or in Settings; see
|
|
78
|
+
[configuration.md](configuration.md#the-hand-off-ladder).
|
|
63
79
|
|
|
64
80
|
**What does `leg uninstall` remove?**
|
|
65
81
|
`~/.leg` and nothing else: sessions, usage files, the extra account
|
|
@@ -74,7 +90,7 @@ package gone too.
|
|
|
74
90
|
Yes. A parent Claude Code session exports `CLAUDECODE` and `CLAUDE_CODE_*`
|
|
75
91
|
markers that make a nested Claude refuse to start; `sanitizeEnv`
|
|
76
92
|
(`src/env.mjs`) strips them along with the API-key variables, so the child
|
|
77
|
-
starts normally. It becomes its own session with its own
|
|
93
|
+
starts normally. It becomes its own session with its own row on the board,
|
|
78
94
|
unrelated to the parent's.
|
|
79
95
|
|
|
80
96
|
**Why are `--dangerously-skip-permissions` and similar flags never
|
|
@@ -118,7 +134,7 @@ and nothing a terminal has said, read or written: no prompt, no file names, no
|
|
|
118
134
|
paths, no bundle, no events, no logs, and none of the pipeline side. The
|
|
119
135
|
limit line keeps only the reason and the reset time, never the raw limit
|
|
120
136
|
text. The one thing they can do on your terminal is ask for a hand-off, which
|
|
121
|
-
you approve or dismiss on the
|
|
137
|
+
you approve or dismiss on the row. `leg share off` ends it and every link
|
|
122
138
|
stops working. There is no TLS, so keep it on Tailscale or a network you trust.
|
|
123
139
|
|
|
124
140
|
**What if the CLI I want isn't installed?**
|
package/docs/getting-started.md
CHANGED
|
@@ -62,10 +62,12 @@ Claude print session. The subscription login is always what runs. Nothing in
|
|
|
62
62
|
|
|
63
63
|
## 4. What you get while it runs
|
|
64
64
|
|
|
65
|
-
- A
|
|
66
|
-
prompt, turns, the files being touched,
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
- A row on the board for this terminal: status, repo@branch, dirty/ahead
|
|
66
|
+
counts, agent/model, the first prompt, turns, the files being touched, and
|
|
67
|
+
the capacity phrase for whichever bucket actually binds (a percentage, or a
|
|
68
|
+
forecast like `about 2h 40m of fable left` once there are enough readings).
|
|
69
|
+
- A warning at 85 % of either window (`LEG_WARN_PCT`): the row goes amber, an
|
|
70
|
+
event, one terminal bell.
|
|
69
71
|
- For Codex, the board and active attach poll the read-only app-server
|
|
70
72
|
`account/rateLimits/read` response every 60 seconds. Leg maps the returned
|
|
71
73
|
300- and 10080-minute durations to 5h and 7d, shows `<n>% used`, and labels
|
|
@@ -77,13 +79,15 @@ Claude print session. The subscription login is always what runs. Nothing in
|
|
|
77
79
|
section before the raw bundle dump.
|
|
78
80
|
- At the limit: the bundle is saved, the agent is stopped, and the next option
|
|
79
81
|
starts in the same terminal from `.leg/RESUME-<session-id>.md`, copied to
|
|
80
|
-
`.leg/RESUME.md`.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
`.leg/RESUME.md`. The order is a ladder of rungs, each an agent, account and
|
|
83
|
+
model: a fresh install goes claude/fable, then claude/opus, then
|
|
84
|
+
claude/sonnet before it ever leaves the claude login, then the remaining
|
|
85
|
+
agents (codex, agy). `leg ladder` (or Settings) edits it. When every option
|
|
86
|
+
is out, Leg prints each reset time, soonest first, waits with a countdown,
|
|
87
|
+
and starts the first one back from the bundle. Ctrl-C quits.
|
|
84
88
|
|
|
85
|
-
Force a hand-off at any time with the **Hand off now** button on the
|
|
86
|
-
`leg sessions handoff <id
|
|
89
|
+
Force a hand-off at any time with the **Hand off now** button on the row, or
|
|
90
|
+
`leg sessions handoff <id> [--to <agent>[/<account>[/<model>]]]`.
|
|
87
91
|
|
|
88
92
|
## Is the resume file still true?
|
|
89
93
|
|
|
@@ -95,7 +99,7 @@ leg resume # the same verdict, then the pointer itself
|
|
|
95
99
|
Leg stamps every resume file with the commit, the working tree and the
|
|
96
100
|
terminals it was written against, and recomputes freshness from git when you
|
|
97
101
|
read it. A commit landing, the tree moving, or the terminal it described going
|
|
98
|
-
away all make it stale; the terminal
|
|
102
|
+
away all make it stale; the terminal's expansion shows the same verdict under
|
|
99
103
|
"What happens next". A session ending rewrites `RESUME.md` to say nothing is in
|
|
100
104
|
flight, and the board does the same at start for a terminal that crashed, so
|
|
101
105
|
nothing is left describing work that has moved on.
|