@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/cli-contracts.md
CHANGED
|
@@ -25,7 +25,7 @@ questions."
|
|
|
25
25
|
| claude | 2.1.268 | exit 0, file yes, DONE yes, 20 s | `src/adapters/claude.mjs` |
|
|
26
26
|
| codex | codex-cli 0.153.4 | exit 0, file yes, DONE yes, 28 s | `src/adapters/codex.mjs` |
|
|
27
27
|
| agy | 1.2.0 | attempt 1 exit 0 but wrote to its scratch workspace; attempt 2 (`--add-dir`) exit 0, file yes, DONE yes, 41 s | `src/adapters/agy.mjs` |
|
|
28
|
-
| grok | 0.
|
|
28
|
+
| grok | 1.0.34 | exit 1, no file, no DONE: the account answered `402 Payment Required: Grok Build usage balance exhausted`, classified `limit` (re-probed 2026-09-17; the 2026-09-11 run on 0.2.51 was not logged in) | `src/adapters/grok.mjs` |
|
|
29
29
|
|
|
30
30
|
The auth-source check: the build shell carried `ANTHROPIC_API_KEY` and
|
|
31
31
|
`OPENAI_API_KEY` (injected by the shell profile). Every adapter's `env()` deletes
|
|
@@ -165,29 +165,48 @@ stderr are 0 bytes, and codex's is the one stdin notice. observed-live.
|
|
|
165
165
|
|
|
166
166
|
## grok
|
|
167
167
|
|
|
168
|
-
- Version grok 0.
|
|
168
|
+
- Version grok 1.0.34 (3736acbc8658) [stable] (source: `grok --version`,
|
|
169
|
+
2026-09-17). The 2026-09-11 sweep saw 0.2.51 on a machine with no login; every
|
|
170
|
+
line below was re-read on 1.0.34.
|
|
169
171
|
- Binary: `~/.grok/bin/grok.exe` (native; also an npm shim on PATH);
|
|
170
172
|
`LEG_GROK_BIN` overrides (source: `where grok`).
|
|
171
|
-
- Headless argv (from cmd.txt): `grok.exe -
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
- Headless argv (from cmd.txt): `grok.exe --prompt-file <run>/prompt.txt
|
|
174
|
+
--output-format json --permission-mode acceptEdits --cwd <worktree>`; stdin
|
|
175
|
+
`ignore`. The file form is used whenever the runner has written a prompt file,
|
|
176
|
+
because a hand-off prompt carries the whole bundle summary and Windows caps a
|
|
177
|
+
command line near 32k; with none, the adapter falls back to `-p "<prompt>"`
|
|
178
|
+
(source: `grok --help`, `-p, --single <PROMPT>` and `--prompt-file <PATH>`).
|
|
179
|
+
`--cwd <CWD>` is passed explicitly rather than trusting the spawn's working
|
|
180
|
+
directory, because grok can run against a shared leader process
|
|
181
|
+
(`~/.grok/leader.sock`) and a leg must edit its own worktree.
|
|
182
|
+
- Output: the Claude Code result envelope. `"type":"result"`, `subtype`,
|
|
183
|
+
`is_error`, `session_id`, `result`, `num_turns`, `stop_reason`, `total_cost`,
|
|
184
|
+
read out of the shipped `grok.exe` on 2026-09-17 alongside the streaming
|
|
185
|
+
types (`assistant`, `system`, `text`, `usage`, `end`, `error`). An error is
|
|
186
|
+
the other envelope, `{"type":"error","message":…}` (observed-live,
|
|
187
|
+
`fixtures/live/grok/out.log`).
|
|
175
188
|
- Exit codes:
|
|
176
189
|
|
|
177
190
|
| exit | meaning | source |
|
|
178
191
|
|------|---------|--------|
|
|
179
|
-
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
| 1 | printed `{"type":"error","message":"Internal error: … API error (status 402 Payment Required): Grok Build usage balance exhausted"}` and did no work: the account had no balance left | observed-live 2026-09-17, fixtures/live/grok/out.log |
|
|
193
|
+
| 0 | printed JSON with `stopReason:"Cancelled"` and did no work: the CLI was not logged in, printed a device-code prompt on stderr and gave up after ~58 s | observed-live 2026-09-11 on 0.2.51 |
|
|
194
|
+
|
|
195
|
+
The 402 is a wall, not a failure: `fixtures/limits/grok/grok-balance-exhausted.json`
|
|
196
|
+
classifies it `limit`, so the card hands off instead of stopping. None of the
|
|
197
|
+
rate-limit strings the tap watched for appear in a 402, which is why an
|
|
198
|
+
exhausted grok terminal used to sit there; `src/taps/grok.mjs` reads it too now.
|
|
199
|
+
- Permission modes: `default`, `acceptEdits`, `auto`, `dontAsk`, `plan`, and a
|
|
200
|
+
bypass mode Leg never passes (source: `grok --help`). Leg: default
|
|
201
|
+
`acceptEdits`; forbidden the bypass mode and `--always-approve`.
|
|
186
202
|
- Login: `grok login` (source: `grok --help` Commands).
|
|
187
|
-
- **Verdict:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
- **Verdict: registered, wall path verified, success path not.** The probe
|
|
204
|
+
reached the account through the real runner, so the binary resolution, the
|
|
205
|
+
argv, the auth and the limit classification are all observed-live. A leg that
|
|
206
|
+
completes its task has not been seen, because that needs balance on the
|
|
207
|
+
account; until it is, a grok leg whose envelope does not parse is judged by
|
|
208
|
+
its `.leg/DONE` marker and its diff, as every adapter is. Re-run
|
|
209
|
+
`node scripts/probe.mjs --adapter grok --repo <toy>` with balance to close it.
|
|
191
210
|
|
|
192
211
|
## `leg harness` (the portable harness)
|
|
193
212
|
|
|
@@ -224,26 +243,78 @@ variable and `LEG_SESSION` (source: src/attach.mjs, src/env.mjs).
|
|
|
224
243
|
|
|
225
244
|
### claude tap
|
|
226
245
|
|
|
227
|
-
- Attach: `claude <args> --settings <LEG_HOME>/sessions/<id>/claude-settings.json`
|
|
246
|
+
- Attach: `claude <args> -n "leg#<short id> <repo>/<branch>" --settings <LEG_HOME>/sessions/<id>/claude-settings.json`
|
|
228
247
|
(source: src/attach.mjs `spawnSpec`; src/taps/claude.mjs `writeSettings`).
|
|
229
248
|
Hooks in a `--settings` file merge with the user's rather than replacing
|
|
230
249
|
them; `statusLine` is the one key that replaces, so Leg runs the user's own
|
|
231
250
|
command first (source: code.claude.com/docs/en/settings;
|
|
232
251
|
src/taps/claude.mjs `userStatusLine`). observed-live 2026-09-11: a Leg
|
|
233
252
|
session ran with every user hook still firing.
|
|
253
|
+
- Terminal title: `-n, --name <name>` "Set a display name for this session
|
|
254
|
+
(shown in the prompt box, /resume picker, and terminal title)" —
|
|
255
|
+
fixtures/help/claude.txt line 132. It sits in the general Options block, not
|
|
256
|
+
among the flags marked "only works with --print", and two of its three
|
|
257
|
+
surfaces (the prompt box, the `/resume` picker) exist only in interactive
|
|
258
|
+
mode, so it is the interactive title flag. Leg passes
|
|
259
|
+
`leg#<short id> <repo>/<branch>`; a `-n` or `--name` the human passed is
|
|
260
|
+
never overwritten. codex, agy and grok have no such flag, so Leg writes OSC 2
|
|
261
|
+
(`\x1b]2;leg#7f3a leg/main\x07`) to stdout once before the child spawns, and
|
|
262
|
+
only on a TTY. **ASSUMED**: that the child does not overwrite the title once
|
|
263
|
+
it starts drawing; no terminal has been read either way yet.
|
|
234
264
|
- Hooks written (source: src/taps/claude.mjs `settingsFor`): `SessionStart`,
|
|
235
265
|
`UserPromptSubmit`, `PostToolUse` with matcher `Edit|Write|MultiEdit|NotebookEdit`,
|
|
266
|
+
`Notification` with matcher
|
|
267
|
+
`permission_prompt|idle_prompt|agent_needs_input|quota_auto_resume_fired`,
|
|
236
268
|
`Stop`, `StopFailure`, `SessionEnd`, each
|
|
237
269
|
`node <src>/hook.mjs claude-hook --session <id>` with a 20 s timeout.
|
|
238
|
-
observed-live 2026-09-11 (hook.log in the
|
|
270
|
+
observed-live 2026-09-11 for the six that shipped then (hook.log in the
|
|
271
|
+
session directory); the `Notification` entry is docs-only until a real
|
|
272
|
+
permission prompt is captured.
|
|
273
|
+
- Waiting on a human: the `Notification` hook. It "matches on notification
|
|
274
|
+
type" and, not being one of the two narrower events (FileChanged,
|
|
275
|
+
StopFailure), takes `|` as its alternation separator
|
|
276
|
+
(https://code.claude.com/docs/en/hooks lines 1424, 173, 165). The payload
|
|
277
|
+
carries `message`, an optional `title` and `notification_type` (line 1480).
|
|
278
|
+
`permission_prompt`, `idle_prompt` and `agent_needs_input` write
|
|
279
|
+
`session.waiting = { type, message (verbatim, 160 chars), since }`;
|
|
280
|
+
`UserPromptSubmit` and `Stop` clear it. `permission_prompt` fires only after
|
|
281
|
+
the prompt has waited about six seconds and shares its timing with desktop
|
|
282
|
+
notifications, so in a terminal it arrives only when the human appears to be
|
|
283
|
+
away (lines 1162, 1443): "waiting on you" is late by design and must not be
|
|
284
|
+
sold as instant.
|
|
285
|
+
- The toast: the hook prints `{"terminalSequence":"\x1b]9;<message>\x07"}`.
|
|
286
|
+
Notification hooks cannot block or modify anything and their `systemMessage`
|
|
287
|
+
and `continue` are discarded, but Claude Code still emits `terminalSequence`
|
|
288
|
+
for them (lines 1490, 622). The field is restricted to OSC 0/1/2/9/99/777 and
|
|
289
|
+
BEL, and is ignored wholesale if anything outside that rides in it (line
|
|
290
|
+
608), so control bytes in the message are stripped first. OSC 9 is what
|
|
291
|
+
Windows Terminal, iTerm2, ConEmu and WezTerm render (line 617). Gated on
|
|
292
|
+
`preferences.notify_terminal`, default true.
|
|
239
293
|
- `autoContinueAtUsageLimit: false` in the same file, because Leg owns the
|
|
240
|
-
hand-off (source: src/taps/claude.mjs `settingsFor`).
|
|
294
|
+
hand-off (source: src/taps/claude.mjs `settingsFor`). If the human's own
|
|
295
|
+
settings re-enable it, the `quota_auto_resume_fired` notification arrives
|
|
296
|
+
anyway; Leg then writes `waiting = { type: 'quota_auto_resume' }` and the
|
|
297
|
+
automatic hand-off stands down for that terminal (src/attach.mjs
|
|
298
|
+
`handoffStoodDown`). Two waiters on one terminal is the failure to avoid.
|
|
299
|
+
- The model that answered: each assistant line of the transcript carries
|
|
300
|
+
`message.model` (VERIFIED: 29 lines of the newest jsonl for this repo read
|
|
301
|
+
`claude-fable-5-1`). The runner reads the tail of that file on the usage poll
|
|
302
|
+
and says it as its alias (`fable`), so a silent fallback off Fable shows on
|
|
303
|
+
the row; an id matching no alias is kept raw (src/taps/claude.mjs
|
|
304
|
+
`modelFromTranscript`, `modelAlias`).
|
|
241
305
|
- Usage: `GET https://api.anthropic.com/api/oauth/usage`
|
|
242
306
|
(`LEG_CLAUDE_USAGE_URL` overrides) with `Authorization: Bearer <accessToken>`
|
|
243
307
|
from `<CLAUDE_CONFIG_DIR>/.credentials.json` key `claudeAiOauth`, and header
|
|
244
308
|
`anthropic-beta: oauth-2025-04-20`; response fields `five_hour` and
|
|
245
309
|
`seven_day`, each `{ utilization, resets_at }`; polled every
|
|
246
310
|
`LEG_USAGE_POLL_MS` ms, default 60000 (source: src/taps/claude-usage.mjs).
|
|
311
|
+
A `limits[]` array, when the response carries one, becomes the per-model
|
|
312
|
+
buckets; a row's `scope.model.display_name` is joined to the alias the rest of
|
|
313
|
+
Leg uses by matching a known name as a whole word, so `Fable 5.1` and
|
|
314
|
+
`Claude Opus 5` are `fable` and `opus` and a name Leg does not know keeps its
|
|
315
|
+
own lowercased text. A response with no `limits` key is an older endpoint
|
|
316
|
+
answering: the key is omitted rather than sent as `[]`, because that is no
|
|
317
|
+
information about buckets and must not erase the measured ones.
|
|
247
318
|
observed-live 2026-09-11: real windows came back and were written to
|
|
248
319
|
`<LEG_HOME>/usage/claude--default.json`; a seven_day window at 93 %
|
|
249
320
|
raised the amber warning.
|
|
@@ -367,7 +438,11 @@ variable and `LEG_SESSION` (source: src/attach.mjs, src/env.mjs).
|
|
|
367
438
|
|
|
368
439
|
- `<LEG_HOME>/usage/<agent>--<account>.json`:
|
|
369
440
|
`{ five_hour: {pct, resets_at}, seven_day: {…}, limited_until,
|
|
370
|
-
limited_reason, source, updated_at
|
|
441
|
+
limited_reason, source, updated_at, buckets, walls, history, extra_usage,
|
|
442
|
+
facts }` (source: src/usage.mjs). The last five are the per-model half of the
|
|
443
|
+
record: measured buckets, walls attributed from wording, a 24-sample ring per
|
|
444
|
+
bucket, the credits block and agent-measured strings. See
|
|
445
|
+
[concepts.md](concepts.md).
|
|
371
446
|
- Warning threshold `WARN_PCT`, default 85, from `LEG_WARN_PCT`; the warning
|
|
372
447
|
is an amber card, an event, and one terminal bell (source: src/usage.mjs,
|
|
373
448
|
src/attach.mjs).
|
|
@@ -391,6 +466,142 @@ in [VOCABULARY.md](VOCABULARY.md). Board routes: `GET /api/sessions`,
|
|
|
391
466
|
`POST /api/sessions/:id/end`, `DELETE /api/sessions/:id`, with the list pushed
|
|
392
467
|
as the SSE `sessions` event (source: src/server.mjs, src/board/sessions.js).
|
|
393
468
|
|
|
469
|
+
`session.ahead` is the number of commits the checkout is past its upstream, or
|
|
470
|
+
past `head_at_start` when it tracks nothing. It is counted with `git rev-list
|
|
471
|
+
--count <base>..HEAD` on the runner's git poll and is `null`, never `0`, when
|
|
472
|
+
this is not a repository or the count could not be taken (source:
|
|
473
|
+
src/attach.mjs `aheadCount`). Like the dirty file list, it is owner-only:
|
|
474
|
+
`redactSession` drops it from another human's row.
|
|
475
|
+
|
|
476
|
+
`GET /api/sessions` also carries `cards_waiting`, so the terminals verdict can
|
|
477
|
+
name a waiting card without reading the pipeline board:
|
|
478
|
+
`{count, first: {id, title, station, since}}`, where `count` is the number
|
|
479
|
+
of cards in `needs_approval` or `waiting_human` and `first` is the one that has
|
|
480
|
+
been waiting longest (`since` is its `updated_at`), or `null` when none is. A
|
|
481
|
+
count alone cannot write `card 3e1c has waited on you for 12 minutes.`, which
|
|
482
|
+
is the sentence the field exists for (redesign C.5). It is sent to anyone who
|
|
483
|
+
may use cards, which is the owner and any operator; a guest has no cards and is
|
|
484
|
+
not sent it (source: src/server.mjs `cardsWaiting`).
|
|
485
|
+
|
|
486
|
+
A guest's own row carries no figure that was measured from this machine's
|
|
487
|
+
logins: `limits`, `all_out` and `usage_source` are dropped, `warning` keeps its
|
|
488
|
+
window and loses its percentage and its clock, a
|
|
489
|
+
`limit` or a `waiting` of type `reset` keeps its words and loses its
|
|
490
|
+
`resets_at`, and a `handoff_targets` row carries no `cost` (the word `credits`
|
|
491
|
+
on a `claude/fable` rung is the owner's `extra_usage` setting by another name).
|
|
492
|
+
A row that is available carries no reason at all, because the reserve and
|
|
493
|
+
`below:N` notes are advice for a human press, not a refusal. Someone else's row
|
|
494
|
+
is redacted further: no `limits`, and `warning` is reduced to its window
|
|
495
|
+
(source: src/server.mjs `redactSession`, `scrubOwnerUsage`).
|
|
496
|
+
|
|
497
|
+
### Making a card over HTTP
|
|
498
|
+
|
|
499
|
+
`POST /api/cards` takes the same body `createCard` takes from the CLI, with one
|
|
500
|
+
difference: `pipeline` must be a preset name (`build`, `build-land`, `factory`)
|
|
501
|
+
or an inline array of stations. A path to a JSON file is a CLI convenience
|
|
502
|
+
(`leg card add --pipeline ./my.json`) and is refused here with `400
|
|
503
|
+
invalid pipeline: pipeline must be one of the presets ... or a list of
|
|
504
|
+
stations`, naming no path and quoting no bytes: an operator may post cards and
|
|
505
|
+
may not read this machine, and the JSON parser's own message quotes the first
|
|
506
|
+
characters of whatever file it opened (source: src/cards.mjs
|
|
507
|
+
`allowPipelineFile`).
|
|
508
|
+
|
|
509
|
+
`chain` is a comma list (`claude,codex`) or an array of entries
|
|
510
|
+
(`[{adapter, model?, mode?, max_turns?}]`). The array form is what the board's
|
|
511
|
+
one-line entry posts, because it is the only one that can put a different model
|
|
512
|
+
on each leg: `[{adapter: 'claude', model: 'fable'}, {adapter: 'claude', model:
|
|
513
|
+
'opus'}]` is two legs on one login, which `--model claude=fable` (one model per
|
|
514
|
+
adapter) cannot express.
|
|
515
|
+
|
|
516
|
+
### A terminal becomes a card, and a card becomes a terminal
|
|
517
|
+
|
|
518
|
+
`POST /api/sessions/:id/end-as-card` (owner or operator; a guest is refused)
|
|
519
|
+
writes the terminal's hand-off bundle with `saveSessionBundle`, creates a card
|
|
520
|
+
whose task is the terminal's prompt plus `Continue from the bundle at <path>.`,
|
|
521
|
+
starts the card's chain at the rung the terminal is standing on, records
|
|
522
|
+
`lineage.from`, and then requests `end` on the terminal exactly as
|
|
523
|
+
`POST /api/sessions/:id/end` does. `201` with
|
|
524
|
+
`{card, bundle, carried: {files, adopted}}`. Where the card works, and when it
|
|
525
|
+
starts, depend on what the terminal had:
|
|
526
|
+
|
|
527
|
+
- **Its own worktree.** The card adopts it (`worktree_adopted: true`, so no
|
|
528
|
+
later run cuts a second worktree on that branch) and is created in
|
|
529
|
+
`backlog`, not `queued`: `end` is a request the terminal's runner reads on
|
|
530
|
+
its own poll, and the scheduler ticks once a second, so queuing it at once
|
|
531
|
+
would put a headless agent in the working tree the interactive one is still
|
|
532
|
+
writing to. The board polls the session record and enqueues the card the
|
|
533
|
+
moment it is no longer active; if the terminal is still going ten minutes
|
|
534
|
+
later, the card stays in the backlog with a `blocked_by` line saying so and
|
|
535
|
+
Run is left to the human. `heldByLiveTerminal` in src/scheduler.mjs refuses
|
|
536
|
+
to start such a card whatever queued it.
|
|
537
|
+
- **No worktree of its own** (the ordinary case: a terminal cuts one only when
|
|
538
|
+
a second live session shares the checkout). A checkout of its own is cut from
|
|
539
|
+
the terminal's branch and the uncommitted work is carried into it before the
|
|
540
|
+
terminal is asked to stop: a patch of everything git tracks
|
|
541
|
+
(`git diff HEAD --binary`, applied with `git apply`) plus the bytes of every
|
|
542
|
+
file it does not (`git ls-files --others --exclude-standard`, skipping Leg's
|
|
543
|
+
own directories and `node_modules`). `carried.files` is how many paths moved.
|
|
544
|
+
The card is queued at once, because nothing else is in that checkout.
|
|
545
|
+
|
|
546
|
+
The bundle is written first, and the uncommitted work is read before anything
|
|
547
|
+
is created: if either fails, nothing is created, nothing is ended, and the
|
|
548
|
+
terminal is left running with `409` (source: src/server.mjs).
|
|
549
|
+
|
|
550
|
+
`POST /api/cards/:id/take-over` moves the card to `paused` through the
|
|
551
|
+
`take_over` transition, killing the child of a running one, and answers `200`
|
|
552
|
+
with `{card, command}`, where `command` is `leg <agent> --resume-card <card-id>`
|
|
553
|
+
for the agent on the card's current leg. Every non-terminal status is paused,
|
|
554
|
+
not only `running`: a `queued` or `handing_off` card is in the set the
|
|
555
|
+
scheduler starts from, so leaving it there would launch a leg into the same
|
|
556
|
+
worktree the human was just handed. One `taken_over` event with the caller's
|
|
557
|
+
name is appended whatever the starting status, and it is in the audit trail. A
|
|
558
|
+
card in `done`, `failed` or `killed` is refused with `409` (source:
|
|
559
|
+
src/server.mjs, src/chain.mjs).
|
|
560
|
+
|
|
561
|
+
`leg <agent> --resume-card <id>` is Leg's own flag and never reaches the
|
|
562
|
+
agent's argv. It opens an ordinary interactive terminal in that card's
|
|
563
|
+
worktree, whatever directory it was run from, primed with the card's bundle as
|
|
564
|
+
the first prompt and with `lineage.from` naming the card. The id may be the
|
|
565
|
+
full card id or a unique tail of it; an ambiguous one is refused by name with
|
|
566
|
+
the matches listed, and an unknown one exits `3` (source: src/attach.mjs
|
|
567
|
+
`takeFlagValue`, `resolveCardId`, `takeOverPrompt`).
|
|
568
|
+
|
|
569
|
+
A live card's board payload carries what was measured and nothing else: `work`
|
|
570
|
+
`{files, insertions, deletions}` parsed from `git diff --shortstat
|
|
571
|
+
<trunk>..HEAD` in its worktree, `tests` `{state, at}` and `land` `{state,
|
|
572
|
+
reason, sha}` read from the card's own ledger. A key that could not be measured
|
|
573
|
+
is absent; a finished card carries none of them (source: src/server.mjs
|
|
574
|
+
`parseShortstat`, `cardOutcomes`, `summarize`).
|
|
575
|
+
|
|
576
|
+
### The hand-off ladder
|
|
577
|
+
|
|
578
|
+
`leg sessions handoff <id> --to <agent>[/<account>[/<model>]]` names the rung
|
|
579
|
+
directly. Three parts are unambiguous; with two, the second is read as an
|
|
580
|
+
account when the agent has one by that name, else as a model when the agent
|
|
581
|
+
has one by that name, else the command is refused by name rather than guessed
|
|
582
|
+
at (source: bin/leg.mjs `parseTarget`). `leg sessions simulate-limit <id>
|
|
583
|
+
--message "<text>"` drives that exact wording through the real classifier
|
|
584
|
+
(`src/hook.mjs` for claude, the session's own log for agy and grok), which is
|
|
585
|
+
how a per-model wall is reached without waiting for a real one; codex has no
|
|
586
|
+
Leg-owned input to simulate through and is refused by name (source:
|
|
587
|
+
bin/leg.mjs `simulateLimit`).
|
|
588
|
+
|
|
589
|
+
`leg ladder` (also `leg ladder ls`) prints the ladder, rung 1 first, each with
|
|
590
|
+
its live state (a wall and its clock, a percentage, or "no figure") and the
|
|
591
|
+
reason it would be skipped right now, then the spending, climb-back, reserve
|
|
592
|
+
and derived-order lines. `leg ladder set <n> <agent>[/<account>[/<model>]]
|
|
593
|
+
[--when always|below:N|walled-only]` writes rung `n` (past the end of the
|
|
594
|
+
ladder, it appends); `leg ladder rm <n>` removes rung `n` and refuses to
|
|
595
|
+
remove the last rung left; `leg ladder spend on|off` flips `may_spend`
|
|
596
|
+
(source: bin/leg.mjs `ladderCommand`).
|
|
597
|
+
|
|
598
|
+
Board routes: `PATCH /api/settings` (owner only) also accepts
|
|
599
|
+
`handoff_ladder`, `climb_back`, `may_spend`, `reserve`, `notify_terminal` and
|
|
600
|
+
`notify_board`. `POST /api/sessions/:id/handoff-order` accepts either
|
|
601
|
+
`{handoff_order}` or `{handoff_ladder}` and rewrites both on the session.
|
|
602
|
+
`POST /api/sessions/:id/handoff` accepts `target.model` alongside
|
|
603
|
+
`target.agent` and `target.account` (source: src/server.mjs).
|
|
604
|
+
|
|
394
605
|
### History index
|
|
395
606
|
|
|
396
607
|
`<LEG_HOME>/history/index.json` is the one file `leg history` writes: per
|
|
@@ -428,12 +639,13 @@ non-zero exit → `failed`. Every outcome except `completed`, `auth_failed` and
|
|
|
428
639
|
`killed` asks the chain to hand off.
|
|
429
640
|
|
|
430
641
|
<!-- limits-table:start -->
|
|
431
|
-
Generated by `node scripts/limits-table.mjs` from
|
|
642
|
+
Generated by `node scripts/limits-table.mjs` from 24 fixtures (6 observed-live, 18 docs-only). Classification `limit` hands the card to the next agent as a usage limit; `auth` is a failed launch (never a limit); `launch` is a failed launch that the next agent may still try; `budget` is a turn or spend cap set by Leg itself; `info` must never classify as a limit.
|
|
432
643
|
|
|
433
644
|
| id | adapter | class | where | source | text (excerpt) | produced by |
|
|
434
645
|
|----|---------|-------|-------|--------|----------------|-------------|
|
|
435
646
|
| agy-resource-exhausted | agy | limit | any | **docs-only** | resource-exhausted | `agy changelog` ("Fixed personal accounts hitting a resource-exhausted error at startup"); agy --help documents no limit wording; falls back to the generic matchers |
|
|
436
647
|
| claude-budget-limit | claude | budget | any | **docs-only** | Budget limit reached | https://code.claude.com/docs/en/cli-reference (--max-budget-usd) |
|
|
648
|
+
| claude-fable-limit | claude | limit | any | **observed-live** | You've reached your Fable limit. Run /usage-credits to continue or switch models with /mod | fixtures/live/claude/limit-rate_limit.json: StopFailure error rate_limit, last_assistant_message on the real run, 2026-09-11 (a per-model wall: the account's session and weekly windows were open) |
|
|
437
649
|
| claude-max-turns | claude | budget | stdout | **observed-live** | {"type":"result","subtype":"error_max_turns","is_error":true,"stop_reason":"tool_use","ter | fixtures/real-run/leg1/out.log: `claude -p --max-turns 2` on the real run, 2026-09-10 (docs: https://code.claude.com/docs/en/cli-reference --max-turns "Exits with an error when the limit is reached") |
|
|
438
650
|
| claude-model-limit | claude | limit | any | **docs-only** | You’ve hit your Opus limit | https://code.claude.com/docs/en/costs ("You’ve hit your Opus limit" / "You’ve hit your Sonnet limit") |
|
|
439
651
|
| claude-session-limit | claude | limit | any | **docs-only** | You’ve hit your session limit | https://code.claude.com/docs/en/costs (section: When a developer asks about a limit) |
|
|
@@ -449,6 +661,7 @@ Generated by `node scripts/limits-table.mjs` from 22 fixtures (4 observed-live,
|
|
|
449
661
|
| generic-rate-limit | * | limit | any | **docs-only** | rate limit | generic matcher; lowest priority |
|
|
450
662
|
| generic-resource-exhausted | * | limit | any | **docs-only** | RESOURCE_EXHAUSTED | generic matcher (gRPC RESOURCE_EXHAUSTED); lowest priority |
|
|
451
663
|
| generic-usage-limit | * | limit | any | **docs-only** | usage limit | generic matcher; lowest priority |
|
|
664
|
+
| grok-balance-exhausted | grok | limit | any | **observed-live** | API error (status 402 Payment Required): Grok Build usage balance exhausted | grok 1.0.34 -p --output-format json on an account with no balance left, 2026-09-17: stdout {"type":"error","message":"Internal error: { \"message\": \"API error (status 402 Payment Required): Grok Build usage balance exhausted\", \"http_status\": 402 }"}, exit 1 |
|
|
452
665
|
| grok-not-logged-in | grok | auth | stderr | **observed-live** | To sign in, open this URL in your browser: https://accounts.x.ai/oauth2/device?user_cod | fixtures/live/grok/err.log from scripts/probe.mjs --adapter grok, 2026-09-10 (stdout JSON stopReason: Cancelled, exit 0) |
|
|
453
666
|
| grok-rate-limit | grok | limit | any | **docs-only** | You've hit the rate limit for your plan. Try again later. | crates/codegen/xai-grok-shell/src/sampling/error.rs:18 (RATE_LIMITED_USER_MESSAGE_OAUTH: "You've hit the rate limit for your plan. Try again later.") |
|
|
454
667
|
| auth-source-set | * | auth | stderr | **docs-only** | another auth source is set | project brief (Wes, 2026-09-10): stderr saying "another auth source is set" counts as a failed launch; wording not yet observed live |
|
package/docs/concepts.md
CHANGED
|
@@ -33,6 +33,28 @@ A session's status is one of `starting`, `running`, `warning`, `limit`,
|
|
|
33
33
|
that owned the terminal is gone (closed window, crash); the board never shows
|
|
34
34
|
it as live.
|
|
35
35
|
|
|
36
|
+
Two fields on `session.json` say what the row is running and what it is stuck
|
|
37
|
+
on. `model` is the model this leg resolved to: the `--model` or `-m` the human
|
|
38
|
+
passed, else null. It is never a guessed default, because a model token nobody
|
|
39
|
+
chose is a wrong number in disguise. For claude the runner corrects it from the
|
|
40
|
+
transcript's per-message `model` while the agent works, so a silent fallback
|
|
41
|
+
off Fable is visible instead of being reported as the model you asked for.
|
|
42
|
+
|
|
43
|
+
`waiting` is null, or what this terminal is waiting for, in one of two shapes
|
|
44
|
+
told apart by `type`. `{ type: 'reset', agent, account, resets_at, since }` is
|
|
45
|
+
the all-out countdown: every option is walled and the terminal is waiting for
|
|
46
|
+
the first one back. `{ type, message, since }` with `type` one of
|
|
47
|
+
`permission_prompt`, `idle_prompt`, `agent_needs_input` or `quota_auto_resume`
|
|
48
|
+
is a human being waited on, from Claude Code's `Notification` hook (claude
|
|
49
|
+
sessions only); `message` is the question verbatim, cut at 160 characters, and
|
|
50
|
+
the next prompt or the end of the turn clears it. `quota_auto_resume` means
|
|
51
|
+
Claude Code is waiting at the limit by itself, and Leg's automatic hand-off
|
|
52
|
+
stands down for that terminal rather than making it wait twice.
|
|
53
|
+
|
|
54
|
+
Both are owner-only on a shared board, and a guest keeps both on their own
|
|
55
|
+
terminal: a terminal stopped at a permission prompt is useless to the person
|
|
56
|
+
sitting at it if the board will not say so, and it is nobody else's business.
|
|
57
|
+
|
|
36
58
|
The board is started detached on `127.0.0.1:4747` by the first session that
|
|
37
59
|
finds it down, and opened once. Later sessions reuse it.
|
|
38
60
|
|
|
@@ -69,9 +91,32 @@ keeps the latest reading per (agent, account) in
|
|
|
69
91
|
|
|
70
92
|
```
|
|
71
93
|
{ five_hour: {pct, resets_at}, seven_day: {pct, resets_at},
|
|
72
|
-
limited_until, limited_reason, source, updated_at
|
|
94
|
+
limited_until, limited_reason, source, updated_at,
|
|
95
|
+
buckets, walls, history, extra_usage, facts }
|
|
73
96
|
```
|
|
74
97
|
|
|
98
|
+
A login can be limited per model as well as per account, so the record carries
|
|
99
|
+
both kinds of fact and keeps them apart. `buckets` is measured: one row per
|
|
100
|
+
bucket the agent publishes (`{kind, group, model, percent, resets_at,
|
|
101
|
+
is_active, severity}`, from Claude's `limits[]`), and `binding(u, model)` picks
|
|
102
|
+
the row that will actually stop a terminal. `walls` is attributed from wording
|
|
103
|
+
rather than measured: `bucketFromWall()` in `src/buckets.mjs` reads the wall
|
|
104
|
+
message, and a model-scoped wall goes to `walls[model]` while the login stays
|
|
105
|
+
open, so a Fable wall never stops `claude/sonnet`; a model wall with no clock of
|
|
106
|
+
its own is dated from that model's bucket, else from the weekly window, because
|
|
107
|
+
a per-model limit is a weekly fact and the five-hour clock would hand the model
|
|
108
|
+
back within the hour. `history` is a ring per bucket, capped by the clock first
|
|
109
|
+
and by 24 samples second: at most one sample a minute, none older than the
|
|
110
|
+
window it was measured in, and started again whenever that window resets (each
|
|
111
|
+
sample carries its own `resets_at`, so a bucket that is missing from one reading
|
|
112
|
+
cannot carry its old samples into the next window). Capping by count alone made
|
|
113
|
+
the forecast vanish from any login with three terminals on it, because three
|
|
114
|
+
pollers fill the ring three times as fast. `extra_usage` is the credits
|
|
115
|
+
sentence, and `facts` holds strings the
|
|
116
|
+
agent measured itself (codex's `plan_type` and `credits_balance`). An older Leg
|
|
117
|
+
reading this file ignores all five, and an agent that publishes no buckets
|
|
118
|
+
leaves them empty.
|
|
119
|
+
|
|
75
120
|
Where each number comes from is per agent, and is in
|
|
76
121
|
[adapters.md](adapters.md). The rules on top of them are shared:
|
|
77
122
|
|
|
@@ -79,15 +124,34 @@ Where each number comes from is per agent, and is in
|
|
|
79
124
|
highest percentage across the known windows is the pressure; the hottest
|
|
80
125
|
window names the warning.
|
|
81
126
|
- **Wall.** `markLimited()` records `limited_until` from the reset time the CLI
|
|
82
|
-
itself reported. With no reset time
|
|
83
|
-
|
|
127
|
+
itself reported. With no reset time, an account wall takes the window that
|
|
128
|
+
actually walled (the highest used percentage, so a weekly wall is not
|
|
129
|
+
recorded as the five-hour window's near reset) and a model wall takes that
|
|
130
|
+
model's own bucket, else the weekly window. With neither it assumes five
|
|
131
|
+
hours.
|
|
84
132
|
- **Clearing.** A usage reading that arrives after `limited_until` has passed
|
|
85
133
|
clears the wall.
|
|
86
134
|
|
|
87
135
|
## Handoff (interactive)
|
|
88
136
|
|
|
137
|
+
A hand-off destination is a **rung**, not just an agent: `{agent, account,
|
|
138
|
+
model, when, cost}`. A rung can name a model, so a hand-off can move from
|
|
139
|
+
`claude/fable` to `claude/opus` without leaving the login, or it can leave the
|
|
140
|
+
model out and behave exactly like the plain agent destinations Leg has always
|
|
141
|
+
had. `preferences.json` keeps an ordered list of rungs, the **ladder**, tried
|
|
142
|
+
top first (`src/preferences.mjs`; the keys are documented in
|
|
143
|
+
[configuration.md](configuration.md)). `handoff_order`, the older list of
|
|
144
|
+
agents, is never removed: it is derived from the ladder's distinct agent order
|
|
145
|
+
every time the ladder is saved, so a reader that has never heard of rungs sees
|
|
146
|
+
the same order it always did.
|
|
147
|
+
|
|
89
148
|
When a session hits its limit, or you press **Hand off now**, Leg does four
|
|
90
|
-
things in order (`src/attach.mjs`, `src/bundle.mjs`)
|
|
149
|
+
things in order (`src/attach.mjs`, `src/bundle.mjs`). Which of the two it was
|
|
150
|
+
decides the gates below: a hand-off nobody asked for (the usage limit, and a
|
|
151
|
+
card the scheduler is driving) is **automatic** and keeps the `reserve` and the
|
|
152
|
+
`climb_back` policy; pressing **Hand off now** is a human pick and is not held
|
|
153
|
+
back by either, with or without a named destination, because the default option
|
|
154
|
+
in that picker ("the next option in the order") is still a press of the button.
|
|
91
155
|
|
|
92
156
|
1. **Bundle.** `sessionNotes()` writes the six sections
|
|
93
157
|
`context-handoff-bundle` parses (Scope, Projects mentioned, Findings,
|
|
@@ -99,17 +163,44 @@ things in order (`src/attach.mjs`, `src/bundle.mjs`):
|
|
|
99
163
|
in place. A checkpoint runs about every two minutes while the session has
|
|
100
164
|
turns, and at every warning, limit and hand-off.
|
|
101
165
|
2. **Choose.** `candidates()` lists the other accounts of the same agent first,
|
|
102
|
-
then
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
it
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
166
|
+
then walks the ladder, and after each rung, the other accounts of that
|
|
167
|
+
rung's agent. `evaluateLadder()` walks that list top to bottom and takes the
|
|
168
|
+
first rung that clears every gate, checked in this order: the agent is
|
|
169
|
+
installed on this machine; the rung was not excluded for this hand-off; its
|
|
170
|
+
live cost (`rungCost()`, computed from usage, never trusted from disk) is
|
|
171
|
+
`free` or `plan`, or `may_spend` is on, else the rung is skipped with the
|
|
172
|
+
reason `it spends usage credits and you have not allowed that`; the rung
|
|
173
|
+
sits on the same login the terminal just fell off of and what stopped that
|
|
174
|
+
login is its own account-wide window, in which case another model there
|
|
175
|
+
cannot help and the rung is skipped as `shares the window that is out, buys
|
|
176
|
+
nothing`, whatever model it names; the account itself is not walled; the
|
|
177
|
+
model, if the rung names one, is not walled; on an automatic hand-off with
|
|
178
|
+
`climb_back` set to `never`, the rung is not a stronger model of the login
|
|
179
|
+
the terminal just left (a human's own pick still reaches it; only an
|
|
180
|
+
automatic climb is held back); the rung's login is not past its `reserve`
|
|
181
|
+
floor; and its `when` is satisfied (`always`; `below:N`, which needs the
|
|
182
|
+
rung's own bucket to read under N percent and is skipped with a reason when
|
|
183
|
+
there is no reading, because a threshold on a login with no figure is a
|
|
184
|
+
wrong number in disguise; `walled-only`, which only opens once no rung above
|
|
185
|
+
it could take the hand-off anyway: walled, at 100 percent, not installed on
|
|
186
|
+
this machine, or refused for this hand-off, but not merely slow and not
|
|
187
|
+
merely dearer than `may_spend` allows). Every rung the walk passes before the one it picks is
|
|
188
|
+
named in the session's ledger with its reason, one line each, for example
|
|
189
|
+
`skipped claude/fable: it spends usage credits and you have not allowed
|
|
190
|
+
that`; a rung is never passed over silently. The board can save a new
|
|
191
|
+
ladder for an active terminal; the wrapper reads it again at the transition
|
|
192
|
+
and during all-out waiting. Machine Settings is copied only when a new
|
|
193
|
+
terminal starts.
|
|
194
|
+
3. **Switch.** The agent process is stopped and the terminal restored. A
|
|
195
|
+
same-login move to a weaker claude model (a downshift, by the
|
|
196
|
+
`fable, opus, sonnet, haiku` order) with the agent's own session id on the
|
|
197
|
+
record keeps the conversation instead: Leg runs
|
|
198
|
+
`claude --resume <agent_session_id> --model <alias>`, no bundle is written
|
|
199
|
+
into a prompt, and the ledger says so. Every other rung, including an
|
|
200
|
+
upshift back to a stronger model, a different account, or a different
|
|
201
|
+
agent, takes the bundle: the `context-handoff-bundle load <id>` output
|
|
202
|
+
(with the `## Synthesis` section prepended if
|
|
203
|
+
`.leg/SYNTHESIS-<session-id>.md` is present) is written to
|
|
113
204
|
`.leg/RESUME-<session-id>.md` and copied to `.leg/RESUME.md`, and the next
|
|
114
205
|
agent starts in the same terminal with a short pointer prompt as its first
|
|
115
206
|
positional argument: `claude "<prompt>"`, `codex "<prompt>"`,
|
|
@@ -125,6 +216,13 @@ things in order (`src/attach.mjs`, `src/bundle.mjs`):
|
|
|
125
216
|
(`{ agent, account, resets_at, since }`) and shows status `waiting`. Ctrl-C
|
|
126
217
|
in the terminal, or End on the card, quits with exit 3.
|
|
127
218
|
|
|
219
|
+
`climb_back` decides what happens once a lower rung's own login recovers.
|
|
220
|
+
`next-handoff`, the default, needs no extra step: `chooseNext()` always walks
|
|
221
|
+
from rung 1, so the next time this terminal hands off it is offered the
|
|
222
|
+
higher rung again. `never` holds a terminal on the rung it downshifted to
|
|
223
|
+
until a human hands it off there by name; an automatic hand-off will not walk
|
|
224
|
+
back up on its own.
|
|
225
|
+
|
|
128
226
|
## The portable harness
|
|
129
227
|
|
|
130
228
|
Off by default. `leg harness enable` adds a step to the hand-off, between
|
|
@@ -218,8 +316,54 @@ working; `leg share rotate <name>` replaces one.
|
|
|
218
316
|
|
|
219
317
|
## Cards, stations and pipelines
|
|
220
318
|
|
|
221
|
-
|
|
222
|
-
|
|
319
|
+
A **card is a terminal you are not sitting at.** It has the same register
|
|
320
|
+
(state, where, model), the same one sentence, the same buttons, the same
|
|
321
|
+
fallback ladder and the same hand-off bundle as a terminal. Two differences
|
|
322
|
+
are real, and the board prints both: a card runs `-p --output-format json`,
|
|
323
|
+
which says nothing until the leg exits, so its sentence reads `no message
|
|
324
|
+
until this leg ends, started 11:04 PM`; and it never waits on a permission
|
|
325
|
+
prompt, because its permissions are decided before it starts.
|
|
326
|
+
|
|
327
|
+
Liveness decides where a card is drawn. A card in `backlog`, `queued`,
|
|
328
|
+
`running`, `handing_off`, `needs_approval`, `waiting_human` or `paused` is a
|
|
329
|
+
row in the Background panel under Terminals. A card in `done`, `failed` or
|
|
330
|
+
`killed` is one line in the ledger: `3 finished cards, 2 done, 1 failed, last
|
|
331
|
+
11:02 PM`. Ten finished cards are one row, not eleven.
|
|
332
|
+
|
|
333
|
+
A live card's row carries a **work stat**, and only the parts of it that were
|
|
334
|
+
measured: `4 files, +212 -18, tests green 6m ago`. The diff comes from `git
|
|
335
|
+
diff --shortstat <trunk>..HEAD` run in the card's own worktree; the test and
|
|
336
|
+
land verdicts come from that card's ledger. A part that was not measured is
|
|
337
|
+
left off, never estimated.
|
|
338
|
+
|
|
339
|
+
### A terminal becomes a card, and a card becomes a terminal
|
|
340
|
+
|
|
341
|
+
`End, and keep going as a card` (the second verb on a terminal's End confirm
|
|
342
|
+
row, `POST /api/sessions/<id>/end-as-card`) is for "I have to leave, keep
|
|
343
|
+
going". It writes the terminal's hand-off bundle at the path every hand-off
|
|
344
|
+
uses, creates a card whose task is the terminal's prompt plus `Continue from
|
|
345
|
+
the bundle at <path>.`, and then ends the terminal exactly the way End does.
|
|
346
|
+
The card **continues in the terminal's own worktree** rather than a fresh one:
|
|
347
|
+
two worktrees on one branch is a conflict machine, and the card carries
|
|
348
|
+
`worktree_adopted: true` so no later run cuts a second one over the top. Its
|
|
349
|
+
chain starts at the rung the terminal was standing on, not at the top of the
|
|
350
|
+
ladder, and `lineage.from` names the terminal it came from.
|
|
351
|
+
|
|
352
|
+
`Take over` in a card's expansion (`POST /api/cards/<id>/take-over`) is the way
|
|
353
|
+
back. It pauses the card, which kills its child and writes its bundle, then
|
|
354
|
+
hands back one command:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
leg claude --resume-card card-20260917-2234-add-the-audit-csv-export
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
That opens an ordinary interactive terminal in the card's worktree, primed
|
|
361
|
+
from the card's bundle, with `lineage.from` naming the card. This is the one
|
|
362
|
+
place Leg hands a human a command to paste, because a terminal cannot be
|
|
363
|
+
opened from a browser tab, and the board says so on the line above it.
|
|
364
|
+
|
|
365
|
+
Everything from here down is the v0.1 pipeline mechanism: headless agents in a
|
|
366
|
+
git worktree, one per card.
|
|
223
367
|
|
|
224
368
|
A **card** is one task moving through a **pipeline**: an ordered list of
|
|
225
369
|
**stations**. A station has a `kind`:
|
|
@@ -345,7 +489,9 @@ Alongside raw state, Leg supports an agent-maintained judgment record in `.leg/S
|
|
|
345
489
|
## Worktrees
|
|
346
490
|
|
|
347
491
|
Every card runs in its own git worktree: `<repo>/.leg-worktrees/<card-id>`
|
|
348
|
-
on branch `leg/<card-id>` (`src/worktree.mjs`)
|
|
492
|
+
on branch `leg/<card-id>` (`src/worktree.mjs`), with one exception: a card born
|
|
493
|
+
from `End, and keep going as a card` adopts the terminal's worktree and works
|
|
494
|
+
there, exactly where the terminal stopped. The repo root is never
|
|
349
495
|
touched by an agent directly. Every git call sets `MSYS_NO_PATHCONV=1` so
|
|
350
496
|
Git Bash on Windows does not rewrite absolute path arguments. Leg never
|
|
351
497
|
pushes, opens a remote, or removes a path outside
|
|
@@ -423,7 +569,9 @@ status (`backlog`, `queued`, `running`, `handing_off`, `waiting_human`,
|
|
|
423
569
|
`needs_approval`, `paused`) straight to `killed`, and `rerun` moves any
|
|
424
570
|
terminal status (`done`, `failed`, `killed`) back to `queued` (station 0,
|
|
425
571
|
leg 0). `reassign` also applies to any non-terminal status when the current
|
|
426
|
-
station is an agent station, staying in `queued
|
|
572
|
+
station is an agent station, staying in `queued`, and `take_over` moves any
|
|
573
|
+
non-terminal status to `paused` (a human is taking the card's checkout, so the
|
|
574
|
+
scheduler must not start a leg in it).
|
|
427
575
|
|
|
428
576
|
```mermaid
|
|
429
577
|
stateDiagram-v2
|