Skip to content

Commit 5a3beca

Browse files
Retire the linked-record plane; st2 resource becomes the binding surface (#361)
* docs(vrs): retire the linked-record plane; add R35 st2-owned Resource profiles st2 carried two disjoint durable edges both called "resource": Agent Spec Resource bindings, and the link records written by `st2 resource add`. Nothing in the corpus said they were disjoint, and `ontology.md` — the disambiguation index that exists to prevent exactly this — defined neither. Measured on one live catalog of 655 declarations: 889 distinct binding URIs and 233 distinct link-record URLs share zero members, exactly and after normalization on all five schemes both planes used. The planes are disjoint by construction — bindings carry what an agent is for, link records carried what it made — so a shared descriptor type deduplicates nothing. The link plane was write-only (its only reader was its own `ls`/`read` verb), adopted by 12.5% of declarations, decaying, and superseded by `axe work update --artifact/--pty`. It is retired. The drift between the planes was caused by write-cost asymmetry, not by two concepts sharing a word: 45 of 241 link records were dependency edges filed in a products store, because a binding needed publisher authority plus whole-declaration CAS republication while a link record was one file write. Adds the 07-resource tree, ontology entries for Resource, Resource binding, linked record (retired), agent resource directory, and working state, and three decision records. R35 names the st2-owned Resource profiles that R20's "st2 does not register schemes" otherwise forbids, so `working-state` can exist without leaving R20 contradicted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: Claude Code agent-tool-version: 2.1.237 agent-runtime: Claude Code 2.1.237 tooling-profile: dotfiles@11eaf2d-dirty * feat(resource): repoint `st2 resource` at declared bindings with mediated writes `st2 resource` managed link records under `<agent-dir>/resources/links/` — a plane nothing read, that 12.5% of declarations used, and that `axe work update --artifact/--pty` has superseded. It is removed, and the verb now reads and writes the plane that has the spec authority. st2 resource ls [<identity>] [--json] st2 resource read [<identity>] <name> [--json] st2 resource add <name> --uri <uri> --reason <text> [--inactive-reason <text>] st2 resource remove <name> st2 resource rename <old> <new> `ls`/`read` project declared bindings; bindings previously had no human surface at all, which is why agents reaching for `st2 resource ls` measured the other store. The write verbs do read-modify-CAS-publish internally so the caller never renders KDL, and they are the fourth caller of the `agent_author` pattern after streams, desired state, and presentation — full-catalog validation, exact-target selection, compare-and-swap, fail-closed concurrent change, and the Nix-managed refusal all come from the existing machinery. A binding-only change does not relaunch healthy work (R21), and URI bytes are preserved without normalization (R20). `<agent-dir>/resources/` is untouched and remains canonical for an agent's resource files; only `links/` goes. Two `catalog_apply` tests used `st2 resource add` as a state-plane write probe. That is now a declaration-plane write, so they probe `context append` and `message send` instead — the writers that still route through `with_resolved_state_dir`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: Claude Code agent-tool-version: 2.1.237 agent-runtime: Claude Code 2.1.237 tooling-profile: dotfiles@11eaf2d-dirty * fix(resource): drop the dead `links` state-dir entry; correct the ls example ordering `marker_state_exists` still validated `resources/links` as a real directory. st2 no longer owns that path — the linked-record plane is retired and nothing reads or writes there — so asserting on it is stale vocabulary. The orphaned directories remain on disk untouched; st2 simply stops making claims about them. The 07-resource worked example listed bindings in declaration order. `ls` sorts by name, which is correct: declaration order has no meaning. Verified against the real catalog byte-for-byte, including the width-aware name padding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: Claude Code agent-tool-version: 2.1.237 agent-runtime: Claude Code 2.1.237 tooling-profile: dotfiles@11eaf2d-dirty * fix(resource): handle a binding's trailing line comment on remove and update A hand-authored `resource "work" uri="…" reason="…" // why it is here` could not be removed: `remove_field`'s standalone-line path accepted only whitespace after the node, so it fell through to the compact path and refused with `unsafe-source-shape`. The line's own trailing `//` comment is now removable trivia and goes with the binding it explains. `/*` is deliberately not accepted — a block comment can span lines and this only ever sees one. Updating the same binding had a quieter defect: the node span runs to the start of trailing trivia, so replacing it verbatim glued the rendered node onto the comment (`reason="Changed."// why`). The separator is now preserved. The output still parsed, so this was cosmetic rather than corrupting. Reported by Codex review on #361 (P2). The remove case is the one that hard-failed; the update case was found while confirming the scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: Claude Code agent-tool-version: 2.1.237 agent-runtime: Claude Code 2.1.237 tooling-profile: dotfiles@11eaf2d-dirty * docs(vrs): reconcile with the merged Resource Profile foundation #351 landed first and rewrote R20: a binding's URI scheme is now the exact lookup key for an optional, catalog-declared Resource Profile, scheme meaning stays downstream-owned, and st2 ships no built-in profiles. Drop R35. It existed only because R20 then read "st2 does not register schemes", which a scheme st2 resolves would have contradicted; it granted an exemption from a clause R20 no longer contains, so rebasing it would have carried a self-contradiction into requirements.md. `working-state` needs no exemption under the merged R20 — st2 writes the carrier through `st2 context`, and resolving the scheme is a catalog's choice. requirements.md is now identical to main: this branch makes no constitutional edit. Decision 0012 records the withdrawal as an amendment rather than rewriting its history. Nothing about the carrier, its scheme name, or its realization changes. Renumber this branch's decisions 0008/0009/0010 to 0011/0012/0013; #351 and #345 took 0008 and 0009 on main. Correct three places that asserted the pre-#351 contract: the ontology's Resource and working-state entries, and 07-resource/spec.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: Claude Code agent-tool-version: 2.1.237 agent-runtime: Claude Code 2.1.237 tooling-profile: dotfiles@11eaf2d-dirty * docs(vrs): scope the resource boundaries to this document, not to st2 "No Resource registry, no generic resolution" described st2 before #351 landed one. Resolution is out of scope for 07-resource, not absent from st2: the scheme-keyed profile registry lives in 07-resource-profile. The two are orthogonal — a binding names a Resource whether or not a profile resolves its scheme, and `st2 resource` neither reads nor requires one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: Claude Code agent-tool-version: 2.1.237 agent-runtime: Claude Code 2.1.237 tooling-profile: dotfiles@11eaf2d-dirty * test: align resource validation coverage with the widened #345 envelope #345 admits a catalog-relative carrier path as a binding uri, not only an absolute URI, and reserves the binding name `declaration` for resync. Three assertions written against the older contract went stale on the rebase: - `agent_author` refused `./issue/1` and `example.com/issue/1`; both are valid relative carriers now. The refusals worth pinning are the ones that escape the catalog (`/etc/passwd`, `../outside`), so those replace them, and a positive case pins that `carriers/goal.md` is admitted. - `agent_resource` asserted a non-absolute uri is refused; it now asserts the escaping paths are refused and that the resync-reserved name is refused. - `agent_publish` drove the retired link-plane `resource add <url>` as a state-plane write under an incomplete-apply marker. A binding write is a declaration write, so it probes `context append` for the state plane and now additionally asserts the marker fences `resource add` — the behaviour that moving planes makes correct. Verified against a same-toolchain `git archive origin/main` baseline: the branch's failing-target set is a strict subset of main's own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: Claude Code agent-tool-version: 2.1.237 agent-runtime: Claude Code 2.1.237 tooling-profile: dotfiles@11eaf2d-dirty --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 4c0c760 commit 5a3beca

18 files changed

Lines changed: 2617 additions & 385 deletions

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ The envelope is `name` + `uri` + a required human-facing `reason`, plus an optio
260260
access, readiness, or lifecycle policy, and URI possession conveys no authority. A Resource URI may
261261
be referenced by any number of agent declarations. Resource-only declaration edits do not stop,
262262
replace, or relaunch a live task. Resource profiles and resolvers remain opaque to st2; catalog
263-
readers use the public `agent-spec` crate to inspect the bindings.
263+
readers use the public `agent-spec` crate to inspect the bindings, and `st2 resource ls|read`
264+
projects them for one agent.
264265

265266
The positional agent value is the stable automation identity. Optional `name` and `description`
266267
fields are presentation only; they never route messages, select tasks, or rename durable state.
@@ -270,6 +271,9 @@ Mutate a catalog-owned KDL declaration through the constrained commands:
270271
st2 rename <stable-id> "Release worker"
271272
st2 describe <stable-id> "Owns release preparation and verification."
272273
st2 rename <stable-id> --clear
274+
st2 resource add <name> --uri <uri> --reason "<why this agent carries it>"
275+
st2 resource remove <name>
276+
st2 resource rename <old> <new>
273277
```
274278

275279
These commands preserve unrelated KDL bytes and serialize local writers through the persistent
@@ -468,8 +472,8 @@ so external harness hooks can read its current name and description without pars
468472
on a duplicate state file.
469473

470474
For a catalog-backed agent, every native bus operation resolves the same agent directory used by
471-
the roster: presence is `<agent-dir>/status`, while unread messages, archive receipts, context, and
472-
links live under `<agent-dir>/resources/`. The flat `<root>/<identity>` layout remains only as the
475+
the roster: presence is `<agent-dir>/status`, while unread messages, archive receipts, and context
476+
live under `<agent-dir>/resources/`. The flat `<root>/<identity>` layout remains only as the
473477
intentional catalog-less fallback used by isolated folder evals. In a catalog-backed root,
474478
`st2 message ls` rejects an absent identity; recovery inspection of a deliberately orphaned flat
475479
box must be explicit with `st2 message ls <identity> --orphan` (and optionally `--archive`).
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# The linked-record plane is retired and `resource` names one concept
2+
3+
Status: accepted
4+
5+
Design decision made by Johannes on 2026-08-27 (interview over the
6+
07-resource measurements and
7+
[dotfiles#2071](https://github.com/schickling/dotfiles/pull/2071)). Merge and
8+
acceptance approval required: upstream maintainers.
9+
10+
## Context
11+
12+
Two durable edges were both called *resource*: Agent Spec Resource bindings, and
13+
the link records written by `st2 resource add`. An agent reading one while
14+
reasoning about the other measured the wrong store — a declaration with five
15+
bindings and no link records reported `# 0 resources`, and a declaration with
16+
two bindings and fourteen link records reported fourteen rows containing neither
17+
binding. Both surfaces answered correctly; neither said which question it had
18+
answered.
19+
20+
The measurements (07-resource `.experiments/2026-08-27-resource-read-surfaces.md`,
21+
one live catalog of 655 declarations) established that the planes are disjoint by
22+
construction rather than by coincidence: 889 distinct binding URIs and 233
23+
distinct link URLs share **zero** members, exactly or normalized. Bindings carry
24+
what an agent is for; link records carried what it produced.
25+
26+
They also established that the link plane is legacy. It was adopted by 82 of 655
27+
declarations, its creation rate decayed from a peak of 45/day on 2026-08-08 to
28+
none after 2026-08-26, **the only reader of a link record in the source tree is
29+
its own `ls`/`read` verb**, and `axe work update --artifact <path> --pty <name>`
30+
now covers the job — matching the observed `relation` values (`output` 185,
31+
`evidence`, `produces`) and the 8 `pty://` URLs.
32+
33+
## Decision
34+
35+
The linked-record plane is **retired**. `st2 resource add|ls|read|remove` and
36+
`resources/links/` are removed, and *resource* names exactly one concept: a
37+
declared Resource binding. The freed verb becomes the binding surface, which the
38+
declared plane never had — bindings were previously visible only through
39+
`st2 agents --json`.
40+
41+
The 241 existing records are left in place as orphaned files rather than
42+
migrated. Most belong to retired declarations whose worktrees are gone, and
43+
`axe work` writes to a gitignored per-worktree path that no longer exists for
44+
them.
45+
46+
Producing agents record artifacts through `axe work update --artifact/--pty`.
47+
48+
## Consequences
49+
50+
- `resource` is unambiguous across the CLI, the declaration, and the corpus.
51+
The read that produced the friction cannot recur, because there is no second
52+
plane to read.
53+
- `<agent-dir>/resources/` is untouched and remains canonical for an agent's
54+
resource files. Only `links/` goes. The directory is the realization surface
55+
for bindings, not a second sense of the word.
56+
- `templates/bus.st2.md` loses the advertisement that produced the adoption.
57+
- Surviving `resources/links/` files become unreferenced. The ontology keeps a
58+
retired entry so a reader who meets one can identify it.
59+
- Nothing downstream breaks: no consumer other than the retired verb read them.
60+
61+
## Options
62+
63+
| Option | Result | Reason |
64+
| --- | --- | --- |
65+
| Retire the plane; `st2 resource` becomes the binding surface | Selected | Nothing but its own verb ever read a linked record, adoption was 12.5% and decaying, and `axe work update --artifact/--pty` covers the job. Retiring makes *resource* unambiguous by removing the second plane rather than by wording around it. |
66+
| Freeze read-only: keep `ls`/`read`, drop `add` | Rejected | Keeps records reachable, but *resource* keeps naming two things for as long as any record survives, so the misread stays possible. |
67+
| Migrate the 241 records into `axe work`, then retire | Rejected | Highest fidelity, but `axe work` writes to `<repo-root>/tmp/worklog/`, gitignored and per-worktree; most of the 82 declarations are retired and have no reachable worktree to write into. |
68+
| Keep and invest: add `--json`, a requirement, a consumer | Rejected | Asks the fleet to adopt a second evidence ledger beside the one it already uses. In the plane's whole lifetime nothing consumed it. |
69+
| Unify both planes behind one typed reference ([#122](https://github.com/compoundingtech/st2/issues/122)) | Rejected | With zero measured overlap a shared descriptor deduplicates nothing and joins nothing, and #122's proposed `{_tag, uri}` assumes a field removed in #307. |
70+
71+
## Evidence and Argument
72+
73+
The measurement is
74+
[`07-resource/.experiments/2026-08-27-resource-read-surfaces.md`](../07-resource/.experiments/2026-08-27-resource-read-surfaces.md),
75+
taken against one live catalog of 655 declarations. Three findings decide it.
76+
77+
**The planes are disjoint by construction.** 889 distinct binding URIs and 233
78+
distinct linked-record URLs share zero members, under exact match and after
79+
normalization on all five schemes both planes used. 81 of the 82
80+
linked-record-carrying declarations also carried bindings, so they coexisted
81+
constantly and still never named one thing. The cause is semantic: 728 bindings
82+
were self-state carriers and 378 were work inputs, while 196 of 241 linked
83+
records were `output`, `produces`, `evidence`, or `verified`. That is what kills
84+
#122 — a shared type has nothing to deduplicate.
85+
86+
**The plane was write-only.** A source search for `links_dir` finds exactly two
87+
readers, both inside the `st2 resource ls|read` implementation. No projection,
88+
roster, or doctor consumed a linked record in the plane's lifetime.
89+
90+
**It was already being abandoned.** Creation peaked at 45 records on 2026-08-08,
91+
fell to 5 on 2026-08-26, and stopped; 14 of the 25 most recent records come from
92+
one declaration on one day. Meanwhile `axe work update` grew `--artifact` and
93+
`--pty`, which match the observed `relation` values and the 8 `pty://` URLs
94+
exactly.
95+
96+
The counter-argument — that low adoption is a surfacing problem, not obsolescence
97+
— is answered by the second finding: a plane nothing reads cannot be surfaced
98+
into usefulness by adding `--json` to it.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Working state is a declared carrier under `working-state://`
2+
3+
Status: accepted
4+
5+
Design decision made by Johannes on 2026-08-27 (interview; supersedes the open
6+
part of [#261](https://github.com/compoundingtech/st2/issues/261), which asked
7+
st2 to pick the name). Merge and acceptance approval required: upstream
8+
maintainers.
9+
10+
## Context
11+
12+
An agent's self-state carriers are declared as Resource bindings and realized
13+
under `<agent-dir>/resources/`: `dev.schickling.agent-goal://` realizes as
14+
`resources/goal.md`, `decision-tree://` as `resources/context/decisions/`, and
15+
so on for notes, private notes, and the friction log.
16+
17+
Working state — R09's restored durable context, written through `st2 context`
18+
is the exception. Measured on one live catalog of 655 declarations, 605 have a
19+
`resources/context/now.md` and **none declares it**; no binding URI anywhere
20+
mentions it. Consumers reach it by joining a literal path onto the declaration's
21+
own directory, which one downstream author annotated in-line as "a CONVENTION,
22+
not a declaration".
23+
24+
## Decision
25+
26+
Working state becomes the sixth self-state carrier, declared like its siblings:
27+
28+
```kdl
29+
resource "working-state" \
30+
uri="working-state://<host>/<identity>" \
31+
reason="Working state for lossless restart."
32+
```
33+
34+
realized at `<agent-dir>/resources/context/now.md`, resolver owned by st2
35+
(`st2 context`). The binding grants no authority, as for every binding.
36+
37+
The scheme is **`working-state`**, un-prefixed. It takes the ontology's already
38+
canonical term for R09's restored durable context — a term the ontology also
39+
already guards against being read as a liveness or activity signal — and the
40+
scheme inherits that guard.
41+
42+
## Consequences
43+
44+
- The carrier is addressable by URI like its five siblings, so a consumer
45+
resolves a declaration instead of walking a path.
46+
- The ontology gains a `working state` entry naming the term, the verb, the
47+
realization path, and the scheme.
48+
- Declaring it across the fleet is a bulk binding write, which is why it
49+
sequences after the mediated write surface (decision 0013).
50+
- This needed no requirements change. It was first drafted as a new requirement
51+
(**R35 st2-owned Resource profiles**) because R20 then read "st2 does not
52+
register schemes", which a scheme st2 resolves would have contradicted.
53+
[#351](https://github.com/compoundingtech/st2/pull/351) landed first and
54+
rewrote R20: a scheme is now the exact lookup key for an optional,
55+
catalog-declared Resource Profile. That removes the barrier R35 existed to lift,
56+
and R35's own text — an exemption from a clause R20 no longer contains — became
57+
incoherent, so it was dropped rather than rebased.
58+
59+
`working-state` is therefore an ordinary scheme under the merged R20. st2's
60+
`st2 context` writes the carrier at `resources/context/now.md`; whether any
61+
catalog registers a Resource Profile that resolves the scheme is a downstream
62+
choice this decision does not make, and R20's "st2 ships no built-in profiles"
63+
stands untouched.
64+
65+
## Amendment 1 — 2026-08-28
66+
67+
The requirements delta this decision originally carried is withdrawn. Nothing
68+
about the carrier, its scheme name, or its realization changes; only the
69+
justification does, because #351 made the exemption unnecessary. See the bullet
70+
above.
71+
72+
## Options
73+
74+
| Option | Result | Reason |
75+
| --- | --- | --- |
76+
| `working-state://` | Selected | Takes the ontology's already canonical term for R09's restored durable context — a term the ontology also already guards against being read as liveness or activity — so the scheme inherits the guard. No known downstream clash. `decision-tree://` sets the un-prefixed precedent. |
77+
| `agent-context://` | Rejected | Matches the `st2 context` verb, the `resources/context/` directory, and the `agent-notes://` prefix pattern, but [#261](https://github.com/compoundingtech/st2/issues/261) states the requesting consumer already uses "Agent Context" for message-envelope relations. Hands them a collision for internal symmetry. |
78+
| `agent-state://` | Rejected before posing | *State* is the most overloaded word in this ontology — session state, observed harness state, desired state, presence — which already carries explicit collision rules. A scheme by that name recreates the ambiguity this work removes. |
79+
| `agent-working-state://` | Rejected | No clash and full prefix symmetry, but `agent-` carries no information: every per-agent carrier is per-agent and the URI authority already names the agent. |
80+
| Leave working state undeclared | Rejected | `st2 context read <identity>` already resolves it, but the five sibling carriers are declared and the asymmetry is what forces downstream path-joining and filesystem crawls. |
81+
82+
## Evidence and Argument
83+
84+
Measured on one live catalog of 655 declarations
85+
([experiment](../07-resource/.experiments/2026-08-27-resource-read-surfaces.md)):
86+
605 declarations have a `resources/context/now.md`, **none** declares it, and no
87+
binding URI anywhere mentions it. Every other near-universal per-agent carrier is
88+
declared — `notes` on 618 declarations, and `goal`, `private-notes`,
89+
`friction-log`, `decisions` on 27 each — and each realizes into the same
90+
`resources/` directory that working state realizes into.
91+
92+
So the asymmetry is not a design boundary, it is an omission: the one carrier st2
93+
itself writes, through `st2 context`, is the one carrier nothing declares.
94+
95+
#261 documents what the omission costs a consumer — a TUI joining a literal path
96+
onto the declaration's directory, annotated in-line by its own author as "a
97+
CONVENTION, not a declaration", plus an hourly timer crawling
98+
`find <agent>/resources` behind a hand-maintained exclusion list to report which
99+
live declarations lack a `now.md`. Both are downstream workarounds for a fact
100+
that no declaration states.
101+
102+
The naming argument turns on which layer owns the word. The verb and the
103+
directory both say *context*; the ontology says *working state*, and says it in a
104+
rule that already exists to stop the term being confused with liveness. A scheme
105+
is read far from its verb, so it should carry the term that travels with its own
106+
guard.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# `st2 resource` is a mediated binding write surface
2+
3+
Status: accepted
4+
5+
Design decision made by Johannes on 2026-08-27 (interview; answers
6+
[#231](https://github.com/compoundingtech/st2/issues/231)). Merge and acceptance
7+
approval required: upstream maintainers.
8+
9+
## Context
10+
11+
Changing one declared binding requires the caller to render a complete candidate
12+
`agent.kdl`, validate it, compute its digest, publish under compare-and-swap, and
13+
read it back. That ceremony is safe and disproportionate.
14+
15+
It also has a measurable consequence. Of 241 link records on one live catalog, 45
16+
were not products at all but dependency and reference edges — `current-work`
17+
(which duplicates the `work` binding), `depends-on-slice-*`, `supervises`,
18+
`blocked-design`. Agents wrote them into the linked-record plane because that was
19+
the plane they were permitted to write cheaply. **The drift between the two
20+
planes was caused by write-cost asymmetry, not by two different relations.**
21+
22+
Retiring the linked-record plane (decision 0011) removes the escape hatch. It
23+
does not remove the pressure that produced it.
24+
25+
## Decision
26+
27+
`st2 resource` gains mediated write verbs alongside `ls` and `read`:
28+
29+
```text
30+
st2 resource add <name> --uri <uri> --reason <text>
31+
st2 resource remove <name>
32+
st2 resource rename <old> <new>
33+
```
34+
35+
Each performs read-modify-CAS-publish internally. The caller never renders KDL.
36+
Full-catalog validation, exact-target selection, compare-and-swap, and
37+
fail-closed concurrent-change behavior are preserved, and a binding-only change
38+
does not stop, replace, or relaunch healthy work (R21).
39+
40+
This is the fourth instance of an existing pattern, not new machinery:
41+
`src/agent_author.rs` already mediates `add_stream`/`remove_stream`
42+
(`st2 agent stream`), `set_desired_state` (`st2 agent desired-state`), and
43+
`set_presentation` (`st2 rename` / `st2 describe`).
44+
45+
## Consequences
46+
47+
- The reason agents reached for the cheap plane is removed, not just the plane.
48+
- Declaring the working-state carrier (decision 0012) across a fleet becomes one
49+
command per declaration instead of a rendered-and-published candidate each.
50+
- URI possession still grants nothing. A mediated write changes a declaration;
51+
it does not touch the thing the URI names, and confers no access to it
52+
([#61](https://github.com/compoundingtech/st2/issues/61)).
53+
- Authoring authority is unchanged: whoever may publish the declaration may
54+
mutate its bindings, and no one else.
55+
56+
## Limits
57+
58+
This does not address a declaration generated read-only by configuration
59+
management, where the next activation overwrites a runtime edit
60+
([#305](https://github.com/compoundingtech/st2/issues/305)). On the catalog
61+
measured, declarations are writable regular files carrying
62+
`meta { managed-by "agent-spec-authoring" }` with no `/nix/store` symlinks, so
63+
the mediated write applies there. The generated-declaration case stays open in
64+
[DQ-R4](../07-resource/open-questions.md).
65+
66+
## Options
67+
68+
| Option | Result | Reason |
69+
| --- | --- | --- |
70+
| Read plus mediated `add`/`remove`/`rename` | Selected | Removes the cause of the drift, not just its symptom. The machinery is the fourth instance of an existing pattern in `src/agent_author.rs`, so the marginal risk is small. |
71+
| Read-only `ls`/`read` | Rejected | Smallest change and a literal reading of #61's read-oriented boundary, but it leaves the write-cost asymmetry intact while decision 0011 removes the escape hatch — pressure with nowhere to go. |
72+
| Ship read-only now, add writes as a follow-up | Rejected | Sequences a breaking rename away from a new write path, which is genuinely safer, but leaves the same interval in which agents have an expensive plane and no cheap one. |
73+
74+
## Evidence and Argument
75+
76+
The link-record census supplies the causal evidence. Of 241 records, 196 are
77+
products — the plane's stated purpose. The remaining 45 are not: `supervises`
78+
(11), `reference` (7), `current-work`, `depends-on-slice-1`,
79+
`depends-on-slice-2`, `depends-on-slice-5`, `depends-on-slices-1-4`,
80+
`blocked-design`. `current-work` duplicates the `work` binding outright, and the
81+
`depends-on-slice-*` records are dependency edges filed in a products store.
82+
83+
Agents did not confuse the two planes. They wrote dependency edges into the
84+
products plane because a binding required publisher authority and
85+
whole-declaration republication under compare-and-swap, while a linked record
86+
required one file write. The observed misfiling is what write-cost asymmetry
87+
looks like from the inside.
88+
89+
That the machinery already exists is the second half of the argument.
90+
[`src/agent_author.rs`](../../../src/agent_author.rs) implements exactly this
91+
read-modify-CAS-publish shape three times — `add_stream`/`remove_stream` behind
92+
`st2 agent stream`, `set_desired_state` behind `st2 agent desired-state`, and
93+
`set_presentation` behind `st2 rename` and `st2 describe`. Bindings are the
94+
fourth field of the same declaration, mutated by the same protocol; this is a new
95+
caller of proven machinery rather than a new mechanism.
96+
97+
[#231](https://github.com/compoundingtech/st2/issues/231) asked for precisely
98+
this and listed the properties it must keep — full-catalog validation, exact
99+
target selection, CAS, fail-closed on concurrent change, non-disruption of
100+
healthy work, and machine-readable publication evidence. All are properties the
101+
existing three callers already have.

0 commit comments

Comments
 (0)