Retire the linked-record plane; st2 resource becomes the binding surface - #361
Merged
Conversation
schickling
marked this pull request as ready for review
August 27, 2026 14:05
There was a problem hiding this comment.
💡 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".
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
… 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
force-pushed
the
schickling/2026-08-27-resource-design
branch
from
August 28, 2026 11:34
7a54db5 to
1e12138
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Retire the linked-record plane;
st2 resourcebecomes the binding surfacest2 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:
# 0 resourcesneither 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:
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, sothe 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
_tagfield removed in #307.)Why the link plane went rather than the wording
ls/readverb. No projection, roster, or doctor consumed one.after. 14 of the 25 most recent came from one declaration on one day.
axe work update --artifact <path> --pty <name>covers thejob, matching the observed
relationvalues and the 8pty://URLs.templates/bus.st2.mdtold every agent on the bus to useit, 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 theworkbinding 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 resourcenow reads and writes declared bindings: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.rsalready does this for streams, desired state, andpresentation.
Removed:
src/resource.rs, the link-recordResourceCmd, and thetemplates/bus.st2.mdadvertisement.<agent-dir>/resources/is untouched and remains canonical for an agent'sresource files. Only
links/goes.Requirements: none
requirements.mdis identical to main. This branch makes no constitutionaledit.
It briefly carried one.
working-stateneeded 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/0013—0008/0009went to #345 and #351), and ontologyentries for
Resource,Resource binding,linked record (retired),agent resource directory, andworking state— the ontology previously hadzero 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
Resourceand
working stateentries and07-resource/spec.mdnow state the post-#351contract 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_publishadditionally asserts that an incomplete-apply marker fencesresource 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-fastcompared against a same-toolchaingit archive origin/mainbaseline. No failure is unique to this branch — itsfailing-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
ptywrapper, and doc drift in files this PR does not touch.New
tests/agent_resource.rscovers 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 --checkclean. Clippy is deliberately not gated (flake.nix:258).Not in this PR
working-statecarrier across existing declarations (decision0012 is accepted; the rollout is a bulk authoring pass, not a code change).
evals/AGENT-SPEC.md(DQ-R8).Posted on behalf of @schickling
agent_identityagent_personaagent_supervisoragent_toolagent_tool_versionagent_runtimetooling_profile