agentcli

package
v0.0.400 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2026 License: MIT Imports: 16 Imported by: 0

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/workflows/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 / DefaultCodexExecCommand are the canonical command templates init/migrate expand into; DefaultCodexACPCommand is the preferred Codex spawn for interface=acp (sty_3b4909bb). NewRunner generates command templates only (init/migrate/detection).

Index

Constants

View Source
const (
	CLIClaude = "claude"
	CLICodex  = "codex"
	CLIGrok   = "grok"
)

Supported agent CLI identifiers.

View Source
const (
	InterfaceCommand = "command"
	InterfaceACP     = "acp"
)

Interface names for RunnerFromBinding (mirror config.Interface* without importing config — agentcli must not depend on the config package).

View Source
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/workflows/agents.toml (transparently), but the default is read-only.

View Source
const DefaultCodexACPCommand = "npx -y @agentclientprotocol/codex-acp"

DefaultCodexACPCommand is the preferred Codex transport spawn for interface=acp (sty_3b4909bb). The @agentclientprotocol/codex-acp adapter starts the Codex App Server and speaks Agent Client Protocol on stdio, so satelle reuses the existing ACP client (sessions, stream, permission policy, tools grant). No third satelle interface is required. Operators set:

interface = "acp"
command   = DefaultCodexACPCommand  # or "codex-acp" with a multi-token spawn

Live dogfood needs the adapter (npx/npm) and an authenticated Codex CLI session; hermetic tests never invoke this binary.

View Source
const DefaultCodexExecCommand = `codex exec -s read-only -m {model} -c model_reasoning_effort="{effort}" {system}`

DefaultCodexExecCommand is the secondary Codex command-template transport (interface=command / NewRunner("codex")). codex exec takes the gate rubric as the initial PROMPT argv token ({system}); the satelle work-item always rides on stdin (dual delivery — do not also place {payload} on argv). -s read-only is the baked sandbox ceiling (Codex analogue of Claude --disallowedTools / Grok --deny). -m {model} drops when model is empty. Preference remains ACP (DefaultCodexACPCommand); this template covers operators who cannot run the adapter. model_reasoning_effort is the Codex config key for reasoning effort; fused {effort} substitution (buildArgs) expands it or drops -c when effort is empty (sty_aa726901). Value is TOML-quoted so Codex -c accepts a string (unquoted bare tokens fail type checks). Preference remains ACP (DefaultCodexACPCommand).

View Source
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

func Available

func Available(name string) bool

Available reports whether the named CLI's binary is on PATH. Known CLI names (claude/grok/codex) are looked up directly — availability does not route through the agents.toml preset resolver (which no longer accepts bare tokens).

func Detect

func Detect() string

Detect returns the first supported agent CLI found on PATH (claude preferred), or "" when none is installed. Used by the install-time selection.

func FormatEvent added in v0.0.350

func FormatEvent(ev Event) string

FormatEvent renders one readable normalized diagnostic line.

func RedactSecrets added in v0.0.350

func RedactSecrets(s string) string

RedactSecrets applies best-effort protection to explicitly enabled raw traces.

func SafeText added in v0.0.350

func SafeText(s string) string

SafeText makes provider prose suitable for progress and normalized logs.

Types

type CaptureMode added in v0.0.339

type CaptureMode int

CaptureMode selects which ACP agent_message_chunk text is returned as the agent's output (sty_844b6ab1). Command transport ignores CaptureMode.

const (
	// CaptureAnswer (default / zero value): the last non-empty agent_message
	// segment, closed when a tool_call / tool_call_update intervenes. Drops
	// pre-tool narration that would otherwise contaminate step summaries and
	// other prose artifacts.
	CaptureAnswer CaptureMode = iota
	// CaptureFull: every agent_message_chunk concatenated in order — the
	// pre-sty_844b6ab1 behaviour. Required for ExpectVerdict so a decision
	// JSON that appears before trailing chatter is still parseable.
	CaptureFull
)

type Event added in v0.0.350

type Event struct {
	Kind   EventKind         `json:"kind"`
	At     time.Time         `json:"at"`
	Text   string            `json:"text,omitempty"`
	Tool   string            `json:"tool,omitempty"`
	Status string            `json:"status,omitempty"`
	Usage  *UsageResult      `json:"usage,omitempty"`
	Error  string            `json:"error,omitempty"`
	Meta   map[string]string `json:"meta,omitempty"`
}

Event is the normalized execution record emitted by command and ACP runners. Text and Error are sanitized and bounded before emission. Metadata must contain only non-sensitive transport labels, never prompts, payloads, settings, or env.

type EventHandler added in v0.0.350

type EventHandler func(Event)

EventHandler receives normalized events as they happen. Implementations may be called from concurrent stdout/stderr reader goroutines.

type EventKind added in v0.0.350

type EventKind string

EventKind is a provider-neutral isolated-agent execution event. Events are an observability side channel only: the byte slice returned by Runner.Run remains the authoritative final response.

const (
	EventStart             EventKind = "start"
	EventHeartbeat         EventKind = "heartbeat"
	EventMessage           EventKind = "message"
	EventToolStart         EventKind = "tool_start"
	EventToolEnd           EventKind = "tool_end"
	EventArtifactCandidate EventKind = "artifact_candidate"
	EventUsage             EventKind = "usage"
	EventCompleted         EventKind = "completed"
	EventFailed            EventKind = "failed"
)

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
	// OnEvent receives provider-neutral progressive events without changing the
	// authoritative stdout returned by Run.
	OnEvent EventHandler
	// HeartbeatInterval controls liveness events while the transport is silent.
	// Zero uses the production default; a negative value disables heartbeats.
	HeartbeatInterval time.Duration
	// Capture selects what an ACP runner returns from streamed session/update
	// chunks (sty_844b6ab1). Zero value is CaptureAnswer: keep only the final
	// agent_message run after tool_call fences, so narration before tools does
	// not contaminate persisted artifacts. CaptureFull returns every message
	// chunk concatenated (byte-identical to pre-fix capture) — used by the
	// verdict path so parseDecision still sees a decision emitted before
	// trailing chatter. Command-transport runners ignore this field entirely.
	Capture CaptureMode
}

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

func NewRunner(name string) (Runner, error)

NewRunner returns the Runner for a bare CLI NAME — the preset. An empty name defaults to claude; "grok" expands to the grok command preset; "codex" expands to DefaultCodexExecCommand (command transport). Preferred Codex path for agents.toml is interface=acp + DefaultCodexACPCommand — NewRunner cannot return an ACP runner (no interface arg); see help agent-dispatch. Unknown names error. Callers with a full command template use RunnerFromCommand.

func RunnerFromBinding added in v0.0.275

func RunnerFromBinding(iface, command string) (Runner, error)

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

func RunnerFromCommand(command string) (Runner, error)

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

type UsageResult struct {
	InputTokens  int
	OutputTokens int
	TotalTokens  int
	Duration     time.Duration
	// Available distinguishes a transport-reported zero from usage that was not
	// reported at all.
	Available bool
}

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 leaves Available false, so unreported usage is distinct from a reported zero. 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 Available false — plain-text harnesses keep working and explicitly report unavailable cost. Duration is set by the caller (UnwrapUsage measures only tokens).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL