Skip to content

Commit 4e25f56

Browse files
committed
Document native bus adoption path
1 parent e6ff814 commit 4e25f56

2 files changed

Lines changed: 253 additions & 18 deletions

File tree

README.md

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ The compact declaration shape is:
6565
```kdl
6666
agent "<identity>" {
6767
host "<host>"
68-
role "worker"
6968
workspace "<workspace>"
70-
supervisor "<supervisor-bus-id>"
69+
// Optional metadata:
70+
// role "worker"
71+
// supervisor "<supervisor-bus-id>"
7172
env { ST_AGENT "<host>.<identity>" }
7273
command #"exec codex --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust '<boot prompt>'"#
7374
ding
@@ -142,6 +143,17 @@ st2 agents --json --enrich
142143
st2 context read --full
143144
```
144145

146+
For a catalog-backed agent, every native bus operation resolves the same agent directory used by
147+
the roster: presence is `<agent-dir>/status`, while unread messages, archive receipts, context, and
148+
links live under `<agent-dir>/resources/`. The flat `<root>/<identity>` layout remains only as the
149+
intentional catalog-less fallback used by isolated folder evals.
150+
151+
Adopters should cut directly to this native layout rather than stage through a retired compatibility
152+
transport. Before launching a migrated identity, install and verify hooks, validate and materialize
153+
its hand-authored declaration, stop the predecessor transport, and decide how any unread legacy
154+
backlog will be archived or forwarded. Never run old and native DING owners concurrently for the same
155+
identity.
156+
145157
Native DING watches the recipient inbox and safely stages:
146158

147159
```text
@@ -181,15 +193,18 @@ message, ding, agents, status, context, resource
181193
env, pty, shell, pretrust
182194
hooks, service, eval
183195
compile-agent (experimental)
196+
completions
184197
```
185198

186-
The project ships no completion or manpage generator.
199+
`st2 completions <shell>` emits completions from the live command tree. No generated completion or
200+
manpage tree is committed.
187201

188202
## Clean-room verification
189203

190-
The test suite builds a temporary `PATH` containing only the current `st2` binary plus `pty`,
191-
`codex`, and `claude` shims. It verifies the supported help/doctor/native authoring surface without
192-
any retired binary:
204+
The test suite builds a temporary `PATH` containing only the current `st2` binary, required Git,
205+
and `pty`, `codex`, and `claude` shims. It installs and verifies a scratch hook receipt, instantiates
206+
both maintained hand-authored KDL examples in fresh Git workspaces, validates and materializes them
207+
twice, and verifies the help/doctor/native authoring surface without any retired binary:
193208

194209
```sh
195210
cargo test --test native_only --all-features
@@ -204,7 +219,7 @@ cargo test --all-targets --all-features
204219

205220
## Eval corpus
206221

207-
The evidence ledger is pinned at
222+
The Codex evidence ledger is pinned at
208223
[`67b45d2694ac40762b09f51bf625d092ab68de74`](https://github.com/compoundingtech/evals/blob/67b45d2694ac40762b09f51bf625d092ab68de74/HARNESS-MATRIX.md).
209224

210225
Current native Codex examples:
@@ -219,14 +234,21 @@ Current native Codex examples:
219234
The pinned [native static checker](https://github.com/compoundingtech/evals/blob/67b45d2694ac40762b09f51bf625d092ab68de74/bin/check-codex-native.sh)
220235
does not consume model usage.
221236

222-
Claude corpus references are historical and are not native-current examples:
237+
The Claude native-readiness ledger is pinned at
238+
[`a52b68dfe4bdb65a3bb6a1ba51c674476cf197df`](https://github.com/compoundingtech/evals/blob/a52b68dfe4bdb65a3bb6a1ba51c674476cf197df/CLAUDE-NATIVE-READINESS.md).
239+
Its current event-first native examples and gates are immutable at
240+
[`56b86a04837dc176f1a53d9f90dee3f3a7e57499`](https://github.com/compoundingtech/evals/commit/56b86a04837dc176f1a53d9f90dee3f3a7e57499):
241+
242+
- [`ding-reply`](https://github.com/compoundingtech/evals/blob/56b86a04837dc176f1a53d9f90dee3f3a7e57499/cells/ding-reply/ding-reply.kdl):
243+
one Claude seat with a native bare `ding`.
244+
- [`signal-rename`](https://github.com/compoundingtech/evals/blob/56b86a04837dc176f1a53d9f90dee3f3a7e57499/cells/signal-rename/signal-rename.kdl):
245+
one Claude supervisor and three specialists, each with a native bare `ding`.
223246

224-
- [`ding-reply`](https://github.com/compoundingtech/evals/blob/67b45d2694ac40762b09f51bf625d092ab68de74/cells/ding-reply/ding-reply.kdl):
225-
free syntax gate `bash -n cells/ding-reply/judges/*.sh`; authoritative run
226-
`st2 eval ./cells/ding-reply/ --keep`, opt-in paid.
227-
- [`team-standup`](https://github.com/compoundingtech/evals/blob/67b45d2694ac40762b09f51bf625d092ab68de74/cells/team-standup/team-standup.kdl):
228-
free syntax gate `bash -n cells/team-standup/judges/*.sh`; authoritative run
229-
`st2 eval ./cells/team-standup/ --keep`, opt-in paid.
247+
Run `bin/check-claude-native.sh` and `bin/check-claude-reset.sh` from that tree for the free static
248+
acceptance. The gates reject polling language and legacy bus declarations, require event-first DING
249+
guidance, and prove repeatable clean fixture resets. They do not invoke a model. A current-build
250+
`st2 eval` model run remains pending explicit authorization, so these examples have static acceptance
251+
but no current native live-smoke claim.
230252

231-
There is currently no native-current Claude eval KDL or free authoritative folder-eval parser.
232-
Claude conversion, a native static gate, and an explicitly authorized current-build run remain open.
253+
[`team-standup`](https://github.com/compoundingtech/evals/blob/a52b68dfe4bdb65a3bb6a1ba51c674476cf197df/cells/team-standup/team-standup.kdl)
254+
remains a legacy reference to the retired generated-declaration flow, not a current native example.

tests/native_only.rs

Lines changed: 215 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::fs;
2-
use std::os::unix::fs::PermissionsExt;
2+
use std::os::unix::fs::{PermissionsExt, symlink};
33
use std::path::{Path, PathBuf};
44
use std::process::Command;
55

@@ -10,6 +10,12 @@ fn executable(path: &Path, body: &str) {
1010

1111
fn clean_path() -> tempfile::TempDir {
1212
let bin = tempfile::tempdir().unwrap();
13+
symlink(env!("CARGO_BIN_EXE_st2"), bin.path().join("st2")).unwrap();
14+
let git = std::env::split_paths(&std::env::var_os("PATH").unwrap())
15+
.map(|dir| dir.join("git"))
16+
.find(|path| path.is_file())
17+
.expect("the native authoring guide requires git on PATH");
18+
symlink(git, bin.path().join("git")).unwrap();
1319
executable(
1420
&bin.path().join("pty"),
1521
"#!/bin/sh\nif [ \"$1\" = \"list\" ]; then printf '[]\\n'; fi\nexit 0\n",
@@ -19,6 +25,19 @@ fn clean_path() -> tempfile::TempDir {
1925
bin
2026
}
2127

28+
fn clean_st2(bin: &Path, state: &Path, hooks: &Path) -> Command {
29+
let mut command = Command::new(bin.join("st2"));
30+
command
31+
.env("PATH", bin)
32+
.env("XDG_STATE_HOME", state)
33+
.env("ST_HOOKS", hooks)
34+
.env_remove("CATALOG")
35+
.env_remove("ST_ROOT")
36+
.env_remove("PTY_ROOT")
37+
.env_remove("ST_AGENT");
38+
command
39+
}
40+
2241
fn native_catalog(root: &Path, workspace: &Path) {
2342
let declaration = root.join("agents/h/worker/agent.kdl");
2443
fs::create_dir_all(declaration.parent().unwrap()).unwrap();
@@ -33,6 +52,193 @@ fn native_catalog(root: &Path, workspace: &Path) {
3352
.unwrap();
3453
}
3554

55+
#[test]
56+
fn clean_path_executes_the_maintained_native_authoring_guide() {
57+
let bin = clean_path();
58+
let tmp = tempfile::tempdir().unwrap();
59+
let state = tmp.path().join("state");
60+
let hooks = tmp.path().join("hooks");
61+
let catalog = tmp.path().join("catalog");
62+
let codex_workspace = tmp.path().join("codex-workspace");
63+
let claude_workspace = tmp.path().join("claude-workspace");
64+
fs::create_dir_all(catalog.join("agents/clean/codex")).unwrap();
65+
fs::create_dir_all(catalog.join("agents/clean/claude")).unwrap();
66+
fs::create_dir_all(catalog.join("_templates")).unwrap();
67+
fs::create_dir_all(&codex_workspace).unwrap();
68+
fs::create_dir_all(&claude_workspace).unwrap();
69+
70+
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
71+
let codex = fs::read_to_string(manifest.join("examples/native/agent-codex.kdl"))
72+
.unwrap()
73+
.replace("<identity>", "codex")
74+
.replace("<host>", "clean")
75+
.replace("<workspace>", &codex_workspace.display().to_string())
76+
.replace("<boot prompt>", "boot");
77+
let claude = fs::read_to_string(manifest.join("examples/native/agent-claude.kdl"))
78+
.unwrap()
79+
.replace("<identity>", "claude")
80+
.replace("<host>", "clean")
81+
.replace("<workspace>", &claude_workspace.display().to_string())
82+
.replace("<boot prompt>", "boot");
83+
fs::write(
84+
catalog.join("agents/clean/codex/agent.kdl"),
85+
codex.as_bytes(),
86+
)
87+
.unwrap();
88+
fs::write(
89+
catalog.join("agents/clean/claude/agent.kdl"),
90+
claude.as_bytes(),
91+
)
92+
.unwrap();
93+
fs::write(
94+
catalog.join("_templates/clean.codex.AGENTS.md"),
95+
"# Clean Codex agent\n",
96+
)
97+
.unwrap();
98+
fs::write(
99+
catalog.join("_templates/clean.claude.persona.md"),
100+
"# Clean Claude persona\n",
101+
)
102+
.unwrap();
103+
fs::copy(
104+
manifest.join("templates/bus.st2.md"),
105+
catalog.join("_templates/bus.st2.md"),
106+
)
107+
.unwrap();
108+
109+
for workspace in [&codex_workspace, &claude_workspace] {
110+
let git = Command::new(bin.path().join("git"))
111+
.args(["init", "-q"])
112+
.arg(workspace)
113+
.env("PATH", bin.path())
114+
.status()
115+
.unwrap();
116+
assert!(git.success());
117+
}
118+
119+
let install = clean_st2(bin.path(), &state, &hooks)
120+
.args(["hooks", "install"])
121+
.output()
122+
.unwrap();
123+
assert!(
124+
install.status.success(),
125+
"{}",
126+
String::from_utf8_lossy(&install.stderr)
127+
);
128+
let verify = clean_st2(bin.path(), &state, &hooks)
129+
.args(["hooks", "verify"])
130+
.output()
131+
.unwrap();
132+
assert!(
133+
verify.status.success(),
134+
"{}",
135+
String::from_utf8_lossy(&verify.stderr)
136+
);
137+
138+
let validate = clean_st2(bin.path(), &state, &hooks)
139+
.arg("validate")
140+
.arg("--catalog")
141+
.arg(&catalog)
142+
.output()
143+
.unwrap();
144+
assert!(
145+
validate.status.success(),
146+
"stdout:\n{}\nstderr:\n{}",
147+
String::from_utf8_lossy(&validate.stdout),
148+
String::from_utf8_lossy(&validate.stderr)
149+
);
150+
151+
let materialize = || {
152+
clean_st2(bin.path(), &state, &hooks)
153+
.arg("up")
154+
.arg("--catalog")
155+
.arg(&catalog)
156+
.args(["--host", "clean", "--materialize-only"])
157+
.output()
158+
.unwrap()
159+
};
160+
let first = materialize();
161+
assert!(
162+
first.status.success(),
163+
"stdout:\n{}\nstderr:\n{}",
164+
String::from_utf8_lossy(&first.stdout),
165+
String::from_utf8_lossy(&first.stderr)
166+
);
167+
let codex_settings = fs::read(codex_workspace.join(".codex/hooks.json")).unwrap();
168+
let claude_settings = fs::read(claude_workspace.join(".claude/settings.local.json")).unwrap();
169+
let second = materialize();
170+
assert!(
171+
second.status.success(),
172+
"stdout:\n{}\nstderr:\n{}",
173+
String::from_utf8_lossy(&second.stdout),
174+
String::from_utf8_lossy(&second.stderr)
175+
);
176+
assert_eq!(
177+
fs::read(codex_workspace.join(".codex/hooks.json")).unwrap(),
178+
codex_settings
179+
);
180+
assert_eq!(
181+
fs::read(claude_workspace.join(".claude/settings.local.json")).unwrap(),
182+
claude_settings
183+
);
184+
185+
let receipt: serde_json::Value =
186+
serde_json::from_slice(&fs::read(hooks.join("current.json")).unwrap()).unwrap();
187+
let selected = hooks.join(receipt["directory"].as_str().unwrap());
188+
for (settings, names) in [
189+
(
190+
serde_json::from_slice::<serde_json::Value>(&codex_settings).unwrap(),
191+
vec![
192+
"codex-session-start.sh",
193+
"codex-pre-compact.sh",
194+
"codex-stop.sh",
195+
],
196+
),
197+
(
198+
serde_json::from_slice::<serde_json::Value>(&claude_settings).unwrap(),
199+
vec![
200+
"claude-session-start.sh",
201+
"claude-pre-compact.sh",
202+
"claude-stop-failure.sh",
203+
],
204+
),
205+
] {
206+
let encoded = settings.to_string();
207+
for name in names {
208+
assert!(
209+
encoded.contains(&selected.join(name).display().to_string()),
210+
"{name} did not resolve into {}:\n{encoded}",
211+
selected.display()
212+
);
213+
}
214+
}
215+
216+
assert_eq!(
217+
fs::read_to_string(codex_workspace.join("AGENTS.md")).unwrap(),
218+
"# Clean Codex agent\n"
219+
);
220+
assert_eq!(
221+
fs::read_to_string(claude_workspace.join(".st2/PERSONA.md")).unwrap(),
222+
"# Clean Claude persona\n"
223+
);
224+
for workspace in [&codex_workspace, &claude_workspace] {
225+
let status = Command::new(bin.path().join("git"))
226+
.arg("-C")
227+
.arg(workspace)
228+
.args(["status", "--porcelain"])
229+
.env("PATH", bin.path())
230+
.output()
231+
.unwrap();
232+
assert!(status.status.success());
233+
assert!(
234+
status.stdout.is_empty(),
235+
"{} was left dirty:\n{}",
236+
workspace.display(),
237+
String::from_utf8_lossy(&status.stdout)
238+
);
239+
}
240+
}
241+
36242
#[test]
37243
fn clean_path_supports_help_validate_env_and_doctor() {
38244
let bin = clean_path();
@@ -53,7 +259,14 @@ fn clean_path_supports_help_validate_env_and_doctor() {
53259
.unwrap();
54260
assert!(help.status.success());
55261
let help = String::from_utf8_lossy(&help.stdout);
56-
for command in ["validate", "message", "ding", "hooks", "compile-agent"] {
262+
for command in [
263+
"validate",
264+
"message",
265+
"ding",
266+
"hooks",
267+
"compile-agent",
268+
"completions",
269+
] {
57270
assert!(help.contains(command), "missing {command} in help:\n{help}");
58271
}
59272
for removed in [

0 commit comments

Comments
 (0)