agentctl is a small Rust library and CLI for giving LLM coding agents compact,
bounded reports from noisy commands and long-running agent tasks.
The common use case is a tool loop around Claude Code, Codex, or another agent: run the expensive command normally, keep the full raw log on disk, and return a short structured summary that is cheap for the model to read.
cargo install --git https://github.com/hydra-dynamix/agentctl agentctlGenerate a starter config:
agentctl config generate
agentctl config checkagentctl reads ./agentctl.toml first, then falls back to
~/.agentctl/config.toml. Raw command logs and detached job records live under
~/.agentctl/jobs/.
On Windows, USERPROFILE is used when HOME is unset.
Inspect the current platform, home-directory resolution, LDGR harness
selection, configured agents, and commands actually available on PATH:
agentctl discover
agentctl discover --jsonFor an ldgr-loop or ldgr-summary profile, agentctl negotiates
ldgr.launcher-compatibility.v1 before reading worker output or starting the
agent. Agentctl 0.1.x requires LDGR Core >=0.1.13, <0.2.0. An older or mixed
Core is rejected with a durable agentctl.compatibility/core-incompatible
recovery record and exact upgrade/rollback commands. Install both executables
from the same LDGR Core release bundle.
Agentctl works with HOME on Unix, macOS, WSL, and Windows environments that
provide it. Native Windows also falls back to USERPROFILE, then
HOMEDRIVE + HOMEPATH; AGENTCTL_HOME is an explicit final configuration
override for unusual service or test environments. A synthesized home is
passed to child harnesses as HOME.
ldgr install records its ordered harness selection in
~/.ldgr/config.toml. Agentctl also reads the older
~/.ldgr/config.json during migration. Resolution order is:
- an explicit
agentctl run <agent>profile; - a runnable generic
ldgr-loopprofile; - the first runnable profile among LDGR's default and selected harnesses;
- for an agent omitted on the command line, another runnable configured profile.
An unavailable explicit non-LDGR profile never silently changes providers.
When the generic LDGR profile is stale, Agentctl can use the next selected
harness and prints the decision to stderr. Pass --no-fallback to disable that
behavior. Failures list the detected platform, config source, selected
harnesses, runnable alternatives, and repair commands.
From a CI checkout of a pull request, launch the built-in audit policy against
the open PR whose head exactly matches HEAD:
agentctl task pr-review \
--agent codex \
--requirements /absolute/path/to/requirements.md \
--detach \
--jsonThe requirements file is optional, must be an absolute path to a readable UTF-8
regular file, and is embedded in the review prompt. The confidence threshold
defaults to 90 and can be changed with --confidence-threshold <0-100>.
The task resolves the repository and associated PR through git and gh, then
pins the audit to the exact HEAD SHA. Blocking findings produce a
request-changes review. A passing review below the confidence threshold produces
a non-approving comment. At or above the threshold, the reviewer may approve and
enable GitHub auto-merge using a merge commit, but only after rechecking that the
PR head is unchanged. Required checks and branch protections are never bypassed.
The merge-commit strategy preserves the PR's development commits and records a
distinct landing event. The reviewer also checks that the PR description records
all material changes so the repository's change history remains useful.
The runner needs GitHub permissions to read pull requests, submit reviews, and
enable auto-merge. Ambient gh authentication works, or configure an
organization-installed GitHub App once in the runner's home config:
[github_app]
app_id_command = ["op-vault", "get", "AGENTCTL_GH_APP_ID", "--account", "production"]
private_key_command = ["op-vault", "get", "AGENTCTL_GH_PRIVATE_KEY", "--account", "production"]For every review, agentctl retrieves the credentials, signs a short-lived JWT,
and mints an installation token scoped to the current repository. The token is
provided to gh and the review agent as GH_TOKEN; it is not written to the
prompt, logs, supervisor specification, or job record. GitHub App credential
commands are accepted only from the machine-owned home config, never from an
untrusted repository agentctl.toml. If [github_app] is absent, existing
ambient gh authentication behavior is unchanged.
Repository and PR content are treated as untrusted review material by the predefined policy.
Use exec when an agent wants to run a command but should receive only the
important lines:
agentctl exec --json --label cargo-test -- cargo testThe exit code is the child process exit code, so existing tool-loop error
handling still works. A failing cargo test returns a compact JSON summary:
{
"command": "cargo test",
"exit_code": 101,
"duration_ms": 1842,
"preview_lines": [
"failures:",
"test result: FAILED. 12 passed; 1 failed"
],
"raw_log_path": "~/.agentctl/jobs/1780000000-1234-cargo-test/output.log"
}The full stdout and stderr remain in raw_log_path for later inspection. Use
--raw when you deliberately want the untouched command output streamed back.
Output is streamed to capture files while only the configured head/tail window is retained in memory; the complete raw log is assembled when the command finishes. On Unix, agent commands run in an isolated process group. Interrupting agentctl forwards the signal to the complete command tree, waits for it, and terminates descendants left behind by a command that exits early. On Linux, agentctl also adopts and reaps orphaned descendants so repeated tool loops do not accumulate zombie or live background agent processes. Cleanup is scoped to each capture, preserving unrelated child processes and concurrent captures in library hosts. Each Linux capture has a dedicated subreaper supervisor, so descendants remain owned even after changing session, clearing their environment, or exiting before cleanup. The embedding application is never made a subreaper. This is lifecycle management, not a security sandbox. On Windows, commands start suspended and join a Job Object before running, so remaining descendants are terminated when the command finishes or its host exits.
Configure agents in TOML:
[summary]
max_output_bytes = 16384
tail_bytes = 4096
max_preview_lines = 12
[agents.codex]
command = ["codex", "exec", "--sandbox", "danger-full-access"]
prompt_stdin = true
[agents.pi]
command = ["pi", "-p"]
prompt_stdin = false
[agents.claude-code]
command = ["claude", "-p"]
prompt_stdin = false
# Alias for Claude Code.
[agents.claude]
command = ["claude", "-p"]
prompt_stdin = false
# Provider-specific wrappers are external commands. Point these profiles at
# your chosen REST/websocket bridge scripts or binaries.
[agents.openai-rest]
command = ["openai-rest-agent"]
prompt_stdin = true
[agents.openai-websocket]
command = ["openai-websocket-agent"]
prompt_stdin = true
[agents.ollama]
command = ["ollama", "run", "llama3"]
prompt_stdin = true
# Native OpenRouter Chat Completions provider. The command returns only the key
# on stdout. agentctl never writes the key to its config or job logs.
[providers.openrouter]
kind = "openrouter"
base_url = "https://openrouter.ai/api/v1"
api_key_command = ["op", "read", "op://vault/OPENROUTER_API_KEY/password"]
model = "openrouter/auto"
# Use the native provider through the normal run command.
[agents.openrouter-api]
provider = "openrouter"
prompt_stdin = true
# Run Pi with its OpenRouter provider instead.
[agents.openrouter]
command = ["pi", "--provider", "openrouter", "--model", "openrouter/auto", "--print", "--no-session"]
prompt_stdin = false
[agents.openclaw]
command = ["openclaw", "run"]
prompt_stdin = false
[agents.opencode]
command = ["opencode", "run"]
prompt_stdin = falseUse either OpenRouter profile:
agentctl run openrouter-api --prompt "Summarize the current design."
agentctl run openrouter --prompt "Inspect the repository and fix the failing test."api_key_env = "OPENROUTER_API_KEY" is also supported. Set exactly one of
api_key_env and api_key_command for each provider.
Start a long task without keeping the caller attached:
agentctl run codex \
--prompt "Run cargo test, fix failures, and report the files changed." \
--cwd /path/to/project \
--detach \
--json
# With no explicit profile, Agentctl uses the valid LDGR selection when
# available and otherwise prefers codex. Stdin is accepted as the prompt:
printf 'Run cargo test and summarize the result.' | agentctl run --jsonCodex profiles now default to --sandbox danger-full-access, allowing Git staging,
commits, and branch creation. To opt into the restricted workspace sandbox, use
agentctl run codex --sandbox --prompt "Inspect the project". This also works with
--detach and --iterations; .git remains read-only in that mode. The flag
requires a profile whose command starts with codex exec (including an absolute
path to Codex). Other runtimes retain their own permission settings.
Existing config files retain their explicit commands. To migrate an older Codex
profile, change its workspace-write argument to danger-full-access in
agentctl.toml or ~/.agentctl/config.toml. Workers still inherit any restrictions
imposed by the process that launches agentctl.
The detached command returns a job record:
{
"id": "1780000000-1234-codex",
"agent": "codex",
"state": "running",
"pid": 12345,
"started_at": 1780000000,
"finished_at": null,
"exit_code": null,
"command": "codex exec --sandbox danger-full-access",
"cwd": "/path/to/project",
"iterations": 1,
"log_path": "~/.agentctl/jobs/1780000000-1234-codex/output.log"
}Poll and inspect it later:
agentctl status 1780000000-1234-codex --json
agentctl logs 1780000000-1234-codex
agentctl list --jsonlogs is intentionally the raw-output command. status and list stay bounded
and machine-readable for agent loops.
Status reads at most the final 16 KiB of a log, even when a line is very long.
Raw logs are streamed without requiring valid UTF-8.
Detached supervisors use isolated process groups on Unix. On Windows they start without a console or inherited caller handles, so the caller's output pipes close immediately after the launch report.
Every public command that has --json uses stable field names.
Summary:
{
"command": "string",
"exit_code": 0,
"duration_ms": 0,
"preview_lines": ["string"],
"raw_log_path": "/path/to/output.log"
}Foreground run --json returns an array of iteration reports:
[
{
"iteration": 1,
"iterations": 2,
"summary": {}
}
]If an iteration fails, its report and preceding successful reports are returned, further iterations stop, and the CLI exits with the failed command's exit code.
Detached run --json and list --json use job records:
{
"id": "string",
"agent": "codex",
"state": "running",
"pid": 12345,
"started_at": 1780000000,
"finished_at": null,
"exit_code": null,
"command": "codex exec --sandbox danger-full-access",
"cwd": "/path/to/project",
"iterations": 1,
"log_path": "/path/to/output.log"
}state is one of running, succeeded, failed, or stale.
Status wraps a job record with bounded log tail lines:
{
"record": {},
"tail_lines": ["string"]
}These shapes are semver-relevant.
The crate also exposes the capture, summary, config, and job modules used by the
CLI. The public API is intentionally small so other local tooling can build the
same bounded reports without shelling out.
Embedding applications can call jobs::launch_detached_with_executable with the
path to an installed agentctl binary. launch_detached re-executes the current
binary and is intended for callers that implement agentctl's supervisor command.
Run dev fix and dev ci using the repository's Rust-only .dev/config.toml,
or use Cargo directly. CLI integration tests require Python on PATH.
CI is configured for Windows, Linux, and macOS. Linux-only subreaper tests are
separate from the portable command, provider, and detached-job tests.
agentctl includes a native OpenRouter Chat Completions provider. Native
provider profiles use the same bounded capture, summaries, detached jobs, and
iteration handling as command profiles.
Other provider-specific clients remain external commands. Configure those
clients under agents, as shown for OpenAI and Ollama above.