Skip to content

Retire the linked-record plane; st2 resource becomes the binding surface - #361

Merged
schickling merged 7 commits into
mainfrom
schickling/2026-08-27-resource-design
Aug 28, 2026
Merged

Retire the linked-record plane; st2 resource becomes the binding surface#361
schickling merged 7 commits into
mainfrom
schickling/2026-08-27-resource-design

Conversation

@schickling-assistant

@schickling-assistant schickling-assistant commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Retire the linked-record plane; st2 resource becomes the binding surface

st2 had two disjoint durable things both called resource, and an agent reading
one while reasoning about the other measured the wrong store. This retires one of
them and repoints the verb at the other.

Closes #62. Closes #231. Refutes #122. Answers #261. Partially answers #60.

The problem, reproduced

Downstream report schickling/dotfiles#2071,
both instances measured on a live catalog:

  • a declaration with 5 bindings and 0 link records printed # 0 resources
  • a declaration with 2 bindings and 14 link records printed 14 rows containing
    neither binding

Both surfaces answered correctly. Neither said which question it had answered.

What the measurement showed

One live catalog, 655 agent declarations, 2026-08-27:

bindings link records
declarations carrying any 647 82
records 1161 241
distinct URIs 889 233

Exact-match URI overlap: 0 — and still 0 after normalizing across all five
schemes both planes used (file, worktree, git-commit, https,
st2-message). 81 of the 82 link-carrying declarations also carried bindings, so
the planes coexisted constantly and never named one thing.

The disjointness is semantic. Bindings carry what an agent is for (728
self-state carriers, 378 work inputs); link records carried what it made (196
of 241 were output/produces/evidence/verified).

So a shared descriptor type — #122's proposal — deduplicates nothing and joins
nothing. (#122 also assumes a _tag field removed in #307.)

Why the link plane went rather than the wording

  • Write-only by construction. The only reader in the source tree was its own
    ls/read verb. No projection, roster, or doctor consumed one.
  • 12.5% adoption, decaying. 45 records on 2026-08-08, 5 on 2026-08-26, none
    after. 14 of the 25 most recent came from one declaration on one day.
  • Superseded. axe work update --artifact <path> --pty <name> covers the
    job, matching the observed relation values and the 8 pty:// URLs.
  • Still advertised. templates/bus.st2.md told every agent on the bus to use
    it, which is what produced the adoption. That advertisement is removed here.

The root cause, which naming would not have fixed

45 of 241 link records were not products at all: supervises, reference,
current-work (duplicating the work binding outright), depends-on-slice-*,
blocked-design — dependency edges filed in a products store.

Agents wrote them there because a binding required publisher authority plus
whole-declaration CAS republication, while a link record was one file write.
The drift was caused by write-cost asymmetry, not by two concepts sharing a
word.
Retiring the plane removes the escape hatch; the mediated write verbs
remove the pressure that produced it.

Changes

st2 resource now reads and writes declared bindings:

st2 resource ls [<identity>] [--json]
st2 resource read [<identity>] <name> [--json]
st2 resource add <name> --uri <uri> --reason <text> [--inactive-reason <text>] [--agent <id>]
st2 resource remove <name> [--agent <id>]
st2 resource rename <old> <new> [--agent <id>]

The write verbs do read-modify-CAS-publish internally; the caller never renders
KDL. Validation, exact-target selection, compare-and-swap, fail-closed
concurrent-change behavior and R21 non-disruption are preserved, and a
Nix-managed declaration refuses the edit.

This is the fourth caller of an existing pattern, not new machinery —
src/agent_author.rs already does this for streams, desired state, and
presentation.

Removed: src/resource.rs, the link-record ResourceCmd, and the
templates/bus.st2.md advertisement.

<agent-dir>/resources/ is untouched and remains canonical for an agent's
resource files. Only links/ goes.

Requirements: none

requirements.md is identical to main. This branch makes no constitutional
edit.

It briefly carried one. working-state needed a requirement only while R20 read
"st2 does not register schemes" — a scheme st2 resolves would have contradicted
it. #351 landed first and
rewrote R20: a scheme is now the exact lookup key for an optional,
catalog-declared Resource Profile. That removed the barrier, and the drafted R35
— an exemption from a clause R20 no longer contains — was withdrawn rather than
rebased into a self-contradiction. Decision 0012 records the withdrawal as an
amendment. Nothing about the carrier, its scheme name, or its realization
changed.

VRS

New docs/vrs/07-resource/ tree, three decision records
(0011/0012/00130008/0009 went to #345 and #351), and ontology
entries for Resource, Resource binding, linked record (retired),
agent resource directory, and working state — the ontology previously had
zero entries for any of them, which is how two concepts came to share one
word.

Rebased onto the merged Resource Profile foundation. The ontology's Resource
and working state entries and 07-resource/spec.md now state the post-#351
contract rather than the one they were written against.

Rebased onto the merged Resource Profile work

#351 and #345 landed while this was open. Rebased onto them, and three
assertions written against the older contract were realigned: #345 admits a
catalog-relative carrier path as a binding uri and reserves the name
declaration, so the refusals now pinned are the ones that escape the catalog.
agent_publish additionally asserts that an incomplete-apply marker fences
resource add — correct now that a binding write is a declaration write.

Verification

nix flake check — the repo's CI gate — passes.

Full cargo test --no-fail-fast compared against a same-toolchain
git archive origin/main baseline. No failure is unique to this branch — its
failing-target set is a strict subset of main's own (main additionally flakes
completed_single_agent_writer_abas_during_runtime_observation_are_incomplete).
The 6 shared failures are pre-existing environmental ones on this host: the
agent-policy pty wrapper, and doc drift in files this PR does not touch.

New tests/agent_resource.rs covers ls/read/--json, add-publishes,
add-idempotent, exact URI byte preservation, remove-idempotent, rename
absent/collision refusal, invalid-URI and empty-reason refusal, the Nix-managed
refusal, and that the retired invocation no longer creates a link store. 7/7.

cargo fmt --check clean. Clippy is deliberately not gated (flake.nix:258).

Not in this PR

  • Declaring the working-state carrier across existing declarations (decision
    0012 is accepted; the rollout is a bulk authoring pass, not a code change).
  • The 241 orphaned link-record files, left in place (DQ-R10).
  • Runtime bindings on a configuration-generated declaration (How should Nix-managed Agent Specs compose runtime Resource bindings? #305, DQ-R4).
  • Syncing the stale upstream evals/AGENT-SPEC.md (DQ-R8).
Posted on behalf of @schickling
field value
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

@schickling
schickling marked this pull request as ready for review August 27, 2026 14:05

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 215f2683bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agent_author.rs
schickling-assistant added a commit that referenced this pull request Aug 27, 2026
… 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
schickling-assistant and others added 7 commits August 28, 2026 12:22
… 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
…ated 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
…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
… 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
#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
"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
#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
@schickling-assistant
schickling-assistant force-pushed the schickling/2026-08-27-resource-design branch from 7a54db5 to 1e12138 Compare August 28, 2026 11:34
@schickling
schickling merged commit 5a3beca into main Aug 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Explore mediated Agent Spec Resource binding mutations Clarify that st2 resource manages linked references, not agent state

2 participants