@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/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`.
|
package/docs/adapters.md
CHANGED
|
@@ -239,25 +239,107 @@ These are what a pipeline card's chain spawns. Unchanged since 0.2.0.
|
|
|
239
239
|
`emulates` to that CLI's name, so the limit classifier applies that CLI's own
|
|
240
240
|
fixtures to it and a demo chain reads like a real fallback.
|
|
241
241
|
|
|
242
|
-
### grok (
|
|
242
|
+
### grok (headless)
|
|
243
243
|
|
|
244
244
|
- **Binary**: `LEG_GROK_BIN`, else `~/.grok/bin/grok.exe`, else `grok` on
|
|
245
245
|
PATH.
|
|
246
|
-
- **Argv**: `grok -
|
|
247
|
-
<mode>`, plus
|
|
246
|
+
- **Argv**: `grok --prompt-file <run>/prompt.txt --output-format json
|
|
247
|
+
--permission-mode <mode> --cwd <worktree>`, plus `--max-turns <n>`,
|
|
248
|
+
`-m <model>`, `-r <session-id>`. Every flag was read from `grok --help` on
|
|
249
|
+
grok 1.0.34 (`3736acbc8658`) on 2026-09-17. The prompt travels by file
|
|
250
|
+
rather than on argv because a hand-off prompt carries the whole bundle
|
|
251
|
+
summary and Windows caps one command line at about 32k; with no prompt file
|
|
252
|
+
the adapter falls back to `-p "<prompt>"`. `--cwd` is passed explicitly
|
|
253
|
+
rather than relying on the spawn's working directory: grok can run against a
|
|
254
|
+
shared leader process (`~/.grok/leader.sock`), and a leg must edit its own
|
|
255
|
+
worktree, not whatever directory the leader started in.
|
|
248
256
|
- **Stdin**: `ignore`.
|
|
249
257
|
- **Modes**: default `acceptEdits`; allowed `default`, `acceptEdits`,
|
|
250
258
|
`auto`, `dontAsk`, `plan`.
|
|
251
259
|
- **Forbidden flags**: `--always-approve`, `bypassPermissions`,
|
|
252
260
|
`--permission-mode=bypassPermissions`.
|
|
253
|
-
- **
|
|
254
|
-
|
|
255
|
-
`
|
|
256
|
-
`
|
|
257
|
-
`
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
+
- **Result**: grok's headless writer emits the Claude Code result envelope.
|
|
262
|
+
The field names (`"type":"result"`, `subtype`, `is_error`, `session_id`,
|
|
263
|
+
`result`, `num_turns`, `stop_reason`, `total_cost`) were read out of the
|
|
264
|
+
shipped `grok.exe` on 2026-09-17. An error is the other envelope,
|
|
265
|
+
`{"type":"error","message":…}`, observed live the same day.
|
|
266
|
+
- **Status**: registered on 2026-09-17. The probe reached the account and came
|
|
267
|
+
back with a real wall — `API error (status 402 Payment Required): Grok Build
|
|
268
|
+
usage balance exhausted`, exit 1, classified `limit` and handed off — so the
|
|
269
|
+
spawn, the argv, the auth and the wall path are all verified live
|
|
270
|
+
(`fixtures/live/grok/`). The **success** path of a grok leg is still
|
|
271
|
+
unprobed: that needs balance on the account. Until it is, a grok leg whose
|
|
272
|
+
envelope does not parse falls back to the `.leg/DONE` marker and the diff,
|
|
273
|
+
which is what every adapter does when `parseResult` returns null. Re-run
|
|
274
|
+
`node scripts/probe.mjs --adapter grok --repo <toy-repo>` with balance to
|
|
275
|
+
close it.
|
|
276
|
+
|
|
277
|
+
## Custom adapters
|
|
278
|
+
|
|
279
|
+
Any other coding-agent CLI becomes a card adapter through a JSON spec in
|
|
280
|
+
`$LEG_HOME/adapters/<name>.json`, with no code in this package. This is the
|
|
281
|
+
open end of the chain: claude, codex, agy and grok ship with taps and a probe,
|
|
282
|
+
and anything else joins as "argv in, JSON out".
|
|
283
|
+
|
|
284
|
+
A custom adapter runs **cards**: headless, in a worktree, handing off like any
|
|
285
|
+
other leg. It is not an interactive `leg <agent>` terminal, because that needs
|
|
286
|
+
a usage tap and a wall signal, which only the four built-ins expose.
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
leg adapter template --name muse > muse.json # a starter spec
|
|
290
|
+
leg adapter add muse.json # validate and install it
|
|
291
|
+
leg adapter check muse # the exact command a leg would run
|
|
292
|
+
leg card add --repo <path> --task "<t>" --chain muse,claude --queue
|
|
293
|
+
leg adapter list | show muse | rm muse
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The spec:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"name": "muse",
|
|
301
|
+
"bin": "muse",
|
|
302
|
+
"stdin": "ignore",
|
|
303
|
+
"args": ["run", "--json",
|
|
304
|
+
["--dir", "{{cwd}}"],
|
|
305
|
+
["--model", "{{model}}"],
|
|
306
|
+
"--prompt-file", "{{promptFile}}"],
|
|
307
|
+
"modes": { "default": "auto", "allowed": ["auto", "readonly"] },
|
|
308
|
+
"forbiddenFlags": ["--unsafe"],
|
|
309
|
+
"result": { "format": "json", "sessionId": "session_id",
|
|
310
|
+
"message": "result", "stopReason": "stop_reason" }
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- **`bin`** is argv[0]. It is spawned directly, never through a shell, so it
|
|
315
|
+
cannot contain `< > | & ;`. `LEG_<NAME>_BIN` overrides it. A `.mjs`, `.cjs`
|
|
316
|
+
or `.js` path runs under this Node.
|
|
317
|
+
- **`args`** is a list of strings and groups. Placeholders: `{{prompt}}`,
|
|
318
|
+
`{{promptFile}}`, `{{cwd}}`, `{{mode}}`, `{{model}}`, `{{resume}}`,
|
|
319
|
+
`{{maxTurns}}`, `{{runDir}}`. A bare string is always kept; a **group** (a
|
|
320
|
+
nested array) is dropped whole when a placeholder inside it has no value, so
|
|
321
|
+
`["--model", "{{model}}"]` disappears rather than passing a bare `--model`.
|
|
322
|
+
Something has to carry the prompt: `{{prompt}}`, `{{promptFile}}`, or
|
|
323
|
+
`"stdin": "pipe"`.
|
|
324
|
+
- **`modes`** is validated before anything spawns, the same way a built-in's
|
|
325
|
+
is: a chain entry naming a mode outside `allowed` throws `forbidden flag`.
|
|
326
|
+
- **`forbiddenFlags`** is yours to add to. The flags that turn a supervised
|
|
327
|
+
agent into an unsupervised one are refused whatever the spec says, in `args`,
|
|
328
|
+
in `modes` and at argv time (`NEVER_ALLOWED`, `src/adapters/custom.mjs`).
|
|
329
|
+
- **`result.format`** is `json` (the first parseable object in stdout),
|
|
330
|
+
`jsonl` (the last line carrying the message field) or `text` (no parsing).
|
|
331
|
+
`sessionId`, `message` and `stopReason` are dotted paths, so
|
|
332
|
+
`"sessionId": "thread.id"` reads `{"thread":{"id":…}}`. With `text`, or when
|
|
333
|
+
nothing parses, the leg is judged by its `.leg/DONE` marker and its diff,
|
|
334
|
+
which is what happens for any adapter whose `parseResult` returns null.
|
|
335
|
+
|
|
336
|
+
A broken spec is reported, never thrown: `leg adapter list` names the file and
|
|
337
|
+
the reason, and the board, the scheduler and `leg card add` carry on without
|
|
338
|
+
it. A spec may not take a built-in's name. The directory is re-read whenever
|
|
339
|
+
the list is asked for, keyed on its entries, so a spec added while the board is
|
|
340
|
+
up appears in the New card form without a restart.
|
|
341
|
+
|
|
342
|
+
## How to add a built-in adapter
|
|
261
343
|
|
|
262
344
|
An adapter is a plain object (see `src/adapters/common.mjs` for the shared
|
|
263
345
|
helpers, `src/adapters/fake.mjs` for the simplest full example):
|