LegCli

Adapters

Two things per agent: what Leg reads from an interactive session (leg claude|codex|agy), 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

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>" (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 (built, not registered)

How to add an 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.