Documentation
¶
Overview ¶
Package cli assembles the command-line surface. Slice 1.6 shipped `awf validate <path>`; slice 2.5 added `awf run`; slice 2.6 added `awf resume`; Phase 3 added `signal` / `pause` / `cancel`; slice 4.5 added `--backend {auto,native,docker,fake}` on `awf run` + log-driven backend selection on `awf resume`. Later phases add `inspect` / `trace` / `ls` (Phase 6). The entry point is Runner.Run, not init() or a package-level CLI framework, so tests drive the full surface with bytes.Buffer for IO and an int return for the exit code — no real os.Exit ever called from package code.
Runner holds the production-vs-test seams: Backend (test injection; production leaves it nil and the run/resume subcommands construct via the package-private newBackend helper when Backend is nil — see cli/backend.go for the kind switch) and IDGen.
Index ¶
Constants ¶
const ( ExitOK = 0 // success: validation produced zero error-severity diagnostics, OR run terminated ok ExitInvalid = 1 // `awf validate` produced ≥1 error-severity diagnostic (also returned by `awf run` if its pre-run validation finds errors) ExitUsage = 2 // usage: bad args/flags, unreadable file, loader-stage failure (parse/path-escape/missing-compose), or a precondition refusal (run-id collision, digest/pin/runtime drift, terminal, capability) ExitRunFailed = 1 // `awf run` completed but the run terminated as retryable_failure / permanent_failure ExitInfra = 3 // environment/setup failure AWF owns: blob store, backend/daemon client, container create/restore, run-dir/log I/O, live-home, run-lock )
Exit codes are part of the CLI contract — downstream tooling (CI, IDE extensions, scripts) will branch on them. Locked by tests.
Note ExitInvalid and ExitRunFailed share the numeric value 1 — both mean "the input was structurally fine but the operation didn't succeed." We keep them as separate Go constants so each subcommand can name its own failure mode for readability.
The 2-vs-3 split is "whose artifact failed". Anything about the user-supplied workflow file or arguments — bad flags, an unreadable/parse-failing file, a path escape, a missing compose, or a precondition the user fixes by editing args (run-id collision, digest mismatch, pin/runtime drift, terminal refusal, capability mismatch) — is ExitUsage (2). Anything about the environment AWF itself owns and sets up — the blob store, the backend/daemon client, container create/restore, the run-dir/log I/O, the live-home, or the run-lock — is ExitInfra (3). 2 = "your input is wrong"; 3 = "my environment is broken".
Variables ¶
var ErrRunLockHeld = runlock.ErrHeld
ErrRunLockHeld is returned by acquireRunLock when a live process holds the lock.
Functions ¶
func Run ¶
Run is the top-level CLI entry point — constructs the production Runner and delegates. cmd/awf wraps the returned int with os.Exit.
Slice 4.5: Backend is intentionally left nil; the run / resume subcommands construct it on-demand via newBackend (chosen by --backend flag on run; read from the log's run.started.Backend on resume). This defers docker-client construction to the moment we know the user actually wants Docker — important because a docker.New failure (Docker daemon not running) shouldn't crash `awf validate` or `awf help`.
Types ¶
type ErrContainerRequired ¶
type ErrContainerRequired struct {
Ref string
}
ErrContainerRequired is returned at run start (and resume) when an agent step omits `container:` but its resolved adapter is NOT Containerless (a CLI-wrapping adapter like claude/droid/goose needs a container to exec in). Permanent: re-running won't fix a missing container. This is where the missing-container check for non-containerless adapters lives, because the structural validator (ir/) is registry-free and cannot know an adapter's Containerless capability.
func (*ErrContainerRequired) Error ¶
func (e *ErrContainerRequired) Error() string
type ErrPersistentSessionContinuesTarget ¶
ErrPersistentSessionContinuesTarget is returned when a `continues:` edge targets a live/persistent adapter. Live transcripts stay provider-owned in this slice, so engine-threaded conversation context cannot be reconstructed from that predecessor without a separate design.
func (*ErrPersistentSessionContinuesTarget) Error ¶
func (e *ErrPersistentSessionContinuesTarget) Error() string
type ErrPersistentSessionGateEvaluate ¶
func (*ErrPersistentSessionGateEvaluate) Error ¶
func (e *ErrPersistentSessionGateEvaluate) Error() string
type ErrRuntimeDrift ¶
ErrRuntimeDrift is the hard-error returned when a resumed run's agent runtime version no longer matches what was persisted in run.started. Mirrors the spec §8 pinning invariant (the existing definition-digest hard-error class); resume cannot adapt to a changed binary.
Phase 5 slice 5.1: scoped per (Ref, Container) pair, since different containers may have different `claude` binaries on PATH (decision 5).
func (*ErrRuntimeDrift) Error ¶
func (e *ErrRuntimeDrift) Error() string
type ErrThreadedRequired ¶
type ErrThreadedRequired struct {
StepID string // the step that declares continues:
Ref string // its uses: ref (the non-threaded adapter)
}
ErrThreadedRequired is returned at run start (and resume) when an agent step declares `continues:` (engine-owned conversation threading) but its resolved adapter does NOT report Caps.Threaded — i.e. the adapter cannot prepend an engine-supplied AgentInvocation.Thread, so threading would silently no-op. Permanent: re-running won't make a non-threaded adapter understand a thread. This lives in cli/ (not ir/) because the structural validator is registry- free and cannot know an adapter's Threaded capability — the same constraint that puts the Containerless guard (ErrContainerRequired) here.
func (*ErrThreadedRequired) Error ¶
func (e *ErrThreadedRequired) Error() string
type Runner ¶
type Runner struct {
// Backend is the container.Backend the run subcommand passes to
// engine.LocalDispatcher. Production: left nil — the run/resume
// subcommands construct on-demand via newBackend (cli/backend.go),
// choosing a concrete fake/docker/native backend from the --backend flag
// after resolving auto. Tests: inject any container.Backend (typically
// a pre-programmed *container.Fake) to short-circuit construction
// via (*Runner).resolveBackend.
Backend container.Backend
// Resolver is the agent.Adapter registry the CLI uses to look up `uses:`
// refs at run-start and resume. Test-injection point:
// tests assign a *agent.Registry populated with agent/fake adapters and
// drive workflows that exercise the version-pinning + drift-check paths.
//
// Production leaves this nil. cli/run.go and cli/resume.go then build a
// fresh *agent.Registry for the current invocation from --agent-env/default
// env allowlists plus workflow env: declarations, and pass that registry
// down without caching it here. This matters for long-lived Runner values:
// imported workflow roles and env allowlists must not leak across runs.
//
// Workflows that don't reference any `uses:` step (every Phase 2-4
// fixture) run unaffected — they hit zero Lookup calls regardless of
// what Resolver is.
Resolver agent.Resolver
// AgentEnv is the env-var allowlist the CLI uses to construct the
// production *agent.Registry (when Resolver is nil). Slice 5.3
// test-injection point: tests assign []string{"ANTHROPIC_API_KEY"} to
// drive cli/agent_registry.go's buildAgentRegistry without the
// os.Setenv dance that --agent-env-via-flag would require.
//
// When Resolver is non-nil, AgentEnv is ignored (the test has injected
// a fully-constructed Registry, bypassing buildAgentRegistry).
// Production: cli/run.go reads --agent-env, splits to []string, and
// passes here BEFORE calling buildAgentRegistry.
AgentEnv []string
// AgentEventTap is the io.Writer agent.AgentEvent live-tap lines are written
// to during dispatch. Test-injection point: tests assign a bytes.Buffer to
// capture the rendered lines, or io.Discard to silence. Production wiring
// in cli/execute.go (Task 11) defaults to stderr.
//
// nil disables the live tap entirely (the dispatcher's writeAgentEventTap
// becomes a no-op). Workflows without any `uses:` step (Phase 2-4
// fixtures) are unaffected — the tap never fires.
AgentEventTap io.Writer
// IDGen mints run ids. Production: clock.CryptoIDGen{}. Tests: a
// seeded *clock.Fake so run dir paths are reproducible.
IDGen clock.IDGen
// BrokerOptions are passed to signal.NewBroker when the run/resume
// subcommands construct a broker. Slice 3.5 test-injection hook: tests
// pass signal.WithPollInterval(time.Millisecond) for fast polling; the
// production cli.Run constructor leaves this nil (defaults to 100ms).
BrokerOptions []signal.BrokerOption
// Stdin is the reader `awf run --input-file -` consumes for stdin-supplied
// run input (S7). Tests inject a strings.Reader; production leaves it nil and
// the run subcommand falls back to os.Stdin (see (*Runner).stdin). Read ONLY
// on the `--input-file -` path — nil everywhere else.
Stdin io.Reader
}
Runner holds the dependencies that vary between production and tests. The zero value is unusable — callers (production: cli.Run; tests: direct construction) populate every field.
Source Files
¶
- agent_registry.go
- agentrender.go
- backend.go
- backend_features.go
- cancel.go
- cli.go
- errors_containerless.go
- errors_threaded.go
- execute.go
- format.go
- graph.go
- input_files.go
- inspect.go
- live_home.go
- ls.go
- outputs.go
- pause.go
- persistent_guard.go
- resume.go
- resume_admission.go
- run.go
- runinput.go
- runlock.go
- runtimecomposeguard.go
- runtimeimageguard.go
- runtimes.go
- signal.go
- snapshotguard.go
- statedir.go
- threaded_guard.go
- trace.go
- ui.go
- util.go
- validate.go
- version.go