st2 now owns ST_AGENT and forces it to the runner-derived bus id <host>.<team>.<seat>. Any authored value that differs is rejected at admission, before any model starts. This is an intentional upstream boundary, not an st2 bug (see Why this is intended below) — but it silently invalidates most of this corpus.
40 of the 59 cells declare the short env { ST_AGENT "<team>.<seat>" } form, across 94 seat declarations, and every one of them now aborts its eval at boot:
Error: agent '<host>.t.alpha' task 'agent' declares conflicting ST_AGENT 't.alpha'; expected runner-owned value '<host>.t.alpha'
The migration is a one-line deletion per seat. The runner injects ST_AGENT unconditionally, so every prompt that uses $ST_AGENT keeps working with no other change.
Reproduction
Free and self-contained — every seat is exec sleep 5, no model is ever launched. Needs only st2 on PATH; does not touch this repo.
#!/usr/bin/env bash
cell=$(mktemp -d); mkdir -p "$cell/fixture/alpha" "$cell/fixture/beta" "$cell/fixture/j"
echo 'Any body.' > "$cell/task.md"
HOST=$(hostname)
run() { # run <label> <team-body> [<extra-eval-body>]
printf '\n--- %s\n' "$1"
cat > "$cell/cell.kdl" <<KDL
team "t" {
$2
}
eval { copy "./fixture"; message { from "requester"; to "t.alpha"; content "./task.md" }; max-timeout "8s"
${3:-}
judges { judge "the team booted" { exec "true" } } }
KDL
st2 eval "$cell" 2>&1 | grep -E '^(== boot|Error:| \[|SCORE|VERDICT)' | sed "s/$HOST/<host>/g;s/^/ /"
}
st2 --version
run 'CASE A — a team seat declares the short form ST_AGENT "t.alpha"' \
' agent "alpha" { workspace "./alpha"; env { ST_AGENT "t.alpha" }; command "exec sleep 5" }'
run 'CASE B — control: the env block omitted entirely' \
' agent "alpha" { workspace "./alpha"; command "exec sleep 5" }'
run "CASE C — control: ST_AGENT declared as the runner-owned <host>.t.alpha" \
" agent \"alpha\" { workspace \"./alpha\"; env { ST_AGENT \"$HOST.t.alpha\" }; command \"exec sleep 5\" }"
run 'CASE D — blast radius: alpha valid, beta conflicting' \
' agent "alpha" { workspace "./alpha"; command "exec sleep 5" }
agent "beta" { workspace "./beta"; env { ST_AGENT "t.beta" }; command "exec sleep 5" }'
run 'CASE E — an agent declared inside eval{} with a bare ST_AGENT' \
' agent "alpha" { workspace "./alpha"; command "exec sleep 5" }' \
' agent "judge" { workspace "./j"; env { ST_AGENT "judge" }; command "exec sleep 5" }'
rm -rf "$cell"
Actual output (hostname elided as <host>):
st2 0.1.0+b868a07 — committed 4 days ago
--- CASE A — a team seat declares the short form ST_AGENT "t.alpha"
== boot team (1 agents) ==
Error: agent '<host>.t.alpha' task 'agent' declares conflicting ST_AGENT 't.alpha'; expected runner-owned value '<host>.t.alpha'
--- CASE B — control: the env block omitted entirely
== boot team (1 agents) ==
[PASS] the team booted (exit 0)
SCORE: 1 PASS / 0 FAIL / 1 gating judges
VERDICT: PASS
--- CASE C — control: ST_AGENT declared as the runner-owned <host>.t.alpha
== boot team (1 agents) ==
[PASS] the team booted (exit 0)
SCORE: 1 PASS / 0 FAIL / 1 gating judges
VERDICT: PASS
--- CASE D — blast radius: alpha valid, beta conflicting
== boot team (2 agents) ==
Error: agent '<host>.t.beta' task 'agent' declares conflicting ST_AGENT 't.beta'; expected runner-owned value '<host>.t.beta'
--- CASE E — an agent declared inside eval{} with a bare ST_AGENT
== boot team (2 agents) ==
Error: agent '<host>.judge' task 'agent' declares conflicting ST_AGENT 'judge'; expected runner-owned value '<host>.judge'
What each case establishes:
- A — the form this corpus uses is rejected at boot. The eval exits non-zero with the reason printed, so it fails closed and loudly (no silent pass).
- B — this is the migration. Deleting the
env block entirely boots cleanly, and $ST_AGENT is still set in the task environment by the runner.
- C — declaring the runner-owned value also boots, but it hard-codes a machine hostname into a cell, so it is not portable. Do not migrate this way.
- D — one conflicting seat aborts the whole eval; the valid sibling is never launched. A partial migration therefore buys nothing: a cell has to be fully clean to run.
- E — an
agent declared inside eval { } rather than team { } goes through the identical check and fails identically. license-mit / license-mit-codex declare env { ST_AGENT "judge" } on exactly this kind of eval-level judge — there is no special case for it, and it needs the same edit.
Why this is intended upstream
- compoundingtech/st2#64 — Decide ownership of stable agent identity at PTY creation — poses the choice explicitly. Direction A. Runner-derived identity: "st2 forces
ST_AGENT=<host.identity> into every task of the agent; authored conflicts either fail validation or lose to the runner." That is the boundary that shipped.
- compoundingtech/st2#192 — One conflicting ST_AGENT aborts the whole reconcile pass, and
st2 up --once still exits 0 — records the provenance: "The behaviour arrives with the runner-owned task identity work; the previous revision supervises both specs and does not fail the pass." The open question there is about st2 up --once's exit status and the scope of the abort, not about whether the runner owns the value. st2 eval (Case A/D above) already exits non-zero with a clear diagnostic.
The relevant st2 code, on main at a8dc061485ec38e8aca2f3118a05b131254c9663:
-
src/reconcile.rs:220-242, validate_task_identities — for every running local spec, task.env.get("ST_AGENT") must either be absent or equal spec.bus_id(this_host); otherwise TaskIdentityAdmissionError::Conflict. It is called from reconcile (:395, :505), from src/materialize.rs (:620, :824, :884) and from the eval/up paths in src/run.rs (:1472, :1643, :1746, :1795), so there is no path that skips it.
-
src/reconcile.rs:245-257, runner_task_env — unconditionally inserts ST_AGENT=<bus_id> into the launched task's environment:
let mut env = task.env.clone();
env.insert("ST_AGENT".to_owned(), bus_id.to_owned());
This is why deleting the declaration is safe: the variable the prompts read is supplied by the runner, with the correct host-qualified value that a cell cannot know at authoring time.
st2 validate reports the same conflict, so a migration can be checked without running an eval:
$ st2 validate --catalog <root> --host h1
ERROR agents/h1/alpha/agent.kdl: agent 'h1.alpha' task 'agent' declares conflicting ST_AGENT 'alpha'; expected runner-owned value 'h1.alpha'
─ 1 error, 0 warnings across 1 agent
Scope in this repo
Measured at 12c96acbbdc654f27ccb27cae064ef4311d4f7f7:
grep -lE 'env[[:space:]]*\{[[:space:]]*ST_AGENT' cells/*/*.kdl | wc -l # -> 40 cells
grep -hcE 'env[[:space:]]*\{[[:space:]]*ST_AGENT' cells/*/*.kdl | paste -sd+ | bc # -> 94 seats
Every declared value is short: 92 are two-part <team>.<seat>, 2 are bare (ST_AGENT "judge"), and zero are host-qualified — so all 94 conflict on every host.
The 40 affected cells (seat counts)
Not affected: the 7 canonical agent.kdl files inside cell fixtures that declare ST_AGENT (context-resource-continuity, hook-integrity ×2, managed-agent-color-env ×2, st2-network, st2-doctor-structure) all declare an explicit host and a fully host-qualified value matching it — e.g. host "cr" with ST_AGENT "cr.agent", host "color" with ST_AGENT "color.ambient". Those already equal the runner-owned bus id, so they pass validate_task_identities unchanged. managed-agent-color-env in particular appears to exercise env precedence deliberately; nothing in this issue asks for it to change. (render-target-safety's fixture spec only consumes $ST_AGENT in a render target, file ".st2/generated.txt" "AGENT=$ST_AGENT" — it declares nothing, and the runner-injected value keeps that working.)
Separate, overlapping breakage worth knowing about while migrating
Migrating ST_AGENT will not by itself make every affected cell green. compoundingtech/st2#248 reports that a compact-team eval's flat message bus is disabled by any spec-shaped file under the run catalog, and compoundingtech/st2#247 reports that any package.json with a top-level type key qualifies. 22 of the 59 cells here ship such a fixture manifest and all 22 declare ding. That is an upstream defect, not a corpus problem, and it is tracked there — noted only so a failing post-migration run is not mistaken for an incomplete migration.
Suggested action
Delete the env { ST_AGENT "…" } declaration from all 94 seats (including the eval-level judges), keeping any other variables in the same env block. Do not substitute a hostname. Prompts referring to $ST_AGENT need no change.
Versions
st2 --version: 0.1.0+b868a07 — b868a0710a59fc40efd44a3e96bc92903d1376bb. All output above was produced by this binary.
- st2 source citations:
main at a8dc061485ec38e8aca2f3118a05b131254c9663 (2026-08-11). Caveat: that tree was source-inspected only, not built or run; validate_task_identities and runner_task_env are unchanged there relative to the binary under test.
- Last revision observed booting the short form: st2
9887b2842222def0838c2cd82e6c24c218f7efa6 (2026-07-28), consistent with the provenance note in st2#192.
- Corpus counts: this repo at
12c96acbbdc654f27ccb27cae064ef4311d4f7f7.
- OS: Linux 6.18.33.
st2 now owns
ST_AGENTand forces it to the runner-derived bus id<host>.<team>.<seat>. Any authored value that differs is rejected at admission, before any model starts. This is an intentional upstream boundary, not an st2 bug (see Why this is intended below) — but it silently invalidates most of this corpus.40 of the 59 cells declare the short
env { ST_AGENT "<team>.<seat>" }form, across 94 seat declarations, and every one of them now aborts its eval at boot:The migration is a one-line deletion per seat. The runner injects
ST_AGENTunconditionally, so every prompt that uses$ST_AGENTkeeps working with no other change.Reproduction
Free and self-contained — every seat is
exec sleep 5, no model is ever launched. Needs onlyst2onPATH; does not touch this repo.Actual output (hostname elided as
<host>):What each case establishes:
envblock entirely boots cleanly, and$ST_AGENTis still set in the task environment by the runner.agentdeclared insideeval { }rather thanteam { }goes through the identical check and fails identically.license-mit/license-mit-codexdeclareenv { ST_AGENT "judge" }on exactly this kind of eval-level judge — there is no special case for it, and it needs the same edit.Why this is intended upstream
ST_AGENT=<host.identity>into every task of the agent; authored conflicts either fail validation or lose to the runner." That is the boundary that shipped.st2 up --oncestill exits 0 — records the provenance: "The behaviour arrives with the runner-owned task identity work; the previous revision supervises both specs and does not fail the pass." The open question there is aboutst2 up --once's exit status and the scope of the abort, not about whether the runner owns the value.st2 eval(Case A/D above) already exits non-zero with a clear diagnostic.The relevant st2 code, on
mainata8dc061485ec38e8aca2f3118a05b131254c9663:src/reconcile.rs:220-242,validate_task_identities— for every running local spec,task.env.get("ST_AGENT")must either be absent or equalspec.bus_id(this_host); otherwiseTaskIdentityAdmissionError::Conflict. It is called from reconcile (:395,:505), fromsrc/materialize.rs(:620,:824,:884) and from the eval/up paths insrc/run.rs(:1472,:1643,:1746,:1795), so there is no path that skips it.src/reconcile.rs:245-257,runner_task_env— unconditionally insertsST_AGENT=<bus_id>into the launched task's environment:This is why deleting the declaration is safe: the variable the prompts read is supplied by the runner, with the correct host-qualified value that a cell cannot know at authoring time.
st2 validatereports the same conflict, so a migration can be checked without running an eval:Scope in this repo
Measured at
12c96acbbdc654f27ccb27cae064ef4311d4f7f7:Every declared value is short: 92 are two-part
<team>.<seat>, 2 are bare (ST_AGENT "judge"), and zero are host-qualified — so all 94 conflict on every host.The 40 affected cells (seat counts)
assignment-contract-cold-assignment(2)assignment-contract-cold-focus(2)assignment-contract-cold-resources(2)assignment-contract-handoff-assignment(4)assignment-contract-handoff-focus(4)assignment-contract-handoff-resources(4)assignment-contract-hot-assignment(3)assignment-contract-hot-focus(3)assignment-contract-hot-resources(3)crash-ding(7)ding-mode(2)ding-reply(1)docs(2)feature-fit(2)fork-in-the-road(4)fork-in-the-road-codex(4)ghost-bug(2)ghost-bug-codex(2)inbox-hygiene(2)incident-response(2)license-mit(3)license-mit-codex(3)migration(2)poisoned-pr(2)poisoned-pr-codex(2)restart-continuity(3)security-audit(2)signal-rename(4)signal-rename-codex(4)skill-inheritance(1)test-writing(2)vrs-cross-file-absent(1)vrs-cross-file-present(1)vrs-definition-of-done-absent(1)vrs-definition-of-done-present(1)vrs-scope-drift-absent(1)vrs-scope-drift-present(1)vrs-scope-pressure-absent(1)vrs-scope-pressure-present(1)weird-git-setup(1)Not affected: the 7 canonical
agent.kdlfiles inside cell fixtures that declareST_AGENT(context-resource-continuity,hook-integrity×2,managed-agent-color-env×2,st2-network,st2-doctor-structure) all declare an explicithostand a fully host-qualified value matching it — e.g.host "cr"withST_AGENT "cr.agent",host "color"withST_AGENT "color.ambient". Those already equal the runner-owned bus id, so they passvalidate_task_identitiesunchanged.managed-agent-color-envin particular appears to exercise env precedence deliberately; nothing in this issue asks for it to change. (render-target-safety's fixture spec only consumes$ST_AGENTin a render target,file ".st2/generated.txt" "AGENT=$ST_AGENT"— it declares nothing, and the runner-injected value keeps that working.)Separate, overlapping breakage worth knowing about while migrating
Migrating
ST_AGENTwill not by itself make every affected cell green. compoundingtech/st2#248 reports that a compact-team eval's flat message bus is disabled by any spec-shaped file under the run catalog, and compoundingtech/st2#247 reports that anypackage.jsonwith a top-leveltypekey qualifies. 22 of the 59 cells here ship such a fixture manifest and all 22 declareding. That is an upstream defect, not a corpus problem, and it is tracked there — noted only so a failing post-migration run is not mistaken for an incomplete migration.Suggested action
Delete the
env { ST_AGENT "…" }declaration from all 94 seats (including the eval-level judges), keeping any other variables in the sameenvblock. Do not substitute a hostname. Prompts referring to$ST_AGENTneed no change.Versions
st2 --version:0.1.0+b868a07—b868a0710a59fc40efd44a3e96bc92903d1376bb. All output above was produced by this binary.mainata8dc061485ec38e8aca2f3118a05b131254c9663(2026-08-11). Caveat: that tree was source-inspected only, not built or run;validate_task_identitiesandrunner_task_envare unchanged there relative to the binary under test.9887b2842222def0838c2cd82e6c24c218f7efa6(2026-07-28), consistent with the provenance note in st2#192.12c96acbbdc654f27ccb27cae064ef4311d4f7f7.