Skip to content

Commit 7e0b380

Browse files
Merge pull request #106 from compoundingtech/schickling-assistant/2026-07-30-catalog-publication
Publish agent specs transactionally
2 parents 1309324 + 7e1bd02 commit 7e0b380

24 files changed

Lines changed: 1990 additions & 979 deletions

INVARIANTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ materialization, messaging, DING, or presence must preserve them.
1414
| **Fail-closed observed native DING** | Each unread message becomes one normalized `[DING]` frame. Fresh delivery records ownership, then preserves the one combined bracketed-paste, 0.5 second delay, and Return transaction. PTY and Return success are transport only: `Delivered` additionally requires adapter classification of the expected notice text in a submitted-prompt or queued-message pattern while the lowest live composer is empty or an accepted idle placeholder. Every other receipt retains staged ownership. Retry never re-pastes and may send one bare Return only after two adjacent `RetainedSafe` observations; human, modal, active, changed, timed-out, and unknown retry states receive no input. Ownership prevents duplicate paste across command failures, receipt ambiguity, archive races, and restart adoption. Startup backlog otherwise becomes one generic recovery DING; new arrivals remain FIFO; `busy` delivers immediately; only fresh `dnd` defers. | `src/ding/mod.rs::poke_text_normalizes_and_bounds_untrusted_fields`; `src/ding/mod.rs::malicious_controls_cannot_escape_the_single_paste_frame`; `src/ding/mod.rs::pty_delivery_uses_face607_delay_order_and_seconds`; `src/ding/mod.rs::maintained_composer_classifiers_require_exact_idle_state`; `src/ding/mod.rs::successful_transport_with_retained_or_unproven_pixels_is_not_delivered`; `src/ding/mod.rs::ambiguous_transport_receipt_and_retry_errors_retain_staged_ownership`; `src/ding/mod.rs::adapter_recognized_notice_with_an_empty_live_composer_is_a_positive_receipt`; `src/ding/mod.rs::staged_retry_submits_only_retained_safe_and_requires_a_receipt`; `src/ding/mod.rs::staged_ownership_survives_archive_and_never_repastes`; `src/ding/mod.rs::pty_commands_have_a_real_outer_timeout`; `src/ding/mod.rs::session_watch_has_startup_grace_debounce_and_live_reset`; `src/ding/mod.rs::new_arrivals_is_fifo_and_archive_receipts_prevent_reding`; `src/ding/mod.rs::pending_delivery_ignores_busy_but_respects_fresh_dnd_archive_and_retry`; `src/ding/mod.rs::startup_recovery_notice_retries_in_memory`; `src/ding/mod.rs::startup_backlog_gets_one_generic_recovery_then_new_arrivals_poke` |
1515
| **Mutation-only filesystem wakeups** | Supervisor and DING filesystem watchers ignore read/open access events and wake early only for create, modify, rename, or remove events. Their own catalog and inbox reads therefore cannot bypass the bounded timer cadence or form a Linux inotify CPU loop. | `src/watch.rs::only_mutations_wake_watch_loops`; `src/watch.rs::linux_reads_are_silent_but_real_mutations_wake`; `src/ding/mod.rs::idle_ding_does_not_spin_on_its_own_inbox_reads`; `src/run.rs::idle_supervisor_does_not_spin_on_its_own_catalog_reads` |
1616
| **Bounded DING PTY probe churn** | An unsafe or active composer retains its FIFO notice but deferred delivery retries use a bounded backoff, so each inbox poll cannot spawn another short-lived PTY probe. | `src/ding/mod.rs::deferred_delivery_backoff_bounds_short_lived_pty_attempts` |
17-
| **Agent-declared presence discipline** | The shipped bus contract requires agents to declare `busy` before executing work, use `available` only while yielding or ready, and reserve `dnd` for an explicit hold. Both native harnesses materialize that contract. Busy remains observable but does not suppress DING; fresh `dnd` is the only delivery gate. | `tests/compile_agent.rs::compile_agent_generates_claude_then_materializes_verbatim_persona`; `tests/compile_agent.rs::compile_agent_generates_codex_then_materializes_composed_agents_md`; `src/ding/mod.rs::pending_delivery_ignores_busy_but_respects_fresh_dnd_archive_and_retry` |
17+
| **Agent-declared presence discipline** | The shipped bus contract requires agents to declare `busy` before executing work, use `available` only while yielding or ready, and reserve `dnd` for an explicit hold. Both native harnesses materialize that contract. Busy remains observable but does not suppress DING; fresh `dnd` is the only delivery gate. | `tests/native_only.rs::clean_path_executes_the_maintained_native_authoring_guide`; `src/ding/mod.rs::pending_delivery_ignores_busy_but_respects_fresh_dnd_archive_and_retry` |
1818
| **Stable roster JSON** | `st2 agents --json [--enrich]` preserves field names, order, null handling, presence, explicit retirement state, opaque declared Resource descriptors, activity, and inbox counts. Human output marks retired declarations without changing active rows. | `src/agents.rs::agents_json_has_stable_wire_shape`; `src/agents.rs::agents_json_preserves_opaque_declared_resource_descriptors`; `tests/status_agents.rs::roster_json_and_human_output_distinguish_retirement_from_presence` |
1919
| **Agent-declared presence** | Refresh preserves non-DND declared status and only advances liveness; a missing status starts as `available`, while `dnd` is never refreshed and an unrefreshed declaration ages to `unknown`. | `src/status.rs::refresh_preserves_value_and_bumps_mtime`; `src/status.rs::refresh_leaves_dnd_to_age_out`; `src/status.rs::refresh_missing_writes_available_default`; `src/status.rs::stale_mtime_reads_as_unknown_regardless_of_contents` |
2020
| **Retirement health** | A retired declaration is healthy only after every declared task ID is absent. Any live or dead declared task record reports incomplete retirement; retired declarations do not require presence. Live declarations retain their existing task and presence checks. | `tests/doctor.rs::retired_declaration_is_healthy_when_tasks_and_presence_are_absent`; `tests/doctor.rs::retired_declaration_is_unhealthy_while_a_declared_task_is_alive`; `tests/doctor.rs::retired_declaration_is_unhealthy_while_a_dead_task_record_remains` |

README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ st2 runs a declarative network of Codex and Claude agents from one catalog. It o
44
reconciliation, native messages, normalized terminal DING delivery, presence, durable context,
55
workspace materialization, and explicit teardown.
66

7-
Hand-authored KDL is the canonical interface. `st2 compile-agent` is experimental and must be
8-
reviewed before its output is materialized.
7+
Canonical KDL is the authoring interface. Publish one explicit Agent Spec transactionally with
8+
`st2 agent publish`; st2 does not compile human intent into declarations.
99

1010
## Product intent and implementation contract
1111

@@ -99,17 +99,17 @@ Start from the maintained [Codex](examples/native/agent-codex.kdl) or
9999

100100
```sh
101101
export CATALOG="${XDG_STATE_HOME:-$HOME/.local/state}/st2/default/catalog"
102-
mkdir -p "$CATALOG/agents/<host>/<identity>" "$CATALOG/_templates"
103-
cp examples/native/agent-codex.kdl "$CATALOG/agents/<host>/<identity>/agent.kdl"
104-
${EDITOR:-vi} "$CATALOG/agents/<host>/<identity>/agent.kdl"
102+
bundle="$(mktemp -d)"
103+
mkdir -p "$bundle/assets"
104+
cp examples/native/agent-codex.kdl "$bundle/agent.kdl"
105+
cp ./composed-AGENTS.md "$bundle/assets/AGENTS.md"
106+
${EDITOR:-vi} "$bundle/agent.kdl"
107+
st2 agent publish --catalog "$CATALOG" --bundle "$bundle" --expect-absent --json
105108
```
106109

107-
Replace `<host>`, `<identity>`, `<workspace>`, and `<boot prompt>`. Add every file referenced by
108-
`copy` under `$CATALOG/_templates`. The maintained declaration does not add workspace trust.
109-
`compile-agent` also omits trust by default. Pass `--harness codex --trust-workspace` to opt in to an
110-
argv-local Codex `projects` trust override. The generator serializes the declared workspace as the
111-
exact decoded key; other harnesses reject the flag. This is a launch convention inside opaque argv,
112-
not agent-spec grammar enforced by st2.
110+
Replace `<host>`, `<identity>`, `<workspace>`, and `<boot prompt>`. Include every file referenced by
111+
`copy` in the bundle. For a later declaration-only update, publish `agent.kdl` with the current
112+
declaration's SHA-256 via `--spec ... --expect-sha256 HEX`; sibling assets and state are preserved.
113113

114114
The compact declaration shape is:
115115

@@ -128,7 +128,7 @@ agent "<identity>" {
128128
ding
129129
130130
render {
131-
copy "_templates/<host>.<identity>.AGENTS.md" "AGENTS.md"
131+
copy "assets/AGENTS.md" "AGENTS.md"
132132
json-upsert ".codex/hooks.json" #"""
133133
{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"$ST_HOOKS/codex-session-start.sh","timeout":5}]}]}}
134134
"""#
@@ -183,7 +183,7 @@ st2 rename <stable-id> --clear
183183
```
184184

185185
These commands preserve unrelated KDL bytes and serialize local writers through the persistent
186-
private `.st2/presentation-authoring.lock`. They refuse TOML, JSON, and
186+
shared `.st2/catalog-authoring.lock`. They refuse TOML, JSON, and
187187
explicitly `meta { managed-by "nix" }` targets. Nix generators must emit that marker before the
188188
compatible st2 binary is activated. In the trusted single-operator fleet, caller-supplied
189189
`ST_AGENT` limits an invocation to itself or declared descendants; it is a guardrail rather than
@@ -415,7 +415,7 @@ ls, up, down, validate, doctor
415415
message, ding, agents, status, context, resource, rename, describe
416416
env, pty, shell, pretrust
417417
hooks, service, eval
418-
compile-agent (experimental)
418+
agent publish
419419
completions
420420
```
421421

@@ -475,5 +475,6 @@ interviewer reply at-or-after the exact kickoff receipt completes it. Canonical
475475
verdict. Without the directive, Agent Spec-shaped files inside a fixture remain inert and compact
476476
evals retain their flat bus and completion semantics.
477477

478-
`st2 compile-agent` remains experimental. Hand-authored KDL is the canonical st2 authoring
479-
interface, and generated output must be reviewed before materialization.
478+
`st2 agent publish --catalog ROOT (--spec FILE | --bundle DIR) (--expect-absent |
479+
--expect-sha256 HEX)` is the sole catalog-declaration writer. The publisher admits the complete
480+
prospective catalog under a compare-and-swap lock before making one atomic change.

docs/vrs/.decisions/0002-stable-agent-identity-and-mutable-presentation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ descendants; this is an operational guardrail, not an authenticated capability,
3737
and absence selects the operator path. Declarations explicitly marked Nix-owned
3838
remain writable only at their Nix source, and Nix emitters must publish that
3939
marker before authoring is activated. st2 serializes these
40-
edits with the private persistent `.st2/presentation-authoring.lock`, preserves
40+
edits with the shared persistent `.st2/catalog-authoring.lock`, preserves
4141
unrelated source bytes, detects stale source, and atomically replaces the
4242
declaration. The lock covers cooperating local st2 writers in one POSIX
4343
filesystem/kernel lock domain; it does not claim exclusion across independently

docs/vrs/requirements.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ accepted.
134134
inspection exposes every Resource binding without interpreting its type or URI.
135135
Resource-only declaration changes do not alter a task's effective launch
136136
definition and do not stop, replace, or relaunch healthy work.
137+
- **R27 Transactional catalog authoring:** One st2 publication operation admits
138+
exactly one canonical KDL Agent Spec, with explicit host and identity, against
139+
the complete prospective catalog. Publication is compare-and-swap, durable,
140+
and atomic: readers observe either the previous declaration set or the next
141+
complete set. Reconciliation holds one coherent declaration snapshot through
142+
materialization, runtime observation, planning, and execution, so a retirement
143+
cannot commit and then be followed by a launch from stale catalog input.
144+
A durable incomplete-apply marker fences every declaration-plane snapshot and
145+
action after a crashed whole-catalog apply; a resident supervisor stays alive
146+
but performs zero lifecycle actions until the transaction is completed.
147+
Presence, messages, context, and Resource state remain independently writable
148+
and are never serialized behind catalog authoring.
137149
- **R23 Fail-closed task inventory:** One read-only machine command exposes
138150
every desired local PTY and exec task by agent identity, task name, runtime
139151
id, kind, lifecycle, retirement, desired state, runtime state, PID, creation
@@ -161,8 +173,8 @@ accepted.
161173
- **R25 Constrained presentation authoring:** `st2 rename` and `st2 describe`
162174
set or clear only their corresponding direct field in one canonical KDL
163175
declaration selected by stable identity. They preserve unrelated source
164-
bytes, serialize cooperating local writers through the persistent private
165-
`.st2/presentation-authoring.lock`, reject a stale source before atomic
176+
bytes, serialize cooperating local writers through the persistent shared
177+
`.st2/catalog-authoring.lock`, reject a stale source before atomic
166178
replacement, fsync the result, and return classified receipts. The lock inode
167179
is never removed or stale-recovered and defines one local POSIX
168180
filesystem/kernel exclusion domain; it is not cross-host coordination or OS

docs/vrs/spec.md

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ st2 neither reads, writes, migrates, nor interprets it.
8989
`--clear`. They edit canonical KDL only. The operation:
9090

9191
1. acquires the persistent exclusive
92-
`<catalog>/.st2/presentation-authoring.lock` before discovery;
92+
`<catalog>/.st2/catalog-authoring.lock` before discovery;
9393
2. resolves exactly one declaration and applies the caller-supplied `ST_AGENT`
9494
self/descendant guardrail when present;
9595
3. refuses declarations explicitly marked `meta { managed-by "nix" }`,
@@ -105,7 +105,7 @@ marker before activating a binary with authoring commands; st2 cannot infer an
105105
unmarked generator from KDL bytes.
106106

107107
The lock file is a persistent real inode and is never removed or stale-recovered.
108-
It serializes cooperating st2 presentation writers in one local POSIX
108+
It serializes cooperating st2 declaration readers and writers in one local POSIX
109109
filesystem/kernel lock domain. Direct same-UID writes and independently
110110
synchronized hosts do not participate; the source recheck detects observed
111111
interference but is not a distributed CAS or lock service. The classified
@@ -153,6 +153,58 @@ renderer integration in [st2#61](https://github.com/compoundingtech/st2/issues/6
153153
and the portable Agent Spec envelope in
154154
[evals#41](https://github.com/compoundingtech/evals/issues/41).
155155

156+
## Transactional catalog authoring
157+
158+
`st2 agent publish --catalog ROOT (--spec FILE | --bundle DIR)
159+
(--expect-absent | --expect-sha256 HEX) --json` is the sole supported
160+
declaration writer. It accepts exactly one canonical KDL `agent` node with an
161+
explicit, path-safe host and identity. st2 no longer exposes an intent compiler:
162+
external renderers own the transformation from human intent to exact Agent Spec
163+
bytes or a create-only publication bundle.
164+
165+
The persistent `<catalog>/.st2/catalog-authoring.lock` defines one cooperative
166+
read/write transaction domain:
167+
168+
```text
169+
publisher (EX) : snapshot input -> CAS -> full-catalog admission -> atomic publish + fsync
170+
reader (SH) : discover -> materialize/observe -> plan -> execute
171+
state plane : message | context | Resource | status (unlocked)
172+
```
173+
174+
The publisher derives the destination from the captured declaration, replaces
175+
only `agent.kdl` for a hash-authorized update, and preserves all sibling runtime
176+
state. A bundle is create-only and is renamed from a hidden same-filesystem
177+
stage; retry reports `unchanged` only when every projected bundle file already
178+
matches. `--expect-absent` is idempotent for identical input.
179+
`--expect-sha256` rejects a stale writer. Full-catalog admission rejects any
180+
structural validation error before publication. The typed result is
181+
`published` or `unchanged`.
182+
183+
The lock file is never removed: replacing its inode would split the transaction
184+
domain for processes that already hold it open. Reconciliation holds SH from
185+
discovery through execution. Validation, doctor, roster, listing,
186+
materialization-only, targeted reconciliation, and catalog teardown take a
187+
coherent SH snapshot. State-plane commands deliberately do not: their atomic
188+
files remain live while a declaration is admitted.
189+
190+
`<catalog>/.st2/catalog-apply-incomplete` is the durable whole-catalog
191+
transaction fence. Any presence is authoritative, including malformed content.
192+
The reserved canonical record is:
193+
194+
```json
195+
{"schema":"st2.catalog-apply-incomplete.v1","desiredStorePath":"/nix/store/..."}
196+
```
197+
198+
After taking its authoring lock, st2 refuses publication, validation,
199+
materialization, teardown, roster, doctor, and catalog listing while the marker
200+
exists. One-shot and selected reconcile fail explicitly. A resident supervisor
201+
instead remains alive, reports a skipped/incomplete pass, and performs no
202+
runtime observation or lifecycle action, avoiding a service restart storm.
203+
Message, context, Resource, and status operations remain available. The future
204+
whole-catalog apply command owns marker creation, admission, durable apply,
205+
verification, and clearing inside one transaction; external lock execution and
206+
bypass flags are not part of the contract.
207+
156208
## Host-local scheduling and supervision
157209

158210
```text

examples/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
# Native st2 examples
22

33
[`native/`](native/) contains the maintained, hand-authored Codex and Claude agent declaration
4-
shapes. Copy one to:
4+
shapes. Render one outside the catalog as:
55

66
```text
77
<catalog>/agents/<host>/<identity>/agent.kdl
88
```
99

10-
Replace every placeholder, add its referenced files under `<catalog>/_templates/`, then gate the
11-
result before starting a process:
10+
Replace every placeholder, put its referenced files in the bundle's `assets/` directory, then publish
11+
and gate the result before starting a process:
1212

1313
```sh
1414
st2 hooks install
1515
st2 hooks verify
16+
st2 agent publish --catalog <catalog> --bundle <bundle> --expect-absent --json
1617
st2 validate --catalog <catalog>
1718
st2 up --catalog <catalog> --host <host> --materialize-only
1819
st2 up --catalog <catalog> --host <host> --once
1920
```
2021

21-
Hand-authored KDL is canonical. `st2 compile-agent` is an experimental generation aid; inspect all
22-
of its KDL and workspace targets before use.
22+
Canonical KDL is the boundary; st2 does not compile intent. Inspect all KDL and workspace targets
23+
before publication.

examples/native/README.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,22 @@ These maintained, hand-authored declarations are the canonical starting points:
1010

1111
The examples use `<host>`, `<identity>`, and `<workspace>` placeholders. st2 provides `CATALOG`,
1212
`ST_ROOT`, `PTY_ROOT`, and `ST_HOOKS` when it starts a task, so hook declarations contain no
13-
machine-specific install paths. Copy the appropriate file into
14-
`<catalog>/agents/<host>/<identity>/agent.kdl`, replace every placeholder, and add the referenced
15-
catalog-owned templates. `role` is optional metadata; `supervisor` is optional runtime routing.
16-
Uncomment them when the agent has an assigned role or reports to another bus identity. In the Codex
17-
declaration, workspace trust is absent by default. The experimental generator adds an argv-local
18-
Codex project trust override only with `compile-agent --harness codex --trust-workspace`. It preserves
19-
the exact decoded workspace bytes as the key. st2 treats that argv as opaque; the trust flag is not
20-
part of generic catalog validation.
13+
machine-specific install paths. Copy the appropriate file outside the catalog, replace every
14+
placeholder, assemble it and the referenced files under the publication bundle's `assets/`
15+
directory, then publish the bundle. `role` is optional metadata; `supervisor` is optional runtime routing.
16+
Uncomment them when the seat has an assigned role or reports to another bus identity.
2117

2218
## Lifecycle
2319

24-
`compile-agent` is an experimental generation aid. It writes one declaration, catalog-owned
25-
templates, and the agent's `resources/{inbox,archive,context,links}` directories without changing
26-
the workspace. Inspect all generated KDL and workspace targets before use.
20+
st2 accepts exact canonical KDL or a create-only bundle; it does not compile human intent. Inspect
21+
all KDL and workspace targets before publication.
2722

28-
Use this sequence for hand-authored or generated declarations:
23+
Use this sequence:
2924

3025
```sh
3126
st2 hooks install
3227
st2 hooks verify
28+
st2 agent publish --catalog <catalog> --bundle <bundle> --expect-absent --json
3329
st2 validate <catalog>
3430
st2 up <catalog> --host <host> --materialize-only
3531
st2 up <catalog> --host <host> --once

examples/native/agent-claude.kdl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ agent "<identity>" {
1717
ding
1818

1919
render {
20-
copy "_templates/<host>.<identity>.persona.md" ".st2/PERSONA.md"
21-
copy "_templates/bus.st2.md" ".st2/bus.md"
20+
copy "assets/PERSONA.md" ".st2/PERSONA.md"
21+
copy "assets/bus.st2.md" ".st2/bus.md"
2222
ensure-line ".claude/rules/st2.md" "@../../.st2/PERSONA.md"
2323
ensure-line ".claude/rules/st2.md" "@../../.st2/bus.md"
2424
json-upsert ".claude/settings.local.json" #"""

0 commit comments

Comments
 (0)