Summary
Add a minimal, human-facing trigger surface to the af CLI so developers and scripts can invoke reasoners and watch workflows directly from the terminal. Today the server exposes POST /api/v1/execute/:target, /reasoners/:id, and /skills/:id, but no CLI command wraps them — the only way to call a reasoner from the shell is af agent batch with a hand-written JSON envelope.
This issue specifies the complete scope. Implementation must not exceed it.
Scope: 3 new verbs
af call <node>.<reasoner> # trigger a reasoner
af ls [query] # discover reasoners (recency-first, fuzzy filter)
af tail <run_id> # attach to a running execution stream
No other verbs. No af find, af show, af history, af repeat, af ctx, af workflow, af cancel. Existing af execution cancel|pause|resume covers run control.
Non-goals (explicit)
- No client-side state files, history database, or contexts file
- No per-user metrics (no "5x today" call counts)
- No TUI / interactive browser
- No pipe-chaining or batch DSL (
af agent batch already covers programmatic batching)
- No editor-based input authoring in v1 (
--edit flag deferred)
- No new env vars (reuse existing
AGENTFIELD_SERVER, AGENTFIELD_API_KEY)
af call <node>.<reasoner>
Triggers a reasoner. TTY-aware: prompts humans, runs silently for scripts.
Flags
| Flag |
Purpose |
--in <json|@path> |
Input payload — inline JSON string, or @path/to/file.{json,yaml} (curl convention, format auto-detected from extension) |
--schema |
Print the reasoner's input schema and exit; do not execute |
--async |
Return run_id immediately and exit; do not stream |
--no-interactive |
Never prompt, even on TTY (script-safe override) |
--interactive |
Force prompt, even when not on TTY |
-o pretty|json|yaml |
Output format. Auto: pretty on TTY, json otherwise |
--field <jq-path> |
Extract a single field from the result (e.g. --field .findings[0].score) |
Input precedence
--in <value> (inline JSON or @file)
- stdin if not a TTY and no
--in passed (e.g. cat payload.json | af call X)
- TTY + no input + reasoner has a non-empty input schema → prompt for required fields, apply defaults for optional, validate per-field against schema, confirm before submit
- TTY + no input + reasoner has empty/no input schema → call immediately with
{}
- Non-TTY + no input + reasoner requires input → exit 2 with hint
Execution behavior
- Default (TTY, sync): stream progress as a live tree of sub-agent calls via the server's SSE endpoint; print final result when done
- Ctrl-C during stream: detach gracefully (run continues server-side); print
af tail <run_id> to resume
--async: print run_id on stdout, exit 0 if queued successfully
- Non-TTY (script mode): no progress rendering; final JSON result to stdout, logs/status to stderr
Exit codes
| Code |
Meaning |
0 |
Reasoner completed successfully |
1 |
Reasoner ran but returned an error / failed status |
2 |
Client-side error (bad input, schema validation, missing required field) |
3 |
Transport / network / auth error (server unreachable, 401, 403) |
Output streams
- stdout: result payload only (so
af call … | jq . works)
- stderr: progress tree, prompts, status messages, errors
Examples
# Inline
af call sec-af.hunt --in '{"target":"acme.com"}'
# From file (JSON or YAML auto-detected)
af call contract-af.review --in @payload.yaml
# Stdin pipe (script-friendly)
cat payload.json | af call contract-af.review
# Interactive form (TTY, no --in)
af call sec-af.hunt
# Inspect schema without running
af call sec-af.hunt --schema -o yaml
# Fire-and-forget
af call contract-af.review --in @payload.yaml --async
# Extract one field
af call sec-af.hunt --in '{"target":"acme.com"}' --field .findings[0].severity
af ls [query]
Lists reasoners. Recency-first by default to keep the screen scannable at 100-reasoner scale.
Flags
| Flag |
Purpose |
[query] |
Positional fuzzy filter on <node>.<reasoner> (no flag needed) |
--all |
Show every reasoner, not just recent |
--node <name> |
Filter to a single node |
--live |
Only reasoners whose node is currently healthy |
-o pretty|json|yaml |
Output format |
Default output (TTY)
$ af ls
RECENT
sec-af.hunt 1m ago ● live
contract-af.review 12m ago ● live
finance.audit_q1 2h ago ● live
sec-af.prove yesterday ○ stale
… 14 more recent • 100 total • use `af ls --all`
$ af ls hunt
sec-af.hunt 1m ago ● live
bug-hunter.scan — ● live
Recency source
last_run_at per reasoner from the server's existing executions table. No client-side state. Server response shape per row:
{ "node": "sec-af", "reasoner": "hunt", "last_run_at": "2026-05-19T10:42:00Z", "status": "live" }
Required server work
The control plane needs a list endpoint that returns reasoners with {node, reasoner, last_run_at, status: live|stale|down}. If /api/v1/agentic/discover doesn't already return last_run_at, extend it; otherwise add a dedicated endpoint (e.g. GET /api/v1/reasoners).
af tail <run_id>
Attaches to a running execution and streams progress events until terminal status.
Flags
| Flag |
Purpose |
<run_id> |
Required positional |
--from <step> |
Resume stream from a specific step (skip already-shown events on reconnect) |
-o pretty|json |
Output format |
Behavior
- Consumes the server's existing SSE/event stream for the given run
- Renders the same tree view as
af call sync mode
- Exits 0 on terminal success, 1 on failure, 3 on transport error
- Ctrl-C disconnects; run continues server-side
Required server work
If a per-run SSE event stream doesn't already exist, add one (e.g. GET /api/v1/executions/:run_id/events with Accept: text/event-stream).
Configuration
Reuses existing mechanisms only:
AGENTFIELD_SERVER env var (default http://localhost:8080)
AGENTFIELD_API_KEY env var
--server and --api-key flags on every command (already global in af)
No new env vars. No config files.
Implementation notes
- New CLI files alongside
control-plane/internal/cli/agent_commands.go:
call.go — af call command and input/output handling
ls.go — af ls command
tail.go — af tail command and SSE consumer
- Wire commands in
control-plane/internal/cli/root.go (NewRootCommand)
- Reuse
agentHTTP() helper from agent_commands.go for HTTP requests
- Schema fetch: use the reasoner's input schema from the existing reasoner registration metadata (whatever endpoint the web UI already calls to render the playground)
- TTY detection:
term.IsTerminal(int(os.Stdin.Fd())) and os.Stdout.Fd()
- Form prompting: use an existing lightweight library already in the module (check
go.mod); if none, prefer github.com/AlecAivazis/survey/v2 — but only if it adds <1MB to the binary
- SSE consumer: standard library
bufio.Scanner over text/event-stream response — no new dependency
Acceptance criteria
Summary
Add a minimal, human-facing trigger surface to the
afCLI so developers and scripts can invoke reasoners and watch workflows directly from the terminal. Today the server exposesPOST /api/v1/execute/:target,/reasoners/:id, and/skills/:id, but no CLI command wraps them — the only way to call a reasoner from the shell isaf agent batchwith a hand-written JSON envelope.This issue specifies the complete scope. Implementation must not exceed it.
Scope: 3 new verbs
No other verbs. No
af find,af show,af history,af repeat,af ctx,af workflow,af cancel. Existingaf execution cancel|pause|resumecovers run control.Non-goals (explicit)
af agent batchalready covers programmatic batching)--editflag deferred)AGENTFIELD_SERVER,AGENTFIELD_API_KEY)af call <node>.<reasoner>Triggers a reasoner. TTY-aware: prompts humans, runs silently for scripts.
Flags
--in <json|@path>@path/to/file.{json,yaml}(curl convention, format auto-detected from extension)--schema--asyncrun_idimmediately and exit; do not stream--no-interactive--interactive-o pretty|json|yamlprettyon TTY,jsonotherwise--field <jq-path>--field .findings[0].score)Input precedence
--in <value>(inline JSON or@file)--inpassed (e.g.cat payload.json | af call X){}Execution behavior
af tail <run_id>to resume--async: printrun_idon stdout, exit 0 if queued successfullyExit codes
0123Output streams
af call … | jq .works)Examples
af ls [query]Lists reasoners. Recency-first by default to keep the screen scannable at 100-reasoner scale.
Flags
[query]<node>.<reasoner>(no flag needed)--all--node <name>--live-o pretty|json|yamlDefault output (TTY)
Recency source
last_run_atper reasoner from the server's existing executions table. No client-side state. Server response shape per row:{ "node": "sec-af", "reasoner": "hunt", "last_run_at": "2026-05-19T10:42:00Z", "status": "live" }Required server work
The control plane needs a list endpoint that returns reasoners with
{node, reasoner, last_run_at, status: live|stale|down}. If/api/v1/agentic/discoverdoesn't already returnlast_run_at, extend it; otherwise add a dedicated endpoint (e.g.GET /api/v1/reasoners).af tail <run_id>Attaches to a running execution and streams progress events until terminal status.
Flags
<run_id>--from <step>-o pretty|jsonBehavior
af callsync modeRequired server work
If a per-run SSE event stream doesn't already exist, add one (e.g.
GET /api/v1/executions/:run_id/eventswithAccept: text/event-stream).Configuration
Reuses existing mechanisms only:
AGENTFIELD_SERVERenv var (defaulthttp://localhost:8080)AGENTFIELD_API_KEYenv var--serverand--api-keyflags on every command (already global inaf)No new env vars. No config files.
Implementation notes
control-plane/internal/cli/agent_commands.go:call.go—af callcommand and input/output handlingls.go—af lscommandtail.go—af tailcommand and SSE consumercontrol-plane/internal/cli/root.go(NewRootCommand)agentHTTP()helper fromagent_commands.gofor HTTP requeststerm.IsTerminal(int(os.Stdin.Fd()))andos.Stdout.Fd()go.mod); if none, prefergithub.com/AlecAivazis/survey/v2— but only if it adds <1MB to the binarybufio.Scannerovertext/event-streamresponse — no new dependencyAcceptance criteria
af call <node>.<reasoner> --in '{…}'runs a reasoner sync, streams progress tree on TTY, returns result on stdoutaf call Xwith no--inon a TTY prompts for required fields from the schema, validates per-field, confirms before submitaf call Xwith no--inand no TTY exits non-zero with a clear hint (does not hang)cat input.json | af call Xworks without--inaf call X --in @file.yamlaccepts YAML and JSON filesaf call X --schemaprints the input schema without executingaf call X --asyncreturnsrun_idimmediatelyaf call X --field .a.bextracts and prints the named fieldaf tailcommand to resumeaf lsshows recency-sorted reasoners on TTY, JSON on pipeaf ls huntfuzzy-filters;af ls --allshows everything;af ls --node sec-afscopes to one nodeaf tail <run_id>streams the same tree view as a liveaf callaf call … | jq .works~/.agentfield/--in> stdin > prompt > empty), schema validation errors, exit-code mappingaf callagainst a local control plane with a real reasoner