Leg

Adapters

Two things per agent: what Leg reads from an interactive session (leg claude|codex|agy|grok), and the headless argv the v0.1 pipeline spawns. Every fact here was written against src/taps/*.mjs, src/attach.mjs and src/adapters/*.mjs; the evidence trail, including which lines an artifact backs, is cli-contracts.md.

An agent is never run through a shell. Interactive sessions are spawn(bin, argv, { stdio: 'inherit' }) (src/attach.mjs spawnSpec); headless legs are spawn(spec.bin, spec.args, …) (src/runner.mjs). Both strip the API-key and base-URL variables and the Claude Code nested-session markers from the child environment (src/env.mjs sanitizeEnv).

What Leg reads from each agent

Nothing is screen-scraped. Each tap was read from the CLI's own source or documentation, then checked on a real machine on 2026-09-11 (Claude Code 2.1.268, codex-cli 0.153.4, agy 1.2.0). Lines that a live run or a fixture backs say observed-live; lines read only from a CLI's source or documentation say docs-only.

claude

codex

agy

grok

Resume prompt per agent

After a hand-off the next agent starts in the same terminal with the pointer prompt as its first positional argument: claude "<prompt>", codex "<prompt>", agy -i "<prompt>", grok "<prompt>" (src/attach.mjs spawnSpec).

Headless adapters (the v0.1 pipeline)

These are what a pipeline card's chain spawns. Unchanged since 0.2.0.

claude (headless)

codex (headless)

agy (headless)

fake (and fake-claude / fake-codex / fake-agy / fake-nostdin)

grok (headless)

Custom adapters

Any other coding-agent CLI becomes a card adapter through a JSON spec in $LEG_HOME/adapters/<name>.json, with no code in this package. This is the open end of the chain: claude, codex, agy and grok ship with taps and a probe, and anything else joins as "argv in, JSON out".

A custom adapter runs cards: headless, in a worktree, handing off like any other leg. It is not an interactive leg <agent> terminal, because that needs a usage tap and a wall signal, which only the four built-ins expose.

leg adapter template --name muse > muse.json   # a starter spec
leg adapter add muse.json                      # validate and install it
leg adapter check muse                         # the exact command a leg would run
leg card add --repo <path> --task "<t>" --chain muse,claude --queue
leg adapter list | show muse | rm muse

The spec:

{
  "name": "muse",
  "bin": "muse",
  "stdin": "ignore",
  "args": ["run", "--json",
           ["--dir", "{{cwd}}"],
           ["--model", "{{model}}"],
           "--prompt-file", "{{promptFile}}"],
  "modes": { "default": "auto", "allowed": ["auto", "readonly"] },
  "forbiddenFlags": ["--unsafe"],
  "result": { "format": "json", "sessionId": "session_id",
              "message": "result", "stopReason": "stop_reason" }
}

A broken spec is reported, never thrown: leg adapter list names the file and the reason, and the board, the scheduler and leg card add carry on without it. A spec may not take a built-in's name. The directory is re-read whenever the list is asked for, keyed on its entries, so a spec added while the board is up appears in the New card form without a restart.

How to add a built-in adapter

An adapter is a plain object (see src/adapters/common.mjs for the shared helpers, src/adapters/fake.mjs for the simplest full example):

{
  name: 'mycli',
  stdin: 'pipe' | 'ignore',
  modes: { default: 'acceptEdits', allowed: ['acceptEdits', 'plan'] },
  forbiddenFlags: ['--any-bypass-flag'],
  emulates: null,             // optional: another registered adapter's name
  resolve() { return { bin, viaNode, entry } },
  argv(opts) { return { bin, args } },   // opts: mode, maxTurns, resume, cwd, prompt, model, ...
  env(base) { return sanitizeEnv(base) },   // from src/env.mjs, always
  parseResult(text) { return { session_id, last_message, stop_reason, raw } | null },
}
  1. Write src/adapters/<name>.mjs exporting that shape as default. argv() must call assertAllowed(adapter, opts) (from src/adapters/common.mjs) first, so a forbidden mode or flag throws before anything spawns.

  2. Add an entry to REGISTRY in src/adapters/index.mjs: <name>: { path: './<name>.mjs' }.

  3. Add its limit/auth/launch signal fixtures under fixtures/limits/ (see the existing ones for the JSON shape src/limits.mjs expects: id, adapter, source, produced_by, where, text, classification).

  4. Run the probe script against a real login before trusting it:

    node scripts/probe.mjs --adapter <name> --repo <existing-git-repo> [--mode <m>] [--timeout-s 300]
    

    It runs one real tiny task (write a file, write .leg/DONE) through the same runner a card uses, and prints probe <name>: exit=<code> file=<yes|no> done=<yes|no> auth_source=<yes|no> seconds=<n>. Keep the evidence it produces under fixtures/live/<name>/ and cite it in cli-contracts.md, the way every existing adapter's section does.

An interactive tap is a separate, larger job: a new agent needs a src/taps/<name>.mjs that answers three questions (what are the usage percentages, what does the wall look like, what are the prompts and edited files) and a branch in src/attach.mjs spawnSpec.

See also


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.