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

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

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

For a catalog-backed agent, every native bus operation resolves the same agent directory used by
the roster: presence is `<agent-dir>/status`, while unread messages, archive receipts, context, and
links live under `<agent-dir>/resources/`. The flat `<root>/<identity>` layout remains only as the
the roster: presence is `<agent-dir>/status`, while unread messages, archive receipts, and context
live under `<agent-dir>/resources/`. The flat `<root>/<identity>` layout remains only as the
intentional catalog-less fallback used by isolated folder evals. In a catalog-backed root,
`st2 message ls` rejects an absent identity; recovery inspection of a deliberately orphaned flat
box must be explicit with `st2 message ls <identity> --orphan` (and optionally `--archive`).
Expand Down
98 changes: 98 additions & 0 deletions docs/vrs/.decisions/0011-the-linked-record-plane-is-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# The linked-record plane is retired and `resource` names one concept

Status: accepted

Design decision made by Johannes on 2026-08-27 (interview over the
07-resource measurements and
[dotfiles#2071](https://github.com/schickling/dotfiles/pull/2071)). Merge and
acceptance approval required: upstream maintainers.

## Context

Two durable edges were both called *resource*: Agent Spec Resource bindings, and
the link records written by `st2 resource add`. An agent reading one while
reasoning about the other measured the wrong store — a declaration with five
bindings and no link records reported `# 0 resources`, and a declaration with
two bindings and fourteen link records reported fourteen rows containing neither
binding. Both surfaces answered correctly; neither said which question it had
answered.

The measurements (07-resource `.experiments/2026-08-27-resource-read-surfaces.md`,
one live catalog of 655 declarations) established that the planes are disjoint by
construction rather than by coincidence: 889 distinct binding URIs and 233
distinct link URLs share **zero** members, exactly or normalized. Bindings carry
what an agent is for; link records carried what it produced.

They also established that the link plane is legacy. It was adopted by 82 of 655
declarations, its creation rate decayed from a peak of 45/day on 2026-08-08 to
none after 2026-08-26, **the only reader of a link record in the source tree is
its own `ls`/`read` verb**, and `axe work update --artifact <path> --pty <name>`
now covers the job — matching the observed `relation` values (`output` 185,
`evidence`, `produces`) and the 8 `pty://` URLs.

## Decision

The linked-record plane is **retired**. `st2 resource add|ls|read|remove` and
`resources/links/` are removed, and *resource* names exactly one concept: a
declared Resource binding. The freed verb becomes the binding surface, which the
declared plane never had — bindings were previously visible only through
`st2 agents --json`.

The 241 existing records are left in place as orphaned files rather than
migrated. Most belong to retired declarations whose worktrees are gone, and
`axe work` writes to a gitignored per-worktree path that no longer exists for
them.

Producing agents record artifacts through `axe work update --artifact/--pty`.

## Consequences

- `resource` is unambiguous across the CLI, the declaration, and the corpus.
The read that produced the friction cannot recur, because there is no second
plane to read.
- `<agent-dir>/resources/` is untouched and remains canonical for an agent's
resource files. Only `links/` goes. The directory is the realization surface
for bindings, not a second sense of the word.
- `templates/bus.st2.md` loses the advertisement that produced the adoption.
- Surviving `resources/links/` files become unreferenced. The ontology keeps a
retired entry so a reader who meets one can identify it.
- Nothing downstream breaks: no consumer other than the retired verb read them.

## Options

| Option | Result | Reason |
| --- | --- | --- |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |

## Evidence and Argument

The measurement is
[`07-resource/.experiments/2026-08-27-resource-read-surfaces.md`](../07-resource/.experiments/2026-08-27-resource-read-surfaces.md),
taken against one live catalog of 655 declarations. Three findings decide it.

**The planes are disjoint by construction.** 889 distinct binding URIs and 233
distinct linked-record URLs share zero members, under exact match and after
normalization on all five schemes both planes used. 81 of the 82
linked-record-carrying declarations also carried bindings, so they coexisted
constantly and still never named one thing. The cause is semantic: 728 bindings
were self-state carriers and 378 were work inputs, while 196 of 241 linked
records were `output`, `produces`, `evidence`, or `verified`. That is what kills
#122 — a shared type has nothing to deduplicate.

**The plane was write-only.** A source search for `links_dir` finds exactly two
readers, both inside the `st2 resource ls|read` implementation. No projection,
roster, or doctor consumed a linked record in the plane's lifetime.

**It was already being abandoned.** Creation peaked at 45 records on 2026-08-08,
fell to 5 on 2026-08-26, and stopped; 14 of the 25 most recent records come from
one declaration on one day. Meanwhile `axe work update` grew `--artifact` and
`--pty`, which match the observed `relation` values and the 8 `pty://` URLs
exactly.

The counter-argument — that low adoption is a surfacing problem, not obsolescence
— is answered by the second finding: a plane nothing reads cannot be surfaced
into usefulness by adding `--json` to it.
106 changes: 106 additions & 0 deletions docs/vrs/.decisions/0012-working-state-is-a-declared-carrier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Working state is a declared carrier under `working-state://`

Status: accepted

Design decision made by Johannes on 2026-08-27 (interview; supersedes the open
part of [#261](https://github.com/compoundingtech/st2/issues/261), which asked
st2 to pick the name). Merge and acceptance approval required: upstream
maintainers.

## Context

An agent's self-state carriers are declared as Resource bindings and realized
under `<agent-dir>/resources/`: `dev.schickling.agent-goal://` realizes as
`resources/goal.md`, `decision-tree://` as `resources/context/decisions/`, and
so on for notes, private notes, and the friction log.

Working state — R09's restored durable context, written through `st2 context` —
is the exception. Measured on one live catalog of 655 declarations, 605 have a
`resources/context/now.md` and **none declares it**; no binding URI anywhere
mentions it. Consumers reach it by joining a literal path onto the declaration's
own directory, which one downstream author annotated in-line as "a CONVENTION,
not a declaration".

## Decision

Working state becomes the sixth self-state carrier, declared like its siblings:

```kdl
resource "working-state" \
uri="working-state://<host>/<identity>" \
reason="Working state for lossless restart."
```

realized at `<agent-dir>/resources/context/now.md`, resolver owned by st2
(`st2 context`). The binding grants no authority, as for every binding.

The scheme is **`working-state`**, un-prefixed. It takes the ontology's already
canonical term for R09's restored durable context — a term the ontology also
already guards against being read as a liveness or activity signal — and the
scheme inherits that guard.

## Consequences

- The carrier is addressable by URI like its five siblings, so a consumer
resolves a declaration instead of walking a path.
- The ontology gains a `working state` entry naming the term, the verb, the
realization path, and the scheme.
- Declaring it across the fleet is a bulk binding write, which is why it
sequences after the mediated write surface (decision 0013).
- This needed no requirements change. It was first drafted as a new requirement
(**R35 st2-owned Resource profiles**) because R20 then read "st2 does not
register schemes", which a scheme st2 resolves would have contradicted.
[#351](https://github.com/compoundingtech/st2/pull/351) landed first and
rewrote R20: a scheme is now the exact lookup key for an optional,
catalog-declared Resource Profile. That removes the barrier R35 existed to lift,
and R35's own text — an exemption from a clause R20 no longer contains — became
incoherent, so it was dropped rather than rebased.

`working-state` is therefore an ordinary scheme under the merged R20. st2's
`st2 context` writes the carrier at `resources/context/now.md`; whether any
catalog registers a Resource Profile that resolves the scheme is a downstream
choice this decision does not make, and R20's "st2 ships no built-in profiles"
stands untouched.

## Amendment 1 — 2026-08-28

The requirements delta this decision originally carried is withdrawn. Nothing
about the carrier, its scheme name, or its realization changes; only the
justification does, because #351 made the exemption unnecessary. See the bullet
above.

## Options

| Option | Result | Reason |
| --- | --- | --- |
| `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. |
| `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. |
| `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. |
| `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. |
| 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. |

## Evidence and Argument

Measured on one live catalog of 655 declarations
([experiment](../07-resource/.experiments/2026-08-27-resource-read-surfaces.md)):
605 declarations have a `resources/context/now.md`, **none** declares it, and no
binding URI anywhere mentions it. Every other near-universal per-agent carrier is
declared — `notes` on 618 declarations, and `goal`, `private-notes`,
`friction-log`, `decisions` on 27 each — and each realizes into the same
`resources/` directory that working state realizes into.

So the asymmetry is not a design boundary, it is an omission: the one carrier st2
itself writes, through `st2 context`, is the one carrier nothing declares.

#261 documents what the omission costs a consumer — a TUI joining a literal path
onto the declaration's directory, annotated in-line by its own author as "a
CONVENTION, not a declaration", plus an hourly timer crawling
`find <agent>/resources` behind a hand-maintained exclusion list to report which
live declarations lack a `now.md`. Both are downstream workarounds for a fact
that no declaration states.

The naming argument turns on which layer owns the word. The verb and the
directory both say *context*; the ontology says *working state*, and says it in a
rule that already exists to stop the term being confused with liveness. A scheme
is read far from its verb, so it should carry the term that travels with its own
guard.
101 changes: 101 additions & 0 deletions docs/vrs/.decisions/0013-resource-is-a-mediated-write-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# `st2 resource` is a mediated binding write surface

Status: accepted

Design decision made by Johannes on 2026-08-27 (interview; answers
[#231](https://github.com/compoundingtech/st2/issues/231)). Merge and acceptance
approval required: upstream maintainers.

## Context

Changing one declared binding requires the caller to render a complete candidate
`agent.kdl`, validate it, compute its digest, publish under compare-and-swap, and
read it back. That ceremony is safe and disproportionate.

It also has a measurable consequence. Of 241 link records on one live catalog, 45
were not products at all but dependency and reference edges — `current-work`
(which duplicates the `work` binding), `depends-on-slice-*`, `supervises`,
`blocked-design`. Agents wrote them into the linked-record plane because that was
the plane they were permitted to write cheaply. **The drift between the two
planes was caused by write-cost asymmetry, not by two different relations.**

Retiring the linked-record plane (decision 0011) removes the escape hatch. It
does not remove the pressure that produced it.

## Decision

`st2 resource` gains mediated write verbs alongside `ls` and `read`:

```text
st2 resource add <name> --uri <uri> --reason <text>
st2 resource remove <name>
st2 resource rename <old> <new>
```

Each performs read-modify-CAS-publish internally. The caller never renders KDL.
Full-catalog validation, exact-target selection, compare-and-swap, and
fail-closed concurrent-change behavior are preserved, and a binding-only change
does not stop, replace, or relaunch healthy work (R21).

This is the fourth instance of an existing pattern, not new machinery:
`src/agent_author.rs` already mediates `add_stream`/`remove_stream`
(`st2 agent stream`), `set_desired_state` (`st2 agent desired-state`), and
`set_presentation` (`st2 rename` / `st2 describe`).

## Consequences

- The reason agents reached for the cheap plane is removed, not just the plane.
- Declaring the working-state carrier (decision 0012) across a fleet becomes one
command per declaration instead of a rendered-and-published candidate each.
- URI possession still grants nothing. A mediated write changes a declaration;
it does not touch the thing the URI names, and confers no access to it
([#61](https://github.com/compoundingtech/st2/issues/61)).
- Authoring authority is unchanged: whoever may publish the declaration may
mutate its bindings, and no one else.

## Limits

This does not address a declaration generated read-only by configuration
management, where the next activation overwrites a runtime edit
([#305](https://github.com/compoundingtech/st2/issues/305)). On the catalog
measured, declarations are writable regular files carrying
`meta { managed-by "agent-spec-authoring" }` with no `/nix/store` symlinks, so
the mediated write applies there. The generated-declaration case stays open in
[DQ-R4](../07-resource/open-questions.md).

## Options

| Option | Result | Reason |
| --- | --- | --- |
| 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. |
| 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. |
| 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. |

## Evidence and Argument

The link-record census supplies the causal evidence. Of 241 records, 196 are
products — the plane's stated purpose. The remaining 45 are not: `supervises`
(11), `reference` (7), `current-work`, `depends-on-slice-1`,
`depends-on-slice-2`, `depends-on-slice-5`, `depends-on-slices-1-4`,
`blocked-design`. `current-work` duplicates the `work` binding outright, and the
`depends-on-slice-*` records are dependency edges filed in a products store.

Agents did not confuse the two planes. They wrote dependency edges into the
products plane because a binding required publisher authority and
whole-declaration republication under compare-and-swap, while a linked record
required one file write. The observed misfiling is what write-cost asymmetry
looks like from the inside.

That the machinery already exists is the second half of the argument.
[`src/agent_author.rs`](../../../src/agent_author.rs) implements exactly this
read-modify-CAS-publish shape three times — `add_stream`/`remove_stream` behind
`st2 agent stream`, `set_desired_state` behind `st2 agent desired-state`, and
`set_presentation` behind `st2 rename` and `st2 describe`. Bindings are the
fourth field of the same declaration, mutated by the same protocol; this is a new
caller of proven machinery rather than a new mechanism.

[#231](https://github.com/compoundingtech/st2/issues/231) asked for precisely
this and listed the properties it must keep — full-catalog validation, exact
target selection, CAS, fail-closed on concurrent change, non-disruption of
healthy work, and machine-readable publication evidence. All are properties the
existing three callers already have.
Loading
Loading