LegCli

CLI contracts

What Leg knows about each coding-agent CLI, where every fact came from, and which facts were observed live on the build machine versus read from --help or docs. Two contracts per CLI: the headless argv a pipeline leg spawns (§ per CLI below), and the interactive tap leg <agent> reads (§ Interactive taps). Evidence: fixtures/help/<cli>.txt (raw --help), fixtures/live/<cli>/ (one real tiny task per CLI, run through src/runner.mjs launch on 2026-09-11 UTC, the evening of 2026-09-10 locally; paths under the local home directory are replaced with ~), and the interactive checks run on 2026-09-11.

Every fact line ends with (source: …). observed-live means the build machine did it; docs-only means the CLI's own --help or documentation says so and Leg has not seen it happen.

Probe task (identical for every CLI, scripts/probe.mjs): "Create a file named hello-.txt in the current directory containing exactly the word hi. Then create the directory .leg if it is missing and write the file .leg/DONE containing the single line: done. Do nothing else. Do not ask questions."

CLI version probe result adapter
claude 2.1.268 exit 0, file yes, DONE yes, 20 s src/adapters/claude.mjs
codex codex-cli 0.153.4 exit 0, file yes, DONE yes, 28 s src/adapters/codex.mjs
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
grok 0.2.51 exit 0, stopReason: Cancelled, no file: not logged in (device-code prompt) src/adapters/grok.mjs exists, NOT registered

The auth-source check: the build shell carried ANTHROPIC_API_KEY and OPENAI_API_KEY (injected by the shell profile). Every adapter's env() deletes them (src/env.mjs). No fixture contains "another auth source" (grep -ril "another auth source" fixtures/live is empty), claude's and agy's stderr are 0 bytes, and codex's is the one stdin notice. observed-live.

claude

codex

agy

grok

Interactive taps

What leg claude|codex|agy reads while the real interactive CLI runs. Same tagging rule: observed-live 2026-09-11 means the build machine did it; docs-only means the CLI's own source or documentation says so and Leg has not seen it happen. Machine: Claude Code 2.1.268, codex-cli 0.153.4, agy 1.2.0.

Shared: the agent is spawned with stdio inherited and the user's arguments passed through (src/attach.mjs spawnSpec); the child environment is sanitizeEnv(process.env, { interactive: true }) plus the account's config-dir variable and LEG_SESSION (source: src/attach.mjs, src/env.mjs).

claude tap

codex tap

agy tap

Usage store and the chooser

Session store

<LEG_HOME>/sessions/<id>/ holds session.json, events.jsonl, hook.log, and, per agent, claude-settings.json or agy.log; control.json appears only once the board has asked for a hand-off or an end (source: src/sessions.mjs header, src/attach.mjs). Statuses and event types are listed in VOCABULARY.md. Board routes: GET /api/sessions, GET /api/sessions/:id, POST /api/sessions/:id/handoff, POST /api/sessions/:id/end, DELETE /api/sessions/:id, with the list pushed as the SSE sessions event (source: src/server.mjs, src/board/sessions.js).

Limit signals

Recorded in fixtures/limits/<group>/<id>.json and classified by src/limits.mjs (classify()); the runner writes the outcome into run.json.outcome with the matched signal. Every limit row below is still docs-only: its text comes from the CLI's own documentation or source, cited in "produced by". Real limits from all three agents were seen on 2026-09-11, but only the claude payload was kept as evidence (fixtures/live/claude/limit-rate_limit.json); the codex and agy ones went to the session ledger, not to fixtures/limits/. The observed-live rows are the non-limit cases the probes actually hit and the detector must never mistake for a limit.

Outcome precedence (src/limits.mjs): spawn error → launch_failed; stderr "another auth source is set" or an auth fixture → auth_failed (wins over any limit text, even with exit 0); killed from the board → killed; kill timer → stalled; exit 0 + .leg/DONEcompleted; adapter-specific then generic limit fixture → limit; launch fixture → launch_failed; exit 0 + changes, no DONE → incomplete; exit 0, nothing changed → no_progress; non-zero exit → failed. Every outcome except completed, auth_failed and killed asks the chain to hand off.

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.

id adapter class where source text (excerpt) produced by
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
claude-budget-limit claude budget any docs-only Budget limit reached https://code.claude.com/docs/en/cli-reference (--max-budget-usd)
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")
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")
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)
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)
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")
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}")])
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
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)
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)
generic-429 * limit any docs-only 429 Too Many Requests generic HTTP matcher (429 Too Many Requests); lowest priority
generic-overloaded * limit any docs-only overloaded_error generic matcher (Anthropic API 529 overloaded_error); lowest priority
generic-quota * limit any docs-only quota generic matcher; lowest priority
generic-rate-limit * limit any docs-only rate limit generic matcher; lowest priority
generic-resource-exhausted * limit any docs-only RESOURCE_EXHAUSTED generic matcher (gRPC RESOURCE_EXHAUSTED); lowest priority
generic-usage-limit * limit any docs-only usage limit generic matcher; lowest priority
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)
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
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)
empty-stdout-exit-0 * info stdout observed-live fixtures/live/grok (exit 0, no work): silence is not a limit

Runner facts that apply to every CLI (observed-live in phase 2/3 tests)


Leg is commercial, source-available software by Wes Sander. The source you run ships in the npm package. Questions or a refund: legcli@practicalsystems.io.