This document specifies st2's current implementation contract. It builds on requirements.md.
Active. This is a concise map to the implementation and its evidence, not a replacement for the README, CLI help, KDL examples, or tests.
st2 validates a declared agent fleet, materializes agent workspaces, launches
host-local work, adopts and supervises independently surviving tasks, and
delivers messages. The agent grammar and harness-facing contract remain
canonical in
compoundingtech/evals/AGENT-SPEC.md.
An agent may directly declare zero or more generic Resource bindings:
resource "work" _tag="github-issue" uri="github-issue://example/project/123"The positional name is an agent-local semantic role, _tag is the concrete
type discriminator, and uri is the exact RFC 3986 absolute resource identity,
preserved byte-for-byte without normalization.
Declaration order has no meaning and binding names are unique within one
agent. The public agent-spec read model preserves the bindings in name order
across canonical KDL and the supported TOML/JSON forms. st2 agents --json
projects the same descriptors for language-neutral inspection.
st2 validates only this portable envelope. It does not define downstream type schemas, resolve targets, infer authority from URI possession, or attach required/optional, access, readiness, or lifecycle semantics. Those concerns remain outside the generic binding contract. A Resource binding is declaration metadata and is absent from task launch targets; changing only Resource bindings adopts an already-live task without stop, replacement, or relaunch.
The unresolved-resource runtime discussion remains tracked in st2#60, read-oriented renderer integration in st2#61, and the portable Agent Spec envelope in evals#41.
hand-authored KDL
│
▼
validate ──► materialize ──► host-local st2 scheduler/reconciler
│
┌────────┴────────┐
▼ ▼
PTY / exec DING sidecar
│ │
└──── state + bus ┘
▲
│
one intelligent root agent
observes · recovers · escalates
-
R01–R03: Fleet validation separates structural errors from selected-host runtime facts. Materialization is inspectable and host reconciliation starts only declarations pinned to the local host. Discovery is recursive: an explicit
identityandhostpair is authoritative independent of the declaration's path, whose parent remains the state/resource anchor. When either field is omitted, the path supplies defaults and mismatches remain diagnostic. Dot-prefixed folders, including.managedand.retired, are ordinary declaration space; only.gitand.st2directories at any depth, the catalog root'sptychild, and a declaration parent'sresources,archive, andinboxchildren are excluded. A resolved workspace-relative render destination has one coherent desired state across the active local fleet: byte-equivalent idempotent claims may share it, while incompatible claims fail every conflicting owner before the first workspace write. Targeted reconciliation checks the selected owner against the full fleet, so selection cannot bypass this ownership boundary. -
R04: Each machine schedules and reconciles only its pinned work. The st2 loop is deterministic; exactly one declared root agent provides intelligent host-local supervision, bounded recovery, and escalation. Filesystem reads never wake reconciliation; only create, modify, rename, or remove events may wake it before the bounded timer.
-
R06: st2 passes the complete effective task definition to the underlying launcher so manual and supervised restarts are equivalent. Harness readiness that depends on a dynamically selected account belongs to that declared command. In particular, reconciliation never mutates an ambient Codex config before launch: the command may select an account-specific
CODEX_HOMEonly after st2 starts it.st2 pretrustremains an explicit operator utility for commands that intentionally use the ambient Claude and Codex configs. -
R07: Hook bundles are explicit, content-addressed, installed separately, and verified before materialization references them. Their receipts use the same resolved build identity as the binary's version surfaces for both hermetic package builds and source builds. Installation automatically accepts an ordered upgrade;
--replaceis the explicit exact-state authority for a downgrade, an unorderable build, or an unreadable receipt. Shipped hooks resolve Bash throughPATH; the Nix package executes their integration gate with Bash andjqdeclared. Runtime materialization verifies the invoking binary's own content-addressed set, independent of which installed binary the receipt currently selects, so old and new supervisors can overlap during cutover.hooks verify-ownexposes that read-only capability to package activation tooling. -
R11:
st2 upis a replaceable control plane, not the lifetime owner of its agents. Normal exit, forced termination, binary replacement, and restart leave every running agent PID and creation identity unchanged. The new control plane adopts those processes and starts only missing work; it does not duplicate them. Agent stop or retirement requires a separate explicit lifecycle action.Executable acceptance starts an agent, terminates
st2 upnormally and with a forced kill, verifies the agent remains alive and usable, replaces the st2 binary, starts the control plane again, and proves adoption with the same agent PID/creation identity and no duplicate process. -
Session registry: A catalog owns the
ptyregistry holding its tasks.<catalog>/ptyis the default; a catalog may declare another so that one host can share a single registry across catalogs. Resolution is an exportedPTY_ROOT, then the catalog's declaration, then the default, applied uniformly to spawn, list, kill, and the bus environment st2 hands to native tools, so every reader that can resolve the catalog agrees about where its sessions are. A declaration whose field set does not match failsst2 validaterather than resolving silently back to the default. Runtime observation has a short outer deadline so a wedged client fails the pass closed instead of hanging reconciliation. The deadline is containment, not the mechanism for admitting a larger fleet.
atomic inbox file → DING attempt → agent reads → archive receipt
└──────── archive with same filename wins ────────┘
- R05: A matching archive filename makes an inbox copy handled; stale
duplicates are removed without another DING. Fresh
dndsuppresses delivery;busydoes not. Failed delivery remains retryable. Sidecar restart emits a bounded recovery notice instead of replaying the inbox. Delivery may wake an agent while it is working, but an active or uncertain human composer must be left untouched. Unsafe delivery retries use a bounded backoff so an active composer cannot create a short-lived PTY probe on every inbox poll. Inbox reads do not wake the sidecar; only mutations bypass its bounded poll cadence.
- R08: Presence and activity status are separate signals. The catalog must also expose the agent's current plan and step with explicit freshness so a human or supervising agent can understand progress without PTY inspection. Current presence/status files provide only part of this contract; the canonical plan-progress shape is not yet specified.
- R09: Durable work state is external to the model transcript and is restored into replacement sessions through declared workspace files and verified hooks.
- R10: Fleet identities are agents. General-purpose identity kinds are unsupported.
The owner updates this spec whenever implementation changes. Changing vision.md or requirements.md requires Nathan's explicit approval.
An event is evidence, not permission to run the world. The reconciler retains path and kind, maps them to the affected identity or template dependency set, and computes the smallest desired-versus-actual delta. One declaration affects only that agent; a template affects only its dependents. A no-op performs zero PTY queries, launches, teardowns, materialization, or writes.
Watchers are deny-by-default. The classifier/action contract is:
| Event | Minimal action |
|---|---|
declaration-space **/agent.kdl create/modify/remove |
validate, materialize, and converge that agent and derived tasks |
referenced _templates/** mutation |
converge dependent agents only |
| inbox create/archive/remove | DING consumer only; supervisor no-op |
| plan/resource/status mutation | specialized consumer only; supervisor no-op |
| PTY/exec/log/PID/socket/lock/temp/backup/read/open/unknown | no-op |
Startup, timer, watcher overflow/loss, and ambiguity are bounded full-audit fallbacks. Accepted streams use head/tail coalescing: immediate head response, one quiet tail after a burst, and a hard maximum. Executable proof covers positive declaration/template wakes, negative runtime/bus events, bounded discovery/materialization/PTY queries and writes, continuous-event starvation, and no-op desired-equals-actual behavior.
st2 up --materialize-only --task <host.agent.task> resolves one exact local
task before writing and renders only its owning agent. st2 up --once --task <host.agent.task> performs the same owner-only materialization, then inspects
PTY/exec state and executes a plan containing only that task. Unknown,
ambiguous, and wrong-host selectors refuse before writes or runner inspection;
unrelated discovery diagnostics remain visible without preventing the selected
owner/task path.
st2 up --materialize-only --agent <id> remains the agent-wide rendering
selector. Targeted task reconciliation is intentionally bounded to --once;
the resident supervisor continues to reconcile the complete local catalog.
- DQ1 Scheduled work: The vision includes per-machine schedulers that form a distributed workflow engine, but the KDL shape, event inbox, deduplication boundary, and execution receipts are not yet specified. A successful executable eval and Nathan's approval should resolve this before adding scheduler requirements.
- DQ2 Safe DING delivery: Bounded observation now replaces the fixed paste-to-Return delay: maintained Codex and Claude composers must be positively empty before paste and show the exact staged notice twice before a separate Return. Human, modal, active, changed, timed-out, and unknown states fail closed, with staged-payload ownership preventing duplicate paste. This measured screen heuristic is still not an evented proof and renderer changes may defer delivery. Resolve the remaining gap with a stronger evented signal or other measured classifier; a small on-device model is an optional experiment, not a required architecture.
- DQ3 Catalog agent state: Define the catalog paths, schemas, freshness
rules, and atomic update semantics for presence, activity status, current
plan, and current plan step. Prove that stale state is distinguishable and
that a supervisor can follow plan progress without inspecting a PTY before
adding the shape to
AGENT-SPEC.md.