Imported from fotoetienne/gru (
docs/AGENTS.md). Install upstream withnpx skills add fotoetienne/gru --skill docs. Copyright stays with the author.
Agent Backends
Gru uses a pluggable agent architecture. Each backend implements the AgentBackend trait, which normalizes different CLI tools into a common event stream that Gru can monitor, log, and act on.
Available Backends
| Backend | CLI Tool | Flag Value | Status |
|---|---|---|---|
| Claude Code | claude |
--agent claude |
Default |
| OpenAI Codex | codex |
--agent codex |
Supported |
| Pi | pi |
--agent pi |
Supported |
Claude Code (default)
Claude Code is the default backend.
Install
npm install -g @anthropic-ai/claude-code
Verify
claude --version
claude --help
Configure
No configuration is required — Gru uses Claude Code by default. Optionally override the binary path or model in ~/.gru/config.toml:
[agent.claude]
binary = "/usr/local/bin/claude"
model = "claude-opus-5-5" # e.g. "claude-opus-5-5" or "sonnet"
Both keys are optional and independent. When model is unset, the Claude Code CLI uses its
own default. model applies to every Claude invocation Gru makes, including gru chat and
gru pm/gru tpm, not just gru do/gru lab.
Interactive sessions (gru chat, gru pm, gru tpm) use:
claude --system-prompt "<role/project prompt>" [--model <model>] [-- "<initial prompt>"]
How Gru Uses It
Gru spawns Claude Code in non-interactive mode with stream JSON output:
claude --print --verbose --session-id <uuid> --output-format stream-json --dangerously-skip-permissions --include-partial-messages [--model <model>] "<prompt>"
Key flags:
--print— non-interactive (prints to stdout and exits)--verbose— include tool calls in output--output-format stream-json— real-time event stream--dangerously-skip-permissions— autonomous operation--session-id <uuid>— maintain context across resumes--model <model>— only added when[agent.claude]setsmodelin config
OpenAI Codex
Codex CLI is an alternative backend using OpenAI models.
Install
npm install -g @openai/codex
Authenticate
Set your OpenAI API key:
export OPENAI_API_KEY="sk-..."
Verify
codex --version
codex --help
How Gru Uses It
Gru spawns Codex in full-auto mode with JSON output:
codex exec --json --full-auto "<prompt>"
Resume support uses:
codex exec resume --last --json --full-auto "<prompt>"
Note: Codex does not support interactive sessions. gru attach will not work with Codex minions, and gru chat/gru pm/gru tpm --agent codex fail with an explanatory error rather than spawning anything — the Codex CLI has no interactive entry point that accepts a custom system prompt. Codex also ignores the session_id parameter — it relies on its own session persistence for both new and resumed sessions.
Optionally override the binary path in ~/.gru/config.toml:
[agent.codex]
binary = "/usr/local/bin/codex"
Pi
Pi is a coding agent CLI (pi) with a pluggable
provider model — it supports many model providers, configured on the Pi side rather than by Gru.
This backend's event parsing (src/pi_backend.rs) was implemented and verified against a
particular Pi distribution's JSON event schema (session/agent_start, turn_start,
tool_execution_start/_end with toolCallId/toolName, turn_end usage, etc.). If your
pi resolves to a build with a different event shape, unrecognized lines are silently
skipped rather than erroring — so a mismatch shows up as missing tool/progress tracking, not
a crash. If progress output is empty or tool calls never appear, compare your pi's
--mode json output against the event names above.
Install
See pi-mono for install options (npm
@earendil-works/pi-coding-agent). Some environments distribute Pi through a wrapper or
internal package rather than a plain pi on $PATH; use [agent.pi].binary in
~/.gru/config.toml to point Gru at it. Model selection can also optionally be driven
from Gru's config — all of these keys live in the same [agent.pi] table:
[agent.pi]
binary = "/usr/local/bin/pi"
model = "anthropic/claude-sonnet-5" # "provider/id", optionally with a ":<thinking>" suffix
thinking = "high" # off | minimal | low | medium | high | xhigh | max
Every key is optional and independent — set only the ones you need. Authentication
belongs to Pi's own configuration, not Gru's — Gru does not manage provider credentials.
When model/thinking are unset, Pi uses whatever it is already configured for.
Verify
pi --version
pi --help
How Gru Uses It
Gru spawns Pi in headless mode with JSON output:
pi -p --mode json --session-id <uuid> [--model <model>] [--thinking <level>] "<prompt>"
--model/--thinking are only added when [agent.pi] sets them in config, and are
always placed before the prompt argument in case Pi's -p positional is greedy.
Resume uses the same --session-id with a new prompt.
Interactive resume (for gru attach) drops -p and --mode json, keeping --session-id, since Pi's TUI supports resuming a session with full history — unlike Codex.
Fresh interactive sessions (gru chat, gru pm, gru tpm) use Pi's TUI with a custom system prompt:
pi --system-prompt "<role/project prompt>" --no-approve [--model <model>] [--thinking <level>] [-- "<initial prompt>"]
There is no --dangerously-skip-permissions equivalent for Pi; bash and edit tool calls run without approval prompts by default under -p.
Selecting a Backend
Per-command
Use the --agent flag on any command that spawns an agent:
gru do 42 --agent codex
gru review 42 --agent codex
gru prompt my-prompt --agent codex
gru chat --agent pi
gru pm --agent pi "write a PRD for hooks"
gru tpm --agent pi
As default
Set the default in ~/.gru/config.toml:
[agent]
default = "codex"
The --agent flag always overrides the config default.
Feature Comparison
| Feature | Claude Code | Codex | Pi |
|---|---|---|---|
Autonomous work (gru do) |
Yes | Yes | Yes |
PR review (gru review) |
Yes | Yes | Yes |
Custom prompts (gru prompt) |
Yes | Yes | Yes |
Session resume (gru resume) |
Yes | Yes (non-interactive) | Yes |
Interactive attach (gru attach) |
Yes | No | Yes |
Interactive sessions (gru chat, gru pm, gru tpm) |
Yes | No | Yes |
| Token usage tracking | Yes | Yes | Yes |
| Stream monitoring | Yes | Yes | Yes |
Adding a New Backend
To add a new agent backend:
- Create
src/<name>_backend.rsimplementing theAgentBackendtrait fromsrc/agent.rs - Declare the module in
src/main.rswithmod <name>_backend;(see the existingmod claude_backend;/mod codex_backend;lines) — without this,crate::<name>_backenddoesn't exist and nothing else here will compile - Register it in
src/agent_registry.rs:- Import the backend type
- Add the name to
AVAILABLE_AGENTS - Add a matching arm in
construct_backend()— this is the single source of truth that bothresolve_backend()andall_process_names()route through. A name added toAVAILABLE_AGENTSwithout aconstruct_backendarm compiles fine but breaks at runtime in two different ways, neither of which is a clean error:resolve_backend()passes theAVAILABLE_AGENTScheck and then panics on the.expect()aroundconstruct_backend'sNoneresult, whileall_process_names()(used bygru stop's process-scan fallback) silently drops that backend's process names via itsfilter_mapwith no error at all. No compile error either way.
- Map the backend's output format to
AgentEventvariants inparse_events() - Update the
docommand's--agenthelp string insrc/main.rs("Agent backend to use (claude, codex). Defaults to claude.") — it enumerates backends by name but isn't derived fromAVAILABLE_AGENTS, so a new backend added without touching it leaves--helpoutput stale. Thereview/promptcommands' help ("Agent backend to use (e.g., 'claude').") is a non-exhaustive example, not an enumeration, so it doesn't need updating for each new backend - If the backend supports interactive sessions, update the three
--agenthelp strings forchat/pm/tpminsrc/main.rs(they enumerate"(claude, pi)") and the suggestion list ininteractive_unsupported_error()(src/agent_registry.rs). Like thedocommand's help, all four are hand-maintained rather than derived from a capability query - (Optional) Implement
install_url()so a "binary not found" spawn failure points at install instructions
The AgentBackend trait (src/agent.rs) currently has fourteen methods:
name()— human-readable identifierprocess_names()— process name(s) to match forgru stop's pgrep fallbackbuild_command()— construct the CLI command for a new sessionparse_events()— convert stdout lines to normalizedAgentEventsbuild_resume_command()— required to implement (no default body); returnNonefrom it if the backend doesn't support resume,Some(...)otherwisebuild_interactive_resume_command()— required to implement (no default body); returnNonefrom it to disable attach supportbuild_interactive_command()— required to implement (no default body, deliberately mirroringbuild_interactive_resume_commandso a new backend has to make an explicit choice); construct a fresh interactive session with a custom system prompt forgru chat/gru pm/gru tpm, or returnNoneto opt out (callers then surface an actionable error naming the command and agent). If you implement it, also update the suggested-agent list ininteractive_unsupported_error()(src/agent_registry.rs) — like thedocommand's--agenthelp, it enumerates backends by hand rather than deriving thembuild_oneshot_command()— construct a single-turn, plain-text command (e.g. for the merge-readiness judge)build_ci_fix_command()— construct a backend-specific streaming-event command (matching whatever formatparse_events()expects, e.g. Claude'sstream-jsonor Codex's JSONL) for stateless CI-fix invocationsyolo_args()— (optional) CLI args to bypass interactive permission prompts forgru attach --yolo; defaults to an emptyVecfinal_usage()— (optional) recover backend-internal accumulated usage on stream EOF when noFinished { usage: Some(_) }was seen; defaults toNone. See the token usage convention below (#914)install_url()— (optional) where to install the backend's CLI; appended tospawn_error_context's message so a missing binary points at install docs. Defaults toNonereset_usage()— (optional) clear backend-internal accumulated usage; called unconditionally by the runner before spawning each new invocation's process. No-op by default. See the token usage convention below (#914)sanitize_oneshot_output()— (optional) strip launcher/wrapper chatter frombuild_oneshot_command's stdout before a consumer treats it as the agent's plain-text answer (seePiBackend). Identity by default
Convention: new trait methods should carry a default body so existing backends keep compiling without changes, as yolo_args() did when added in #911. process_names() broke this convention when added in #912 (no default body), which meant every existing backend had to be updated in the same change — do this deliberately, not by accident.
Token usage convention (#914): accumulate_token_usage() in src/agent_runner.rs reads different fields depending on the event: Started contributes input + both cache fields only (no output_tokens); MessageComplete contributes output_tokens only; Finished unconditionally contributes all four fields (input, output, both cache). If your backend reports input/cache usage per-turn rather than once at session start (as Pi and Codex do), do not add it to MessageComplete's usage — that path is shared by every backend and would double-count or misattribute input tokens for backends that report them elsewhere (e.g. Claude's Started event). Instead, accumulate per-turn input/cache totals in backend-internal state (see PiBackend/CodexBackend's Mutex<TokenUsage> field — safe without real concurrency since one backend instance drives one session's parse_events() calls sequentially) and emit the running totals once in a session-end Finished event, with output_tokens: 0 in that event since output is already accumulated per-turn via MessageComplete. A backend instance is reused across independent invocations (e.g. the CI-fix retry loop in src/ci.rs drives multiple attempts through the same &dyn AgentBackend), so implement AgentBackend::reset_usage() to clear this state — run_agent_with_stream_monitoring calls it unconditionally before spawning each invocation's process. Don't reset on a stream-start event instead (Pi's session/agent_start, Codex's thread.started): a process that crashes or fails auth before ever emitting that event would leave a prior invocation's totals in place. If your session-end event is inferred rather than confirmed against real CLI output (as Codex's thread.completed currently is), also implement AgentBackend::final_usage() to return the same accumulated totals — run_agent_with_stream_monitoring calls it once the stream hits EOF if no Finished { usage: Some(_) } was observed, so a wrong event name degrades to "recovered a different way" instead of "silently zero".
