Skip to content

Commit 57bb745

Browse files
feat: publish agent specs transactionally
agent-session-id: dev3.dotfiles-cos-misc-agent-runtime-simplification agent-tool: Codex agent-tool-version: 0.145.0 agent-model: gpt-5.6-sol agent-runtime-profile: /home/schickling/.config/coding-agents/profile.json agent-skills-manifest: /nix/store/nk9iml2841l1yjjg0f6f0d3y60zkg1nn-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@4a0515f
1 parent c6846f6 commit 57bb745

21 files changed

Lines changed: 1881 additions & 685 deletions

INVARIANTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ materialization, messaging, DING, or presence must preserve them.
1313
| **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` |
1414
| **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` |
1515
| **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` |
16-
| **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` |
16+
| **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` |
1717
| **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` |
1818
| **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` |
1919
| **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: 16 additions & 11 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

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

9393
```sh
9494
export CATALOG="${XDG_STATE_HOME:-$HOME/.local/state}/st2/default/catalog"
95-
mkdir -p "$CATALOG/agents/<host>/<identity>" "$CATALOG/_templates"
96-
cp examples/native/agent-codex.kdl "$CATALOG/agents/<host>/<identity>/agent.kdl"
97-
${EDITOR:-vi} "$CATALOG/agents/<host>/<identity>/agent.kdl"
95+
bundle="$(mktemp -d)"
96+
mkdir -p "$bundle/assets"
97+
cp examples/native/agent-codex.kdl "$bundle/agent.kdl"
98+
cp ./composed-AGENTS.md "$bundle/assets/AGENTS.md"
99+
${EDITOR:-vi} "$bundle/agent.kdl"
100+
st2 agent publish --catalog "$CATALOG" --bundle "$bundle" --expect-absent --json
98101
```
99102

100-
Replace `<host>`, `<identity>`, `<workspace>`, and `<boot prompt>`. Add every file referenced by
101-
`copy` under `$CATALOG/_templates`.
103+
Replace `<host>`, `<identity>`, `<workspace>`, and `<boot prompt>`. Include every file referenced by
104+
`copy` in the bundle. For a later declaration-only update, publish `agent.kdl` with the current
105+
declaration's SHA-256 via `--spec ... --expect-sha256 HEX`; sibling assets and state are preserved.
102106

103107
The compact declaration shape is:
104108

@@ -115,7 +119,7 @@ agent "<identity>" {
115119
ding
116120
117121
render {
118-
copy "_templates/<host>.<identity>.AGENTS.md" "AGENTS.md"
122+
copy "assets/AGENTS.md" "AGENTS.md"
119123
json-upsert ".codex/hooks.json" #"""
120124
{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"$ST_HOOKS/codex-session-start.sh","timeout":5}]}]}}
121125
"""#
@@ -360,7 +364,7 @@ ls, up, down, validate, doctor
360364
message, ding, agents, status, context, resource
361365
env, pty, shell, pretrust
362366
hooks, service, eval
363-
compile-agent (experimental)
367+
agent publish
364368
completions
365369
```
366370

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

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

docs/vrs/requirements.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,15 @@ accepted.
122122
inspection exposes every Resource binding without interpreting its type or URI.
123123
Resource-only declaration changes do not alter a task's effective launch
124124
definition and do not stop, replace, or relaunch healthy work.
125+
- **R22 Transactional catalog authoring:** One st2 publication operation admits
126+
exactly one canonical KDL Agent Spec, with explicit host and identity, against
127+
the complete prospective catalog. Publication is compare-and-swap, durable,
128+
and atomic: readers observe either the previous declaration set or the next
129+
complete set. Reconciliation holds one coherent declaration snapshot through
130+
materialization, runtime observation, planning, and execution, so a retirement
131+
cannot commit and then be followed by a launch from stale catalog input.
132+
A durable incomplete-apply marker fences every declaration-plane snapshot and
133+
action after a crashed whole-catalog apply; a resident supervisor stays alive
134+
but performs zero lifecycle actions until the transaction is completed.
135+
Presence, messages, context, and Resource state remain independently writable
136+
and are never serialized behind catalog authoring.

docs/vrs/spec.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,58 @@ renderer integration in [st2#61](https://github.com/compoundingtech/st2/issues/6
8989
and the portable Agent Spec envelope in
9090
[evals#41](https://github.com/compoundingtech/evals/issues/41).
9191

92+
## Transactional catalog authoring
93+
94+
`st2 agent publish --catalog ROOT (--spec FILE | --bundle DIR)
95+
(--expect-absent | --expect-sha256 HEX) --json` is the sole supported
96+
declaration writer. It accepts exactly one canonical KDL `agent` node with an
97+
explicit, path-safe host and identity. st2 no longer exposes an intent compiler:
98+
external renderers own the transformation from human intent to exact Agent Spec
99+
bytes or a create-only publication bundle.
100+
101+
The persistent `<catalog>/.st2/catalog-authoring.lock` defines one cooperative
102+
read/write transaction domain:
103+
104+
```text
105+
publisher (EX) : snapshot input -> CAS -> full-catalog admission -> atomic publish + fsync
106+
reader (SH) : discover -> materialize/observe -> plan -> execute
107+
state plane : message | context | Resource | status (unlocked)
108+
```
109+
110+
The publisher derives the destination from the captured declaration, replaces
111+
only `agent.kdl` for a hash-authorized update, and preserves all sibling runtime
112+
state. A bundle is create-only and is renamed from a hidden same-filesystem
113+
stage; retry reports `unchanged` only when every projected bundle file already
114+
matches. `--expect-absent` is idempotent for identical input.
115+
`--expect-sha256` rejects a stale writer. Full-catalog admission rejects any
116+
structural validation error before publication. The typed result is
117+
`published` or `unchanged`.
118+
119+
The lock file is never removed: replacing its inode would split the transaction
120+
domain for processes that already hold it open. Reconciliation holds SH from
121+
discovery through execution. Validation, doctor, roster, listing,
122+
materialization-only, targeted reconciliation, and catalog teardown take a
123+
coherent SH snapshot. State-plane commands deliberately do not: their atomic
124+
files remain live while a declaration is admitted.
125+
126+
`<catalog>/.st2/catalog-apply-incomplete` is the durable whole-catalog
127+
transaction fence. Any presence is authoritative, including malformed content.
128+
The reserved canonical record is:
129+
130+
```json
131+
{"schema":"st2.catalog-apply-incomplete.v1","desiredStorePath":"/nix/store/..."}
132+
```
133+
134+
After taking its authoring lock, st2 refuses publication, validation,
135+
materialization, teardown, roster, doctor, and catalog listing while the marker
136+
exists. One-shot and selected reconcile fail explicitly. A resident supervisor
137+
instead remains alive, reports a skipped/incomplete pass, and performs no
138+
runtime observation or lifecycle action, avoiding a service restart storm.
139+
Message, context, Resource, and status operations remain available. The future
140+
whole-catalog apply command owns marker creation, admission, durable apply,
141+
verification, and clearing inside one transaction; external lock execution and
142+
bypass flags are not part of the contract.
143+
92144
## Host-local scheduling and supervision
93145

94146
```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: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +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.
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.
1616
Uncomment them when the seat has an assigned role or reports to another bus identity.
1717

1818
## Lifecycle
1919

20-
`compile-agent` is an experimental generation aid. It writes one declaration, catalog-owned
21-
templates, and the agent's `resources/{inbox,archive,context,links}` directories without changing
22-
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.
2322

24-
Use this sequence for hand-authored or generated declarations:
23+
Use this sequence:
2524

2625
```sh
2726
st2 hooks install
2827
st2 hooks verify
28+
st2 agent publish --catalog <catalog> --bundle <bundle> --expect-absent --json
2929
st2 validate <catalog>
3030
st2 up <catalog> --host <host> --materialize-only
3131
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" #"""

examples/native/agent-codex.kdl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ agent "<identity>" {
1818
ding
1919

2020
render {
21-
copy "_templates/<host>.<identity>.AGENTS.md" "AGENTS.md"
21+
copy "assets/AGENTS.md" "AGENTS.md"
2222
json-upsert ".codex/hooks.json" #"""
2323
{
2424
"hooks": {

0 commit comments

Comments
 (0)