Documentation
¶
Overview ¶
Package agentcli abstracts the headless agent CLI the quality-management spine shells out to for isolated reviews and summaries. satellites hardcoded claude's flag surface (claude -p --append-system-prompt …); satelle routes every subprocess through a Runner driven by a CONFIG TEMPLATE, so the operator picks their agent and its exact argv in `.satelle/agents.toml` and no reviewer code names a binary or a flag directly.
A command string is a command template: the first token is the binary, the rest are argv tokens that may carry the placeholders {system}, {tools}, {model}, {settings}, and {payload}. At call time satelle substitutes each placeholder into its own argv token (so a multi-line system prompt or a JSON payload stays a single argument). The work-item payload is ALWAYS also written to the child stdin (dual delivery): stdin-first CLIs (claude -p) keep using stdin alone; argv-first CLIs (e.g. grok -p {payload}) include {payload} in the template.
agents.toml bindings carry a FULL command template (or "in-loop"/empty). Bare single-token CLI names are NOT accepted on the agents.toml path — the operator must see the real argv. DefaultClaudeCommand / DefaultGrokCommand are the canonical templates init seeds and migrate expands into; NewRunner still generates those templates for init/migrate/detection only.
Index ¶
Constants ¶
const ( CLIClaude = "claude" CLICodex = "codex" CLIGrok = "grok" )
Supported agent CLI identifiers.
const ( InterfaceCommand = "command" InterfaceACP = "acp" )
Interface names for RunnerFromBinding (mirror config.Interface* without importing config — agentcli must not depend on the config package).
const DefaultClaudeCommand = "" /* 166-byte string literal not displayed */
DefaultClaudeCommand is the claude preset template — the satellites gate argv reproduced behind the template seam, hardened with a --disallowedTools denylist so the reviewer's grant is a CEILING (deny wins over allow) over any permissions the repo's .claude/settings.json would otherwise inherit. {system} is the gate/skill body, {tools} the allow-grant, {model} the optional model (dropped, with its flag, when unset). The work-item body rides on stdin only — this template does NOT place {payload} on -p, so Claude is not double-fed; the {payload} placeholder is still available for other harness templates (sty_5cf4a1fb dual delivery).
The denylist keeps the work-tree MUTATORS off (Write, Edit, NotebookEdit, and Bash — a shell redirect writes files whatever the user's ~/.claude permission allows; sty_892517e7) so a reviewer can never modify the repo it judges — the read-only invariant. The reviewer's default allow-grant ({tools}) is read-only (Read, Grep, Glob) and needs no shell: the substrate it reasons about is materialised as markdown under .satelle, so it reads it directly. A repo MAY widen the grant in .satelle/agents.toml (transparently), but the default is read-only.
const DefaultGrokCommand = "" /* 251-byte string literal not displayed */
DefaultGrokCommand is the grok preset template — the proven dogfood reviewer command (this repo's own [reviewer]) behind the single-token "grok" preset. Grok is argv-first, so the work-item rides on -p {payload} (and also stdin, dual delivery). Unlike claude, the read-only grant is BAKED IN (--tools with grok's own tool names read_file,grep,list_dir, plus --deny on every mutator) rather than drawn from {tools}: the reviewer's default {tools} is Claude tool names (Read,Grep,Glob), which grok does not understand — feeding them to grok was the exact failure this preset removes. To widen grok's grant, author a full command template instead of the bare preset. {model} is dropped (with -m) when unset, so grok falls back to its own default unless the binding pins one (e.g. grok-4.5).
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Request ¶
type Request struct {
SystemPrompt string // {system}: appended as the system prompt (the gate/skill body)
// Payload is the work-item / transition JSON. Dual delivery (sty_5cf4a1fb):
// always written to the subprocess stdin, and substituted into {payload} when
// that token appears in the harness template (one argv token). Empty Payload
// is still delivered (empty stdin + empty argv token if {payload} is present)
// — unlike {model}/{settings}, empty does not drop a preceding flag.
Payload string
AllowedTools string // {tools}: comma-separated tool grant
Model string // {model}: optional model override; "" drops the placeholder
// Effort is {effort}: optional reasoning/thinking level (sty_657f77b9);
// "" drops the placeholder AND a directly preceding flag (like Model).
Effort string
// Settings is {settings}: a pre-marshalled JSON object mirroring claude's
// settings.local.json schema (env/model/permissions), already ${VAR}-resolved
// by the caller (config.ResolveAgentEnvs / agentstep.buildRequest) — agentcli
// never sees the raw map, only this JSON string, like Payload. "" drops the
// placeholder AND a directly preceding flag, exactly like an empty Model, so a
// binding with no settings emits no --settings arg. Passed INLINE (no
// temp-file), so a secret it carries never touches disk. Values MAY be
// secrets, so like Env it is never included in Command() evidence, logs, or
// error strings.
Settings string
Dir string // working directory for the subprocess
// Env sets environment variables on the subprocess, layered onto the parent
// env with these keys winning (composeEnv). Already ${VAR}-resolved by the
// caller (config.ResolveAgentEnvs). Values MAY be secrets (an API token), so
// Env is NEVER included in Command() evidence, logs, or error strings
// (sty_001558ce).
Env map[string]string
// Sink, when non-nil, receives a live TEE of the subprocess's stdout/stderr AS
// IT RUNS (stderr lines prefixed "[stderr] "), so a caller can write it to a
// tailable per-dispatch log an operator watches while a long dispatch runs —
// the only way to see a hang or an approaching timeout before the SIGKILL
// (sty_0aa67b7f). It never affects the accumulated bytes runProcess returns for
// the final JSON/usage parse; nil is a no-op (the default, unchanged behaviour).
// Sink.Write errors are ignored (best-effort, like the reviewer/executor logs).
Sink io.Writer
}
Request is one headless agent invocation.
type Runner ¶
type Runner interface {
// Name reports the agent CLI identifier (the template's binary).
Name() string
// Command reports the resolved command/harness template (binary + argv with the
// {system}/{tools}/{model}/{settings}/{payload} placeholders intact) — a
// concise, body-free description of HOW the agent is invoked, recorded as
// invocation evidence (sty_fb3e0873). It never expands the rubric body or the
// payload bytes (the literal {payload} token stays in the string when present).
Command() string
// Run executes the agent over req and returns its raw stdout.
Run(ctx context.Context, req Request) ([]byte, error)
}
Runner invokes an agent CLI headlessly and returns its stdout.
func NewRunner ¶
NewRunner returns the Runner for a bare CLI NAME — the preset. An empty name defaults to claude; "grok" expands to the grok preset; "codex" is the not-yet-mapped stub; an unknown name errors. Callers with a full command template use RunnerFromCommand instead.
func RunnerFromBinding ¶ added in v0.0.275
RunnerFromBinding resolves an agents.toml transport + command to a Runner (epic:agent-dispatch-transport). iface is "command" (default when empty) or "acp".
- command / empty: same as RunnerFromCommand (full argv template; in-loop → nil).
- acp: spawn line only (no {system}/{payload} substitution on argv); ACP session protocol carries system/payload. Does not silently fall back to command.
func RunnerFromCommand ¶ added in v0.0.180
RunnerFromCommand resolves an agents-layer command binding to a Runner. An empty or "in-loop" command returns (nil, nil): no agent-CLI runner, so the caller keeps its configured default. A SINGLE non-in-loop token is rejected (bare CLI presets removed — write a full command template, or run satelle init to migrate). A MULTI-token command is a literal command template: the first token is the binary, the rest the argv template.
type UsageResult ¶ added in v0.0.132
UsageResult is the cost of one agent invocation — token counts and, when the caller times the run, its wall-clock duration. Populated from a machine-readable agent envelope (claude's `--output-format json`); a plain-text harness yields the zero value, so cost capture degrades gracefully (sty_a699ad14). It never carries the env or any secret — only numbers.
func UnwrapUsage ¶ added in v0.0.132
func UnwrapUsage(stdout []byte) ([]byte, UsageResult)
UnwrapUsage splits an agent's raw stdout into the INNER result text (what verdict parsing consumes, unchanged) and its token UsageResult. Recognized envelopes:
- Claude `--output-format json`: unwraps `.result` and captures `.usage`
- Grok `--output-format json`: unwraps `.text` (usage zero when absent)
Otherwise stdout is returned verbatim with a zero UsageResult — plain-text harnesses keep working and simply report no cost. Duration is set by the caller (UnwrapUsage measures only tokens).