@ucsandman/legcli 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/LICENSE +58 -0
  3. package/NOTICE +4 -0
  4. package/README.md +558 -0
  5. package/bin/fake-agent.mjs +143 -0
  6. package/bin/leg.mjs +449 -0
  7. package/docs/DECISIONS.md +119 -0
  8. package/docs/DEMO.md +214 -0
  9. package/docs/DEVIATIONS.md +182 -0
  10. package/docs/ERRORS.md +326 -0
  11. package/docs/README.md +40 -0
  12. package/docs/REUSE.md +289 -0
  13. package/docs/ROADMAP-v2.md +55 -0
  14. package/docs/VOCABULARY.md +243 -0
  15. package/docs/adapters.md +300 -0
  16. package/docs/board-guide.md +579 -0
  17. package/docs/cli-contracts.md +401 -0
  18. package/docs/concepts.md +426 -0
  19. package/docs/configuration.md +220 -0
  20. package/docs/faq.md +206 -0
  21. package/docs/getting-started.md +251 -0
  22. package/docs/real-run.md +80 -0
  23. package/docs/review-2026-09-14.md +72 -0
  24. package/docs/screenshots/board-400px.png +0 -0
  25. package/docs/screenshots/board-details-open.png +0 -0
  26. package/docs/screenshots/board-done.png +0 -0
  27. package/docs/screenshots/board-drawer.png +0 -0
  28. package/docs/screenshots/board-empty.png +0 -0
  29. package/docs/screenshots/board-handoff.png +0 -0
  30. package/docs/screenshots/board-running.png +0 -0
  31. package/docs/screenshots/demo-1-claude-running.png +0 -0
  32. package/docs/screenshots/demo-2-limit-hit.png +0 -0
  33. package/docs/screenshots/demo-3-handoff-bundle.png +0 -0
  34. package/docs/screenshots/demo-4-codex-running.png +0 -0
  35. package/docs/screenshots/demo-5-done.png +0 -0
  36. package/docs/screenshots/floor-final-1280.png +0 -0
  37. package/docs/screenshots/floor-final-400.png +0 -0
  38. package/docs/screenshots/floor-landing.png +0 -0
  39. package/docs/screenshots/floor.png +0 -0
  40. package/docs/screenshots/share-guest-1280.png +0 -0
  41. package/docs/screenshots/share-owner-1280.png +0 -0
  42. package/docs/screenshots/terminals-1280.png +0 -0
  43. package/docs/screenshots/thumb-done.jpg +0 -0
  44. package/docs/screenshots/thumb-handoff-details.jpg +0 -0
  45. package/docs/screenshots/thumb-limit-hit.jpg +0 -0
  46. package/docs/screenshots/youtube-thumbnail.jpg +0 -0
  47. package/fixtures/limits/agy/agy-resource-exhausted.json +11 -0
  48. package/fixtures/limits/claude/claude-budget-limit.json +11 -0
  49. package/fixtures/limits/claude/claude-max-turns.json +11 -0
  50. package/fixtures/limits/claude/claude-model-limit.json +11 -0
  51. package/fixtures/limits/claude/claude-session-limit.json +11 -0
  52. package/fixtures/limits/claude/claude-weekly-limit.json +11 -0
  53. package/fixtures/limits/codex/codex-quota-exceeded.json +11 -0
  54. package/fixtures/limits/codex/codex-rate-limit-exceeded.json +11 -0
  55. package/fixtures/limits/codex/codex-skills-notice.json +11 -0
  56. package/fixtures/limits/codex/codex-usage-limit-reached.json +11 -0
  57. package/fixtures/limits/codex/codex-usage-limit.json +11 -0
  58. package/fixtures/limits/generic/generic-429.json +11 -0
  59. package/fixtures/limits/generic/generic-overloaded.json +11 -0
  60. package/fixtures/limits/generic/generic-quota.json +11 -0
  61. package/fixtures/limits/generic/generic-rate-limit.json +11 -0
  62. package/fixtures/limits/generic/generic-resource-exhausted.json +11 -0
  63. package/fixtures/limits/generic/generic-usage-limit.json +11 -0
  64. package/fixtures/limits/grok/grok-not-logged-in.json +11 -0
  65. package/fixtures/limits/negative/auth-source-set.json +11 -0
  66. package/fixtures/limits/negative/compile-error.json +11 -0
  67. package/fixtures/limits/negative/empty-stdout-exit-0.json +11 -0
  68. package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +1 -0
  69. package/fixtures/live/agy/cmd.txt +1 -0
  70. package/fixtures/live/agy/err.log +0 -0
  71. package/fixtures/live/agy/out.log +1 -0
  72. package/fixtures/live/agy/parsed.json +5 -0
  73. package/fixtures/live/agy/run.json +18 -0
  74. package/fixtures/live/agy/supervisor.log +2 -0
  75. package/fixtures/live/claude/cmd.txt +1 -0
  76. package/fixtures/live/claude/err.log +0 -0
  77. package/fixtures/live/claude/limit-rate_limit.json +21 -0
  78. package/fixtures/live/claude/out.log +1 -0
  79. package/fixtures/live/claude/parsed.json +11 -0
  80. package/fixtures/live/claude/run.json +18 -0
  81. package/fixtures/live/claude/supervisor.log +2 -0
  82. package/fixtures/live/codex/cmd.txt +1 -0
  83. package/fixtures/live/codex/err.log +1 -0
  84. package/fixtures/live/codex/last.md +2 -0
  85. package/fixtures/live/codex/limit-usage_limit_exceeded.json +25 -0
  86. package/fixtures/live/codex/out.log +8 -0
  87. package/fixtures/live/codex/parsed.json +15 -0
  88. package/fixtures/live/codex/run.json +18 -0
  89. package/fixtures/live/codex/supervisor.log +2 -0
  90. package/fixtures/live/grok/cmd.txt +1 -0
  91. package/fixtures/live/grok/err.log +32 -0
  92. package/fixtures/live/grok/out.log +7 -0
  93. package/fixtures/live/grok/parsed.json +5 -0
  94. package/fixtures/live/grok/run.json +18 -0
  95. package/fixtures/live/grok/supervisor.log +2 -0
  96. package/fixtures/verified.json +34 -0
  97. package/package.json +68 -0
  98. package/scripts/board-shots.mjs +69 -0
  99. package/scripts/build-docs-site.mjs +308 -0
  100. package/scripts/check-claims.mjs +150 -0
  101. package/scripts/clean-clone-check.sh +42 -0
  102. package/scripts/license-sign.mjs +23 -0
  103. package/scripts/limits-table.mjs +38 -0
  104. package/scripts/live-limits.mjs +36 -0
  105. package/scripts/privacy-check.mjs +63 -0
  106. package/scripts/probe.mjs +92 -0
  107. package/scripts/seed-fake-cards.mjs +34 -0
  108. package/scripts/seed-floor-board.mjs +69 -0
  109. package/scripts/seed-wes-board.mjs +91 -0
  110. package/scripts/stripe-setup.mjs +99 -0
  111. package/scripts/vercel-env.mjs +40 -0
  112. package/src/accounts.mjs +119 -0
  113. package/src/adapters/agy.mjs +56 -0
  114. package/src/adapters/claude.mjs +54 -0
  115. package/src/adapters/codex.mjs +73 -0
  116. package/src/adapters/common.mjs +32 -0
  117. package/src/adapters/fake.mjs +48 -0
  118. package/src/adapters/grok.mjs +51 -0
  119. package/src/adapters/index.mjs +27 -0
  120. package/src/adapters/resolve.mjs +40 -0
  121. package/src/attach.mjs +592 -0
  122. package/src/auth.mjs +85 -0
  123. package/src/board/board.css +543 -0
  124. package/src/board/board.js +1307 -0
  125. package/src/board/favicon.svg +1 -0
  126. package/src/board/floor.html +127 -0
  127. package/src/board/floor.js +670 -0
  128. package/src/board/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
  129. package/src/board/fonts/OFL-azeret-mono.txt +94 -0
  130. package/src/board/fonts/atkinson-board.woff2 +0 -0
  131. package/src/board/fonts/azeret-board.woff2 +0 -0
  132. package/src/board/index.html +251 -0
  133. package/src/board/sessions.js +1451 -0
  134. package/src/bundle.mjs +103 -0
  135. package/src/cards.mjs +114 -0
  136. package/src/chain.mjs +264 -0
  137. package/src/commands.mjs +79 -0
  138. package/src/contract.mjs +73 -0
  139. package/src/env.mjs +19 -0
  140. package/src/fsx.mjs +72 -0
  141. package/src/git-snapshot.mjs +152 -0
  142. package/src/handoff.mjs +171 -0
  143. package/src/hook.mjs +49 -0
  144. package/src/land.mjs +106 -0
  145. package/src/launcher.mjs +272 -0
  146. package/src/leases.mjs +86 -0
  147. package/src/ledger.mjs +369 -0
  148. package/src/license.mjs +168 -0
  149. package/src/limits.mjs +97 -0
  150. package/src/live-capture.mjs +69 -0
  151. package/src/mergequeue.mjs +225 -0
  152. package/src/orchestrator.mjs +365 -0
  153. package/src/pipeline.mjs +100 -0
  154. package/src/preferences.mjs +47 -0
  155. package/src/presets.mjs +23 -0
  156. package/src/ratelimit.mjs +49 -0
  157. package/src/redact.mjs +47 -0
  158. package/src/resume.mjs +354 -0
  159. package/src/runner.mjs +438 -0
  160. package/src/scheduler.mjs +118 -0
  161. package/src/server.mjs +845 -0
  162. package/src/session-detail.mjs +129 -0
  163. package/src/sessions.mjs +202 -0
  164. package/src/share.mjs +163 -0
  165. package/src/stations/agent.mjs +42 -0
  166. package/src/stations/build.mjs +9 -0
  167. package/src/stations/human.mjs +5 -0
  168. package/src/stations/land.mjs +6 -0
  169. package/src/stations/plan.mjs +8 -0
  170. package/src/stations/pr.mjs +31 -0
  171. package/src/stations/review.mjs +8 -0
  172. package/src/stations/test.mjs +27 -0
  173. package/src/store.mjs +90 -0
  174. package/src/sync/dashclaw.mjs +70 -0
  175. package/src/sync/index.mjs +54 -0
  176. package/src/sync/workboard.mjs +54 -0
  177. package/src/taps/agy.mjs +64 -0
  178. package/src/taps/claude-usage.mjs +67 -0
  179. package/src/taps/claude.mjs +170 -0
  180. package/src/taps/codex.mjs +286 -0
  181. package/src/trust.mjs +323 -0
  182. package/src/usage.mjs +179 -0
  183. package/src/wait.mjs +30 -0
  184. package/src/worktree.mjs +207 -0
@@ -0,0 +1,401 @@
1
+ # CLI contracts
2
+
3
+ What Leg knows about each coding-agent CLI, where every fact came from, and
4
+ which facts were observed live on the build machine versus read from `--help`
5
+ or docs. Two contracts per CLI: the headless argv a pipeline leg spawns
6
+ (§ per CLI below), and the interactive tap `leg <agent>` reads
7
+ (§ [Interactive taps](#interactive-taps)). Evidence:
8
+ `fixtures/help/<cli>.txt` (raw `--help`), `fixtures/live/<cli>/` (one real tiny
9
+ task per CLI, run through `src/runner.mjs launch` on 2026-09-11 UTC, the
10
+ evening of 2026-09-10 locally; paths under the local home directory are
11
+ replaced with `~`), and the interactive checks run on 2026-09-11.
12
+
13
+ Every fact line ends with `(source: …)`. `observed-live` means the build machine
14
+ did it; `docs-only` means the CLI's own `--help` or documentation says so and
15
+ Leg has not seen it happen.
16
+
17
+ Probe task (identical for every CLI, `scripts/probe.mjs`): "Create a file named
18
+ hello-<name>.txt in the current directory containing exactly the word hi.
19
+ Then create the directory .leg if it is missing and write the file
20
+ .leg/DONE containing the single line: done. Do nothing else. Do not ask
21
+ questions."
22
+
23
+ | CLI | version | probe result | adapter |
24
+ |-----|---------|--------------|---------|
25
+ | claude | 2.1.268 | exit 0, file yes, DONE yes, 20 s | `src/adapters/claude.mjs` |
26
+ | codex | codex-cli 0.153.4 | exit 0, file yes, DONE yes, 28 s | `src/adapters/codex.mjs` |
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.2.51 | exit 0, `stopReason: Cancelled`, no file: not logged in (device-code prompt) | `src/adapters/grok.mjs` exists, NOT registered |
29
+
30
+ The auth-source check: the build shell carried `ANTHROPIC_API_KEY` and
31
+ `OPENAI_API_KEY` (injected by the shell profile). Every adapter's `env()` deletes
32
+ them (`src/env.mjs`). No fixture contains "another auth source"
33
+ (`grep -ril "another auth source" fixtures/live` is empty), claude's and agy's
34
+ stderr are 0 bytes, and codex's is the one stdin notice. observed-live.
35
+
36
+ ## claude
37
+
38
+ - Version 2.1.268 (source: `claude --version`, fixtures/help/claude.version.txt).
39
+ - Binary: `~/.local/bin/claude.exe` (native exe, 211 MB); Leg resolves it
40
+ there first, else `claude` on PATH; `LEG_CLAUDE_BIN` overrides (source: `ls
41
+ ~/.local/bin/claude.exe`; src/adapters/claude.mjs `resolve()`).
42
+ - Headless argv (exact, from fixtures/live/claude/cmd.txt):
43
+ `claude.exe -p --output-format json --permission-mode acceptEdits`
44
+ with the prompt on **stdin** (stdin: `pipe`). Optional: `--max-turns N`,
45
+ `--resume <session-id>`, `--model <alias|name>`, `--allowedTools a,b`
46
+ (source: `claude --help` lines for each flag; observed-live for the base argv).
47
+ - Output: ONE JSON object on stdout, printed only when the session ends
48
+ (test/lessons.test.mjs `json-only-at-end`; observed-live: 20 s of silence
49
+ then the object). Top-level
50
+ keys observed: `type:"result"`, `subtype:"success"`, `is_error`,
51
+ `stop_reason:"end_turn"`, `terminal_reason:"completed"`, `api_error_status`,
52
+ `session_id`, `num_turns`, `result` (final text), `permission_denials` (array),
53
+ `usage{input_tokens,output_tokens,cache_read_input_tokens,…}`, `modelUsage`,
54
+ `total_cost_usd`, `duration_ms` (source: fixtures/live/claude/out.log).
55
+ Leg reads `session_id`, `result`, `stop_reason`, `subtype`, `is_error`,
56
+ `terminal_reason`, `api_error_status`, `permission_denials.length`.
57
+ - Model: with no `--model`, the account's default model answered
58
+ (`modelUsage` names claude-fable-5-1 and claude-haiku-4-5). A chain entry
59
+ should name `model` to control spend; Leg passes it through as `--model`
60
+ (source: fixtures/live/claude/out.log `modelUsage`; `claude --help --model`).
61
+ - Exit codes:
62
+
63
+ | exit | meaning | source |
64
+ |------|---------|--------|
65
+ | 0 | completed, JSON printed | observed-live, fixtures/live/claude/run.json |
66
+ | non-zero | launch or fatal error; JSON may be absent | docs-only (`claude --help` does not enumerate codes; phase 4 records the fixture cases) |
67
+
68
+ - Permission modes (`--permission-mode` choices in 2.1.268): `acceptEdits`,
69
+ `auto`, `bypassPermissions`, `manual`, `dontAsk`, `plan` (source: `claude
70
+ --help`). Leg: default `acceptEdits`; allowed `acceptEdits`, `auto`, `plan`,
71
+ `manual`, `dontAsk`; forbidden `bypassPermissions`,
72
+ `--dangerously-skip-permissions`, `--allow-dangerously-skip-permissions`.
73
+ `acceptEdits` allowed the file write headless (observed-live). The Agent tool
74
+ is never disallowed (test/lessons.test.mjs `agent-tool-never-disallowed`).
75
+ `--permission-prompts none` exists
76
+ ("anything that would prompt is denied automatically") and is not passed;
77
+ headless `-p` has no host to answer prompts anyway (source: `claude --help`).
78
+ - Resume: `--resume <session-id>` (docs-only until phase 5 exercises it);
79
+ `--fork-session` creates a new id on resume (source: `claude --help`).
80
+ - Child-process rules: `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`,
81
+ `ANTHROPIC_BASE_URL`, `CLAUDECODE`, `CLAUDE_CODE_*`, `CLAUDE_EFFORT`,
82
+ `CLAUDE_PLUGIN_DATA` deleted; `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0` set
83
+ (source: src/env.mjs; the "another auth source" check above, observed-live).
84
+ - Limit signals: see § Limit signals (phase 4).
85
+
86
+ ## codex
87
+
88
+ - Version codex-cli 0.153.4 (source: `codex --version`).
89
+ - Binary: npm package `@openai/codex` whose `bin/codex.js` only spawns the
90
+ platform package's native exe. Leg spawns that exe directly:
91
+ `~/AppData/Roaming/npm/node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc/bin/codex.exe`
92
+ (one process to kill); falls back to `node <bin/codex.js>` via
93
+ `resolveNpmCliEntry`, then `codex` on PATH; `LEG_CODEX_BIN` overrides
94
+ (source: `bin/codex.js` read 2026-09-10; fixtures/live/codex/cmd.txt).
95
+ - Headless argv (exact, from cmd.txt):
96
+ `codex.exe exec --json -s workspace-write -C <worktree> -c sandbox_workspace_write.network_access=false -o <run>/last.md "<prompt>"`.
97
+ Prompt is the last positional argument; **stdin is `ignore`**: `codex exec`
98
+ reads stdin whenever it is not a TTY and hangs on an open pipe ("Reading
99
+ additional input from stdin..."; plan-time probe hung 170 s with a pipe,
100
+ finished in 20 s with stdin closed; observed-live). `-o` writes the final
101
+ agent message to a file (observed-live: fixtures/live/codex/last.md).
102
+ - Output: JSONL events on stdout (source: fixtures/live/codex/out.log):
103
+ `thread.started{thread_id}`, `turn.started`, `item.started/item.completed{item{type:
104
+ agent_message|command_execution|error, text|command|message}}`,
105
+ `turn.completed{usage{input_tokens,cached_input_tokens,output_tokens,reasoning_output_tokens}}`.
106
+ An `item.type:"error"` is a non-fatal notice (observed: "Skill descriptions
107
+ were shortened…"). Leg reads `thread_id` as the session id, the last
108
+ `agent_message` text, `turn.completed` as the stop reason, and the error
109
+ messages.
110
+ - Exit codes:
111
+
112
+ | exit | meaning | source |
113
+ |------|---------|--------|
114
+ | 0 | turn completed | observed-live, fixtures/live/codex/run.json |
115
+ | (none) | hangs with an open stdin pipe until it is killed | observed at plan time; no artifact kept, the behaviour is pinned by test/lessons.test.mjs `codex-stdin-ignored` |
116
+ | non-zero | launch/auth/fatal | docs-only |
117
+
118
+ - Sandbox modes (`-s`): `read-only`, `workspace-write`, `danger-full-access`
119
+ (source: `codex exec --help`). Leg: default `workspace-write`; allowed
120
+ `read-only`, `workspace-write`; forbidden `danger-full-access`,
121
+ `--dangerously-bypass-approvals-and-sandbox`, `--dangerously-bypass-hook-trust`,
122
+ `--approve-for-me` (routes approvals through automatic review), `--full-auto`
123
+ (not present in 0.153.4 `exec --help`; kept on the forbidden list). Blast
124
+ radius is `-C <worktree>`; network off unless the chain entry sets
125
+ `network: true` (PROTOCOL sandbox ladder). `workspace-write` allowed the
126
+ write headless (observed-live).
127
+ - Resume: `codex exec resume [SESSION_ID] [PROMPT]` with `--last` for the newest
128
+ (source: `codex exec resume --help`). Leg's adapter emits
129
+ `exec … resume <thread_id> <prompt>`; docs-only until phase 5 exercises it.
130
+ - Limit signals: see § Limit signals (phase 4).
131
+
132
+ ## agy
133
+
134
+ - Version 1.2.0 (source: `agy --version`).
135
+ - Binary: `~/AppData/Local/agy/bin/agy.exe` (native); `LEG_AGY_BIN` overrides
136
+ (source: `where agy`; src/adapters/agy.mjs).
137
+ - Headless argv (exact, from cmd.txt):
138
+ `agy.exe -p "<working-directory preamble + prompt>" --output-format json --mode accept-edits --add-dir <worktree> --print-timeout 90m`;
139
+ stdin `ignore`.
140
+ - **agy does not work in the process cwd.** Attempt 1 (no `--add-dir`) exited
141
+ 0 and wrote `hello-agy.txt` under `~/.gemini/antigravity-cli/scratch/`
142
+ (observed-live, fixtures/live/agy/attempt-1-scratch-workspace.out.log).
143
+ Attempt 2 adds `--add-dir <worktree>` and prefixes the prompt with
144
+ "Working directory: <worktree>"; it wrote both files in the repo
145
+ (observed-live).
146
+ - `--print-timeout` defaults to 5m0s; Leg sets it from the kill timer in Go
147
+ duration syntax so the supervisor decides what a runaway is (source: `agy --help`).
148
+ - Output: one JSON object: `conversation_id`, `status:"SUCCESS"`, `response`,
149
+ `duration_seconds`, `num_turns`, `usage{input_tokens,output_tokens,thinking_tokens,cache_read_tokens,total_tokens}`
150
+ (observed-live, fixtures/live/agy/out.log). Leg reads `conversation_id` as
151
+ the session id, `response`, `status`.
152
+ - Exit codes:
153
+
154
+ | exit | meaning | source |
155
+ |------|---------|--------|
156
+ | 0 | `status: SUCCESS` | observed-live (both attempts; attempt 1 was a success in the wrong directory) |
157
+ | non-zero | fatal/auth | docs-only |
158
+
159
+ - Modes (`--mode`): `accept-edits`, `plan` (source: `agy --help`). Leg: default
160
+ `accept-edits`; forbidden `--dangerously-skip-permissions`. `accept-edits`
161
+ allowed the write headless (observed-live).
162
+ - Resume: `--conversation <id>` (by id) or `--continue` (most recent) (source:
163
+ `agy --help`; docs-only until phase 5).
164
+ - Limit signals: see § Limit signals (phase 4).
165
+
166
+ ## grok
167
+
168
+ - Version grok 0.2.51 (f4f85a649) [stable] (source: `grok --version`).
169
+ - Binary: `~/.grok/bin/grok.exe` (native; also an npm shim on PATH);
170
+ `LEG_GROK_BIN` overrides (source: `where grok`).
171
+ - Headless argv (from cmd.txt): `grok.exe -p "<prompt>" --output-format json --permission-mode acceptEdits`; stdin `ignore`.
172
+ `--prompt-file <path>` also exists (source: `grok --help`).
173
+ - Output: one JSON object `{text, stopReason, sessionId, requestId, thought}`
174
+ (observed-live, fixtures/live/grok/out.log).
175
+ - Exit codes:
176
+
177
+ | exit | meaning | source |
178
+ |------|---------|--------|
179
+ | 0 | printed JSON with `stopReason:"Cancelled"` and did no work: the CLI was not logged in, printed a device-code prompt on stderr (`https://accounts.x.ai/oauth2/device?user_code=…`, "Waiting for authorization...") and gave up after ~58 s | observed-live, fixtures/live/grok/err.log |
180
+
181
+ A zero exit with no DONE marker and no diff is exactly the `no_progress` class
182
+ the completion contract exists for.
183
+ - Permission modes: `default`, `acceptEdits`, `auto`, `dontAsk`,
184
+ `bypassPermissions`, `plan` (source: `grok --help`). Leg: default
185
+ `acceptEdits`; forbidden `bypassPermissions`, `--always-approve`.
186
+ - Login: `grok login` (source: `grok --help` Commands).
187
+ - **Verdict: not verified, no adapter registered.** `src/adapters/grok.mjs` is
188
+ built from `--help` and unit-tested for shape and forbidden flags, but stays
189
+ out of `src/adapters/index.mjs` until `grok login` has been completed on the
190
+ machine and `node scripts/probe.mjs --adapter grok --repo <toy>` passes.
191
+
192
+ ## Interactive taps
193
+
194
+ What `leg claude|codex|agy` reads while the real interactive CLI runs. Same
195
+ tagging rule: `observed-live 2026-09-11` means the build machine did it;
196
+ `docs-only` means the CLI's own source or documentation says so and Leg has
197
+ not seen it happen. Machine: Claude Code 2.1.268, codex-cli 0.153.4, agy 1.2.0.
198
+
199
+ Shared: the agent is spawned with stdio inherited and the user's arguments
200
+ passed through (`src/attach.mjs` `spawnSpec`); the child environment is
201
+ `sanitizeEnv(process.env, { interactive: true })` plus the account's config-dir
202
+ variable and `LEG_SESSION` (source: src/attach.mjs, src/env.mjs).
203
+
204
+ ### claude tap
205
+
206
+ - Attach: `claude <args> --settings <LEG_HOME>/sessions/<id>/claude-settings.json`
207
+ (source: src/attach.mjs `spawnSpec`; src/taps/claude.mjs `writeSettings`).
208
+ Hooks in a `--settings` file merge with the user's rather than replacing
209
+ them; `statusLine` is the one key that replaces, so Leg runs the user's own
210
+ command first (source: code.claude.com/docs/en/settings;
211
+ src/taps/claude.mjs `userStatusLine`). observed-live 2026-09-11: a Leg
212
+ session ran with every user hook still firing.
213
+ - Hooks written (source: src/taps/claude.mjs `settingsFor`): `SessionStart`,
214
+ `UserPromptSubmit`, `PostToolUse` with matcher `Edit|Write|MultiEdit|NotebookEdit`,
215
+ `Stop`, `StopFailure`, `SessionEnd`, each
216
+ `node <src>/hook.mjs claude-hook --session <id>` with a 20 s timeout.
217
+ observed-live 2026-09-11 (hook.log in the session directory).
218
+ - `autoContinueAtUsageLimit: false` in the same file, because Leg owns the
219
+ hand-off (source: src/taps/claude.mjs `settingsFor`).
220
+ - Usage: `GET https://api.anthropic.com/api/oauth/usage`
221
+ (`LEG_CLAUDE_USAGE_URL` overrides) with `Authorization: Bearer <accessToken>`
222
+ from `<CLAUDE_CONFIG_DIR>/.credentials.json` key `claudeAiOauth`, and header
223
+ `anthropic-beta: oauth-2025-04-20`; response fields `five_hour` and
224
+ `seven_day`, each `{ utilization, resets_at }`; polled every
225
+ `LEG_USAGE_POLL_MS` ms, default 60000 (source: src/taps/claude-usage.mjs).
226
+ observed-live 2026-09-11: real windows came back and were written to
227
+ `<LEG_HOME>/usage/claude--default.json`; a seven_day window at 93 %
228
+ raised the amber warning.
229
+ - The wall: `StopFailure` hook with `error: rate_limit` (source:
230
+ https://code.claude.com/docs/en/hooks#stopfailure).
231
+ **observed-live 2026-09-11** <!-- live:claude/rate_limit -->: a real
232
+ `StopFailure` with `error: rate_limit` (a 429 `rate_limit_error`) arrived
233
+ at 07:46:37Z and is kept, scrubbed, as
234
+ `fixtures/live/claude/limit-rate_limit.json` (src/live-capture.mjs). The
235
+ path can also be run live with `leg sessions simulate-limit` (the same
236
+ payload through `src/hook.mjs`, marked `leg_simulated`, never kept as
237
+ evidence).
238
+ - Status line, not usable on 2.1.268: a custom `statusLine` command passed
239
+ through `--settings`, and again through a project
240
+ `.claude/settings.local.json`, was not run at all when it was tried; an
241
+ `echo` command at both levels left the built-in status line in place while
242
+ hooks from the same `--settings` file fired (no artifact kept; note in the
243
+ src/taps/claude-usage.mjs header, 2026-09-11). Leg still writes the
244
+ `statusLine` entry, which records the
245
+ same `rate_limits.five_hour` / `seven_day` fields
246
+ (https://code.claude.com/docs/en/statusline), so the endpoint poll becomes a
247
+ fallback if a later build honours it.
248
+
249
+ ### codex tap
250
+
251
+ - Attach: nothing injected. `codex <args>` runs as it always does, and Leg
252
+ finds the session's rollout file afterwards (source: src/attach.mjs
253
+ `spawnSpec`). Reason: injecting a hook makes codex show its hooks-review
254
+ prompt on every Leg session.
255
+ - Which file: `<CODEX_HOME>/sessions/YYYY/MM/DD/rollout-*.jsonl`, matched on
256
+ `session_meta.payload.cwd` equal to the session's cwd with a birth time or
257
+ mtime at or after the spawn time, five seconds of slack (source:
258
+ src/taps/codex.mjs `findRollout`). observed-live 2026-09-11: the right
259
+ rollout was found for a real hand-off. The file's mtime is not a content
260
+ clock on Windows, six rollouts from that day had mtimes 7 s to 9 min
261
+ behind their last line, so nothing depends on the two matching.
262
+ - Usage: read-only app-server `account/rateLimits/read`, polled every 60
263
+ seconds by the board and active attach. Leg maps windows by duration,
264
+ including 300 minutes to 5h and 10080 minutes to 7d, rather than assuming
265
+ `primary` or `secondary` names. It sends no model turn and does not hardcode
266
+ a quota; only explicit backend availability clears a previous wall (sources:
267
+ `src/taps/codex.mjs`, `src/attach.mjs`, `src/server.mjs`, `src/usage.mjs`).
268
+ - The wall: `event_msg.task_complete.error` with
269
+ `codex_error_info: "usage_limit_exceeded"` and message "You've hit your usage
270
+ limit … try again at \<date>" (source:
271
+ github.com/openai/codex `codex-rs/protocol/src/error.rs`
272
+ `UsageLimitReachedError`). The event shape was read from local rollouts;
273
+ the error itself is **observed-live 2026-09-15** <!-- live:codex/usage_limit_exceeded -->.
274
+ A `task_complete.error` with `codex_error_info: usage_limit_exceeded` walled
275
+ a codex leg once, but no scrubbed payload was kept, so
276
+ `fixtures/live/codex/limit-usage_limit_exceeded.json` is still the slot for
277
+ the first one.
278
+ - Transcript: `response_item.message` with `role: user` and
279
+ `content[].type: input_text` for prompts; `output_text` and
280
+ `task_complete.last_agent_message` for assistant text (source:
281
+ src/taps/codex.mjs; observed-live 2026-09-11).
282
+ - Edited files: the `*** Add File:` / `*** Update File:` / `*** Delete File:`
283
+ lines of an `apply_patch` payload (source: src/taps/codex.mjs; observed-live
284
+ 2026-09-11).
285
+
286
+ ### agy tap
287
+
288
+ - Attach: `agy <args> --log-file <LEG_HOME>/sessions/<id>/agy.log` (source:
289
+ src/attach.mjs `spawnSpec`). agy 1.2.0 is a closed Go binary with no hook
290
+ surface.
291
+ - Usage: none. No percentage is written to any file agy owns; its own status
292
+ line fetches a quota summary from the backend and stores nothing (source:
293
+ src/taps/agy.mjs header).
294
+ - The wall: `RESOURCE_EXHAUSTED`, "it resets in %s", "out of quota", and
295
+ "quota exhausted/exceeded" in the log, plus a relative reset parsed out of
296
+ "resets in \<n>\<s|m|h|d>" (source: strings present in `agy.exe`;
297
+ src/taps/agy.mjs `scanLog`).
298
+ **docs-only** <!-- live:agy/agy-resource-exhausted -->:
299
+ `RESOURCE_EXHAUSTED (code 429): Individual quota reached … Resets in
300
+ 71h19m42s.` appeared in a session's `agy.log` at 08:02:42Z, `scanLog` read
301
+ the relative reset, and the agent was walled. No payload was kept, the
302
+ capture call in `src/attach.mjs` was added while that session's runner was
303
+ already running, so `fixtures/live/agy/limit-agy-resource-exhausted.json`
304
+ is still the slot for the next one.
305
+ - Prompts and conversation id: `~/.gemini/antigravity-cli/history.jsonl`, one
306
+ `{ display, timestamp, workspace, conversationId }` per prompt (source:
307
+ src/taps/agy.mjs `historyFile`; shape observed on disk, last written
308
+ 2026-09-09, the 2026-09-11 agy leg produced no history entry).
309
+ - One account only: agy 1.2.0 has no config-directory override, so
310
+ `LAYOUT.agy.env` is `null` and `accounts add agy` is refused (source:
311
+ src/accounts.mjs).
312
+
313
+ ### Usage store and the chooser
314
+
315
+ - `<LEG_HOME>/usage/<agent>--<account>.json`:
316
+ `{ five_hour: {pct, resets_at}, seven_day: {…}, limited_until,
317
+ limited_reason, source, updated_at }` (source: src/usage.mjs).
318
+ - Warning threshold `WARN_PCT`, default 85, from `LEG_WARN_PCT`; the warning
319
+ is an amber card, an event, and one terminal bell (source: src/usage.mjs,
320
+ src/attach.mjs).
321
+ - `markLimited()` takes the reset the CLI reported; failing that the soonest
322
+ known window reset; failing that now + 5 h (source: src/usage.mjs
323
+ `DEFAULT_LIMIT_S`).
324
+ - `candidates()` yields the other accounts of the same agent first, then the
325
+ remaining agents in the order claude, codex, agy, wrapping, so every option
326
+ is tried once. `chooseNext()` skips any option still walled and returns the
327
+ walled ones sorted by reset (source: src/usage.mjs).
328
+
329
+ ### Session store
330
+
331
+ `<LEG_HOME>/sessions/<id>/` holds `session.json`, `events.jsonl`,
332
+ `hook.log`, and, per agent, `claude-settings.json` or `agy.log`;
333
+ `control.json` appears only once the board has asked for a hand-off or an
334
+ end (source: src/sessions.mjs header, src/attach.mjs). Statuses and event
335
+ types are listed
336
+ in [VOCABULARY.md](VOCABULARY.md). Board routes: `GET /api/sessions`,
337
+ `GET /api/sessions/:id`, `POST /api/sessions/:id/handoff`,
338
+ `POST /api/sessions/:id/end`, `DELETE /api/sessions/:id`, with the list pushed
339
+ as the SSE `sessions` event (source: src/server.mjs, src/board/sessions.js).
340
+
341
+ ## Limit signals
342
+
343
+ Recorded in `fixtures/limits/<group>/<id>.json` and classified by
344
+ `src/limits.mjs` (`classify()`); the runner writes the outcome into
345
+ `run.json.outcome` with the matched `signal`. Every `limit` row below is
346
+ still **docs-only**: its text comes from the CLI's own documentation or
347
+ source, cited in "produced by". Real limits from all three agents were seen
348
+ on 2026-09-11, but only the claude payload was kept as evidence
349
+ (`fixtures/live/claude/limit-rate_limit.json`); the codex and agy ones went
350
+ to the session ledger, not to `fixtures/limits/`. The observed-live rows are
351
+ the non-limit cases the probes actually hit and the detector must never
352
+ mistake for a limit.
353
+
354
+ Outcome precedence (`src/limits.mjs`): spawn error → `launch_failed`; stderr
355
+ "another auth source is set" or an `auth` fixture → `auth_failed` (wins over
356
+ any limit text, even with exit 0); killed from the board → `killed`; kill timer
357
+ → `stalled`; exit 0 + `.leg/DONE` → `completed`; adapter-specific then
358
+ generic `limit` fixture → `limit`; `launch` fixture → `launch_failed`; exit 0 +
359
+ changes, no DONE → `incomplete`; exit 0, nothing changed → `no_progress`;
360
+ non-zero exit → `failed`. Every outcome except `completed`, `auth_failed` and
361
+ `killed` asks the chain to hand off.
362
+
363
+ <!-- limits-table:start -->
364
+ Generated by `node scripts/limits-table.mjs` from 21 fixtures (4 observed-live, 17 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.
365
+
366
+ | id | adapter | class | where | source | text (excerpt) | produced by |
367
+ |----|---------|-------|-------|--------|----------------|-------------|
368
+ | 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 |
369
+ | claude-budget-limit | claude | budget | any | **docs-only** | Budget limit reached | https://code.claude.com/docs/en/cli-reference (--max-budget-usd) |
370
+ | 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") |
371
+ | 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") |
372
+ | 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) |
373
+ | claude-weekly-limit | claude | limit | any | **docs-only** | You’ve hit your weekly limit | https://code.claude.com/docs/en/costs (section: When a developer asks about a limit) |
374
+ | codex-quota-exceeded | codex | limit | any | **docs-only** | quota exceeded | github.com/openai/codex codex-rs/response-debug-context/src/lib.rs (ApiError::QuotaExceeded => "quota exceeded") |
375
+ | codex-rate-limit-exceeded | codex | limit | any | **docs-only** | rate limit exceeded: | github.com/openai/codex codex-rs/codex-api/src/error.rs (#[error("rate limit exceeded: {message}")]) |
376
+ | codex-skills-notice | codex | info | stdout | **observed-live** | Skill descriptions were shortened to fit the skills context budget. Codex can still see ev | fixtures/live/codex/out.log (item.completed, item.type=error) from scripts/probe.mjs --adapter codex, 2026-09-10 |
377
+ | codex-usage-limit-reached | codex | limit | any | **docs-only** | Usage limit reached. You've reached your usage limit. Increase your limits to continue | github.com/openai/codex codex-rs/tui/src/chatwidget/turn_runtime.rs (WorkspaceOwnerUsageLimitReached) |
378
+ | codex-usage-limit | codex | limit | any | **docs-only** | You’ve hit your usage limit for {limit_name}. Switch to another model now, | github.com/openai/codex codex-rs/protocol/src/error.rs (gh search code "usage limit" --repo openai/codex, 2026-09-10) |
379
+ | generic-429 | * | limit | any | **docs-only** | 429 Too Many Requests | generic HTTP matcher (429 Too Many Requests); lowest priority |
380
+ | generic-overloaded | * | limit | any | **docs-only** | overloaded_error | generic matcher (Anthropic API 529 overloaded_error); lowest priority |
381
+ | generic-quota | * | limit | any | **docs-only** | quota | generic matcher; lowest priority |
382
+ | generic-rate-limit | * | limit | any | **docs-only** | rate limit | generic matcher; lowest priority |
383
+ | generic-resource-exhausted | * | limit | any | **docs-only** | RESOURCE_EXHAUSTED | generic matcher (gRPC RESOURCE_EXHAUSTED); lowest priority |
384
+ | generic-usage-limit | * | limit | any | **docs-only** | usage limit | generic matcher; lowest priority |
385
+ | 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) |
386
+ | 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 |
387
+ | compile-error | * | info | stderr | **docs-only** | SyntaxError: Unexpected token ) at compileSourceTextModule (node:internal/modules/esm/ | synthetic negative fixture (a crashed agent is not a limit) |
388
+ | empty-stdout-exit-0 | * | info | stdout | **observed-live** | | fixtures/live/grok (exit 0, no work): silence is not a limit |
389
+ <!-- limits-table:end -->
390
+
391
+ ## Runner facts that apply to every CLI (observed-live in phase 2/3 tests)
392
+
393
+ - The supervisor is detached (`detached: true, windowsHide: true`) and survives
394
+ the launcher; `run.json` moves `launching → running → exited|killed|failed`.
395
+ - Kill is `taskkill /PID <pid> /T /F` on Windows with one verify-retry
396
+ (`LEG_TIMERS_MS=notify,kill,verify`, default 30 min / 90 min / 30 s).
397
+ - No adapter ever sets `shell: true`; every spawn is `spawn(bin, argv)` with a
398
+ native exe or `node <entry.js>` (test/lessons.test.mjs `no-shell-spawn`).
399
+ - A chain entry naming a forbidden mode or flag makes `argv()` throw
400
+ `forbidden flag: …`; the supervisor records the refusal and exits 13 without
401
+ spawning (test/adapters.test.mjs, test/runner.test.mjs).