release v0.31.0: serve the agent doctrine as an MCP skill, and a version bump that cannot half-land (#284) #292

Merged
buildagent merged 4 commits from feat-mcp-skills-extension into master 2026-09-22 14:01:38 +02:00
Member

The agent skill

An agent reads a code-index payload the way it reads any search result: a zero means none, a count means the count. Both readings are wrong here, and the payload's own disclosure fields are what make them wrong. Until now the only place that was written down was CLAUDE.md, which an MCP client never receives.

The server now serves that doctrine as a skill under the official Skills extension io.modelcontextprotocol/skills (SEP-2640):

  • skills/list and skills/get, and the resource skill://code-index/SKILL.md
  • code-index rules writes the same bytes to .claude/skills/code-index/SKILL.md

Both doors read one include_str! in crates/core/src/skills.rs. The extension's host-side verification makes byte identity a requirement: a host compares sha256 and size against the manifest and discards a mismatch as corrupt. So both halves are graded by digest — served_skill_bytes_match_the_published_manifest_digest and the_emitted_skill_is_byte_identical_to_the_shipped_const — because a contains() check passes against every rewrite that actually breaks a host.

Two defects found rather than assumed:

  • The ordinary resource path truncates. A skill body served through it would have been cut mid-document and still passed any non-digest test. every_skill_file_fits_the_resource_budget_untruncated closes it.
  • My first cut early-returned the skill arm and so bypassed the universal evidence_gaps grader — caught by the_grader_is_called_once_for_every_resource_and_outside_the_match, the gate that exists for exactly that mistake.

rmcp 1.8 → 3.4, protocol pinned to 2026-07-28. server/discover is a modelled method in 3.4, so a pre-initialize call now answers -32600 rather than -32601; three stdio tests and the module doc table record why.

A version bump that cannot half-land (#284)

Eight sites, six of which were found by a gate going red rather than by anyone looking, and four (crates/guest/*/Cargo.lock) invisible to every workspace-wide command because those crates sit outside the workspace (#276).

The site set is derived, not listed. version_site_registry.rs walks the tree and compares what it finds against BUMP_SITES, so a new file naming the release goes red in the commit that adds it. _prdoc/ and dot-directories are excluded by rule — the second was measured, not anticipated, when the gate went red on .code-index/index.db, the daemon's own database, which stamps the version it was written by.

Three claims no gate held before: README.md, the guest lockfiles, and occurrence-level agreement. All three per-file claims pass over a README where three of five occurrences moved; only no_human_facing_file_names_a_release_that_is_not_this_one fails, and that mutation was run to prove it.

Sites 5 and 6 are deliberately not re-asserted — registry_schema_gate already owns the catalog's version, tag and the /download/{tag}/ segment of all eight plugin asset URLs. Two gates over one claim can disagree about which failed.

install.ps1 left the bump set entirely. Its two example tags said "a specific release, including an older one" while naming the current release; the install.sh lines they mirror have sat at v0.28.0 untouched. Freezing them removes a site instead of automating it.

.forgejo/scripts/version_bump.sh derives the same set, hands lockfiles to cargo, and asserts afterwards that nothing still states the old version. Letting cargo own the lockfiles was measured: rusqlite was already sitting at the version being bumped to, and a textual rewrite would have moved a third-party pin to a version that does not exist.

Two of my own tests were wrong

  • the_bump_script_refuses_what_it_cannot_do first asserted only a non-zero exit. The mutation survived — the script wrote version = "v9.9.9" into Cargo.toml and then died on cargo update, a non-zero exit from a different gate over a tree it had already half-rewritten. Each arm now matches the refusal's own words.
  • posix_script_gate caught Command::new("sh") in the new test: Windows has no shebang handling, so that form fails at spawn on the runner that gates releases. Now posix::run_posix_script, as the sibling CI gates already do.

Documentation

code-index rules and code-index query shipped in v0.30.0 — a release whose stated purpose was agent adoption — and appeared nowhere in README.md for its whole life. Both are documented now, and readme_cli_reference.rs reads the verb list out of code-index --help and fails on any subcommand the README omits.

Gates

All green on one unchanged tree (hash recorded before and after the run):

gate result
cargo fmt --all --check 0
cargo clippy --workspace --all-targets -D warnings 0
cargo doc (-D warnings, --document-private-items) 0
workspace tests 373 suites, 4003 passed, 0 failed
e2e, daemon leg 73 suites, 840 passed, 0 failed
guest gates (--locked) 0
corpus_ratchet 6/6, tests/corpus/baseline.json unmoved
precision_gate 7/7, every POPULATION line printed

The catalog's platform block is byte-identical and still state: unmeasured (I066) — only the release may write checksums. No package digest moved: the release packs a checked-in extractor.wasm.

Closes #284.

🤖 Generated with Claude Code

https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu

## The agent skill An agent reads a code-index payload the way it reads any search result: a zero means none, a count means the count. Both readings are wrong here, and the payload's own disclosure fields are what make them wrong. Until now the only place that was written down was `CLAUDE.md`, which an MCP client never receives. The server now serves that doctrine as a skill under the official Skills extension `io.modelcontextprotocol/skills` (SEP-2640): * `skills/list` and `skills/get`, and the resource `skill://code-index/SKILL.md` * `code-index rules` writes the same bytes to `.claude/skills/code-index/SKILL.md` Both doors read one `include_str!` in `crates/core/src/skills.rs`. The extension's host-side verification makes byte identity a **requirement**: a host compares sha256 and size against the manifest and discards a mismatch as corrupt. So both halves are graded **by digest** — `served_skill_bytes_match_the_published_manifest_digest` and `the_emitted_skill_is_byte_identical_to_the_shipped_const` — because a `contains()` check passes against every rewrite that actually breaks a host. Two defects found rather than assumed: * The ordinary resource path **truncates**. A skill body served through it would have been cut mid-document and still passed any non-digest test. `every_skill_file_fits_the_resource_budget_untruncated` closes it. * My first cut early-returned the skill arm and so **bypassed the universal `evidence_gaps` grader** — caught by `the_grader_is_called_once_for_every_resource_and_outside_the_match`, the gate that exists for exactly that mistake. rmcp 1.8 → 3.4, protocol pinned to 2026-07-28. `server/discover` is a modelled method in 3.4, so a pre-initialize call now answers `-32600` rather than `-32601`; three stdio tests and the module doc table record why. ## A version bump that cannot half-land (#284) Eight sites, six of which were found by a gate going red rather than by anyone looking, and four (`crates/guest/*/Cargo.lock`) invisible to every workspace-wide command because those crates sit outside the workspace (#276). **The site set is derived, not listed.** `version_site_registry.rs` walks the tree and compares what it finds against `BUMP_SITES`, so a new file naming the release goes red in the commit that adds it. `_prdoc/` and dot-directories are excluded by **rule** — the second was measured, not anticipated, when the gate went red on `.code-index/index.db`, the daemon's own database, which stamps the version it was written by. Three claims no gate held before: `README.md`, the guest lockfiles, and **occurrence-level agreement**. All three per-file claims pass over a README where three of five occurrences moved; only `no_human_facing_file_names_a_release_that_is_not_this_one` fails, and that mutation was run to prove it. Sites 5 and 6 are deliberately **not** re-asserted — `registry_schema_gate` already owns the catalog's `version`, `tag` and the `/download/{tag}/` segment of all eight plugin asset URLs. Two gates over one claim can disagree about which failed. `install.ps1` **left the bump set entirely**. Its two example tags said *"a specific release, including an older one"* while naming the current release; the `install.sh` lines they mirror have sat at `v0.28.0` untouched. Freezing them removes a site instead of automating it. `.forgejo/scripts/version_bump.sh` derives the same set, hands lockfiles to cargo, and asserts afterwards that nothing still states the old version. Letting cargo own the lockfiles was **measured**: `rusqlite` was already sitting at the version being bumped to, and a textual rewrite would have moved a third-party pin to a version that does not exist. ## Two of my own tests were wrong * `the_bump_script_refuses_what_it_cannot_do` first asserted only a non-zero exit. The mutation **survived** — the script wrote `version = "v9.9.9"` into `Cargo.toml` and then died on `cargo update`, a non-zero exit from a *different* gate over a tree it had already half-rewritten. Each arm now matches the refusal's own words. * `posix_script_gate` caught `Command::new("sh")` in the new test: Windows has no shebang handling, so that form fails **at spawn** on the runner that gates releases. Now `posix::run_posix_script`, as the sibling CI gates already do. ## Documentation `code-index rules` and `code-index query` shipped in v0.30.0 — a release whose stated purpose was agent adoption — and appeared nowhere in `README.md` for its whole life. Both are documented now, and `readme_cli_reference.rs` reads the verb list out of `code-index --help` and fails on any subcommand the README omits. ## Gates All green on one unchanged tree (hash recorded before and after the run): | gate | result | |---|---| | `cargo fmt --all --check` | 0 | | `cargo clippy --workspace --all-targets -D warnings` | 0 | | `cargo doc` (`-D warnings`, `--document-private-items`) | 0 | | workspace tests | 373 suites, **4003 passed, 0 failed** | | e2e, daemon leg | 73 suites, **840 passed, 0 failed** | | guest gates (`--locked`) | 0 | | `corpus_ratchet` | 6/6, `tests/corpus/baseline.json` **unmoved** | | `precision_gate` | **7/7**, every POPULATION line printed | The catalog's platform block is byte-identical and still `state: unmeasured` (I066) — only the release may write checksums. No package digest moved: the release packs a checked-in `extractor.wasm`. Closes #284. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
The Skills extension (SEP-2640, Final 2026-09-13) declares capabilities
through `server/discover`, a method MCP revision 2026-07-28 defines and
rmcp 1.8 did not model at all. This is the SDK half of getting there.

API CHANGES, all in `crates/mcp-server/src/server.rs`:

  * `Content` / `RawContent::Text` / `content.raw` -> `ContentBlock`,
    which is now the type rather than a wrapper.
  * `Resource::new(RawResource::new(..), None)` -> `Resource::new(..)`;
    the `Annotated` wrapper is gone (6 sites). Same for
    `RawResourceTemplate` -> `ResourceTemplate`.
  * `PromptMessageRole` -> `Role`.
  * `ServerInfo` is deprecated in 3.4 -> `ServerConfig`.
  * `ServerHandler` now returns the MRTR enums `CallToolResponse`,
    `ReadResourceResponse`, `GetPromptResponse`; each has a `From` impl
    from the old `*Result`, so the internal pipelines are unchanged and
    only the boundary converts.

THE MRTR ENUM IS REFUSED, NOT PASSED THROUGH. `CallToolResponse` is
`Complete | InputRequired | Task`, and `annotate_evidence_gaps` can only
annotate the first. That grader sits above the router precisely so no
tool answer escapes it, so the other two arms return an internal error
NAMING the variant instead of shipping an ungraded body. Every tool here
returns a `CallToolResult`, which rmcp wraps as `Complete`, so no arm is
reachable today -- but an ungraded body arriving through the SDK is the
same defect as one arriving through a forgetful tool author.

ANTIGRAVITY'S PROBE IS NO LONGER NON-STANDARD, and five tests said
otherwise. `server/discover` was unmodelled by rmcp 1.8, arrived as a
`CustomRequest`, and this transport answered -32601 -- true at the time.
Revision 2026-07-28 defines it and rmcp 3.4 models it
(`DiscoverRequestMethod`), listing it among the methods allowed before
`initialize`, so it now reaches the typed arm and is answered -32600.

That is the truthful code of the two, by this module's own rule: the
method exists -- it is the one the Skills extension declares
capabilities through -- and the server simply is not initialized yet.
-32601 would now claim a real method does not exist, which
`stdio.rs`'s own doc calls "a lie it may cache for the whole session".
The tests and the module's behaviour table encoded the old world; both
are updated with the reason, not just the number.

THE PROTOCOL VERSION IS PINNED, NOT DEFAULTED. rmcp 3.4's
`ProtocolVersion::default()` is still `V_2025_11_25`. This server needs
`V_2026_07_28` by name, and pinning means an rmcp bump cannot move the
wire out from under a client as a changelog entry nobody read.

Gates: fmt, clippy, cargo test --workspace (3979 passed, 0 failed, 369
suites), daemon E2E leg (829 passed, 0 failed, 72 suites).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
An agent that has never read this repository's doctrine reads a
code-index payload the way it reads any search result: a zero means
none, a count means the count, a resolved symbol means the symbol.
Every one of those readings is wrong here, and the payload's own
disclosure fields are what make them wrong. Until now the only place
that was written down was CLAUDE.md, which an MCP client does not get.

This ships the doctrine as a skill the server serves, under the
official Skills extension `io.modelcontextprotocol/skills` (SEP-2640),
and out of the same const as the on-disk copy.

  * crates/core/src/skills.rs is the single owner: one `include_str!`
    of SKILL.md, the name and entry-file constants, and a scalar-only
    frontmatter parser. Two doors -- the MCP resource and
    `code-index rules` -- read that one const, so they cannot drift.

  * The extension's host-side verification makes byte identity a
    REQUIREMENT, not a nicety: a host checks sha256 and size against
    the manifest and discards a mismatch as corrupt. Both the served
    bytes (`served_skill_bytes_match_the_published_manifest_digest`)
    and the written bytes (`the_emitted_skill_is_byte_identical_to_
    the_shipped_const`) are graded by digest, because a `contains()`
    check passes against every rewrite that actually breaks a host.

  * `skills/list` and `skills/get` answer via `on_custom_request`;
    `skill://code-index/SKILL.md` serves the same body as a resource.
    `directoryRead: false` is declared and MEASURED against reality.

Two things this found rather than assumed:

  * The resource budget truncates. A skill body served through the
    ordinary resource path would have been cut mid-document and still
    verified as "a skill" by any non-digest test.
    `every_skill_file_fits_the_resource_budget_untruncated` closes it.

  * My first cut early-returned the skill arm and so bypassed the
    universal `evidence_gaps` grader. `the_grader_is_called_once_for_
    every_resource_and_outside_the_match` caught it. The skill arm now
    records identity from inside the match and the verbatim-or-refuse
    policy runs after the grader.

`server/discover` became a modelled method in rmcp 3.4, so a pre-init
call now answers -32600 rather than -32601. Three stdio tests and the
module doc table record the new, more correct behaviour.

Gates: fmt, clippy -D warnings, rustdoc -D warnings, 3995 workspace
tests, 840 daemon-leg e2e tests -- all green, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
release: v0.31.0, and a version bump that cannot half-land (#284)
Some checks failed
CI / cargo fmt (pull_request) Successful in 54s
CI / OSS corpus tier-3 scale (nightly) (pull_request) Has been skipped
CI / Grammar rebuild from source (nightly) (pull_request) Has been skipped
CI / CI lane wall-clock headroom (pull_request) Successful in 1m5s
CI / guest crates (fmt, clippy, doc) (pull_request) Successful in 1m52s
CI / cargo doc (intra-doc links) (pull_request) Successful in 8m30s
CI / cargo test (abi, 32-bit + wasm32) (pull_request) Successful in 10m12s
CI / cargo deny (pull_request) Successful in 10m26s
CI / cargo check (MSRV 1.98) (pull_request) Successful in 11m7s
CI / cargo clippy (pull_request) Successful in 11m16s
CI / cargo check (windows-gnu) (pull_request) Successful in 11m35s
CI / OSS corpus (tier 1) (pull_request) Successful in 41m18s
CI / cargo test (pull_request) Failing after 37m36s
CI / cargo test (daemon transport) (pull_request) Has been skipped
CI / Plugin path cost + pool throughput (nightly) (pull_request) Has been skipped
CI (Windows) / fmt + clippy + build + test (windows) (pull_request) Successful in 56m12s
14dc4af692
A bump is one number in `Cargo.toml` and seven other places that have to
follow. Six of the eight sites were found by a gate going red rather
than by anyone looking, and the four guest lockfiles are invisible to
every workspace-wide command: those crates carry their own `[workspace]`
table and sit in the root manifest's `exclude` (#276), so no `--workspace`
flag reaches them.

THE SITE SET IS DERIVED, NOT LISTED.
`crates/indexer/tests/version_site_registry.rs` walks the tree and
compares what it FINDS against `BUMP_SITES`, so a new file that starts
naming the release goes red in the commit that adds it — the only moment
the author still knows why. `_prdoc/` and dot-directories are excluded
by RULE: the first is records, where naming an old release is correct;
the second was measured, not anticipated, when the gate went red on
`.code-index/index.db`, the daemon's own database, which stamps the
version it was written by.

Three claims no gate held before: `README.md`, the four guest lockfiles,
and — the one that matters most — occurrence-level agreement. All three
per-file claims pass over a README where three of five occurrences
moved; only `no_human_facing_file_names_a_release_that_is_not_this_one`
fails, and that mutation was run to prove it.

Sites 5 and 6 are deliberately NOT re-asserted. `registry_schema_gate`
already owns the catalog's `version`, `tag` and the `/download/{tag}/`
segment of all eight plugin asset URLs. Two gates over one claim can
disagree about which one failed.

`install.ps1` LEFT THE BUMP SET ENTIRELY. Its two example tags said "a
specific release, including an older one" while naming the CURRENT
release — they had been bumped every release to keep that comment false,
where the `install.sh` lines they mirror have sat at v0.28.0 untouched.
Freezing them removes a site instead of automating it.

`.forgejo/scripts/version_bump.sh` derives the same set, hands the
lockfiles to cargo, and asserts afterwards that nothing outside the
exclusion rules still states the old version. Letting cargo own the
lockfiles was measured rather than tidy: on the first real run
`rusqlite` was already sitting at the version being bumped TO, and a
textual rewrite would have moved a third-party pin to a version that
does not exist.

TWO OF MY OWN TESTS WERE WRONG, AND HOW THAT WAS FOUND:

  * `the_bump_script_refuses_what_it_cannot_do` first asserted only a
    non-zero exit. The mutation SURVIVED it — the script wrote
    `version = "v9.9.9"` into `Cargo.toml` and then died on `cargo
    update`, a non-zero exit from a DIFFERENT gate over a tree it had
    already half-rewritten. Each arm now matches the refusal's own
    words.
  * That mutation, run against the working tree, also rewrote
    `installer_e2e.rs` and `installer_ps1_e2e.rs`, where `v9.9.9` is the
    fake release tag their fixtures are built around. The script now
    refuses when any candidate site is a `.rs` file: Rust reads the
    version through `env!("CARGO_PKG_VERSION")`, so a bump has nothing
    to write in source.
  * `posix_script_gate` caught `Command::new("sh")` in the new test.
    Windows has no shebang handling, so that form fails AT SPAWN on the
    runner that gates releases. Now `posix::run_posix_script`, as the
    two sibling CI gates already do.

DOCUMENTATION. `code-index rules` and `code-index query` shipped in
v0.30.0 — a release whose stated purpose was agent adoption — and
appeared nowhere in `README.md` for its whole life. Both are documented
now, along with the served skill, and
`crates/cli/tests/readme_cli_reference.rs` reads the verb list out of
`code-index --help` and fails on any subcommand the README omits.

Gates, all green on one unchanged tree: fmt, clippy -D warnings, 373
workspace suites / 4003 tests, 73 e2e suites / 840 tests on the daemon
leg, the guest gates under --locked, corpus ratchet 6/6 with
`tests/corpus/baseline.json` unmoved, and precision_gate 7/7 with every
POPULATION line printed.

The catalog's platform block is byte-identical and still
`state: unmeasured` (I066): only the release may write checksums. No
package digest moved — the release packs a checked-in `extractor.wasm`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
fix(test): the version-site scan measures TRACKED files, not the working directory
All checks were successful
CI / cargo fmt (pull_request) Successful in 52s
CI / OSS corpus tier-3 scale (nightly) (pull_request) Has been skipped
CI / Grammar rebuild from source (nightly) (pull_request) Has been skipped
CI / guest crates (fmt, clippy, doc) (pull_request) Successful in 58s
CI / CI lane wall-clock headroom (pull_request) Successful in 1m9s
CI / cargo doc (intra-doc links) (pull_request) Successful in 6m21s
CI / cargo deny (pull_request) Successful in 6m44s
CI / cargo clippy (pull_request) Successful in 7m4s
CI / cargo test (abi, 32-bit + wasm32) (pull_request) Successful in 7m18s
CI / cargo check (MSRV 1.98) (pull_request) Successful in 7m26s
CI / cargo check (windows-gnu) (pull_request) Successful in 7m42s
CI / OSS corpus (tier 1) (pull_request) Successful in 31m30s
CI / cargo test (pull_request) Successful in 30m51s
CI / cargo test (daemon transport) (pull_request) Successful in 10m29s
CI / Plugin path cost + pool throughput (nightly) (pull_request) Has been skipped
CI (Windows) / fmt + clippy + build + test (windows) (pull_request) Successful in 1h1m27s
b6c82fc344
`every_file_that_names_this_release_is_a_declared_site` passed locally
and FAILED on the runner, naming twelve undeclared sites:

    these files name release `<version>` and are not declared bump
    sites: ["crates/indexer/core.36683", "crates/indexer/core.36705", …]

Core dumps. CI's container has them enabled, test subprocesses that are
deliberately killed leave `core.<pid>` in the crate directory, and a
core dump of course contains the version string — it is a snapshot of a
process whose binary embeds `CARGO_PKG_VERSION`.

NOT A REGRESSION, and worth saying so precisely: the same job passed
4002 of its 4003 tests, and nothing in its log reports a signal. Those
files were simply the first untracked ones anything in this repository
had ever looked at.

THE GATE WAS WRONG, NOT CI. A version site is something under version
control: nobody edits an untracked file during a bump and none of it
ships. A directory walk measures whatever the working directory happens
to be holding — build output, editor swap files, core dumps — which
differs on every machine that runs it, so it was not measuring the
population the claim is about. `git ls-files` asks the question the
claim actually asks.

The "a new file joins the set silently" claim survives, because the
gate runs in CI over a committed tree: by the time a new site matters,
it is tracked. Both directions were run — a TRACKED file naming the
release goes red, the SAME file untracked is ignored.

An anti-vacuity floor on the listing itself (>500 paths) is new: an
empty or truncated `git ls-files` would otherwise let every claim pass
over nothing, and would look exactly like a clean tree.

Third time this file has caught itself: the doc comment above quoted
the CI failure verbatim, which put a concrete version back into a file
that must never name one. Placeholder now.

fmt, clippy -D warnings, and the whole indexer crate (102 suites, 1232
tests) green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
buildagent deleted branch feat-mcp-skills-extension 2026-09-22 14:01:39 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index!292
No description provided.