release v0.31.0: serve the agent doctrine as an MCP skill, and a version bump that cannot half-land (#284) #292
No reviewers
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index!292
Loading…
Reference in a new issue
No description provided.
Delete branch "feat-mcp-skills-extension"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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/listandskills/get, and the resourceskill://code-index/SKILL.mdcode-index ruleswrites the same bytes to.claude/skills/code-index/SKILL.mdBoth doors read one
include_str!incrates/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_digestandthe_emitted_skill_is_byte_identical_to_the_shipped_const— because acontains()check passes against every rewrite that actually breaks a host.Two defects found rather than assumed:
every_skill_file_fits_the_resource_budget_untruncatedcloses it.evidence_gapsgrader — caught bythe_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/discoveris a modelled method in 3.4, so a pre-initialize call now answers-32600rather 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.rswalks the tree and compares what it finds againstBUMP_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; onlyno_human_facing_file_names_a_release_that_is_not_this_onefails, and that mutation was run to prove it.Sites 5 and 6 are deliberately not re-asserted —
registry_schema_gatealready owns the catalog'sversion,tagand the/download/{tag}/segment of all eight plugin asset URLs. Two gates over one claim can disagree about which failed.install.ps1left the bump set entirely. Its two example tags said "a specific release, including an older one" while naming the current release; theinstall.shlines they mirror have sat atv0.28.0untouched. Freezing them removes a site instead of automating it..forgejo/scripts/version_bump.shderives the same set, hands lockfiles to cargo, and asserts afterwards that nothing still states the old version. Letting cargo own the lockfiles was measured:rusqlitewas 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_dofirst asserted only a non-zero exit. The mutation survived — the script wroteversion = "v9.9.9"intoCargo.tomland then died oncargo 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_gatecaughtCommand::new("sh")in the new test: Windows has no shebang handling, so that form fails at spawn on the runner that gates releases. Nowposix::run_posix_script, as the sibling CI gates already do.Documentation
code-index rulesandcode-index queryshipped in v0.30.0 — a release whose stated purpose was agent adoption — and appeared nowhere inREADME.mdfor its whole life. Both are documented now, andreadme_cli_reference.rsreads the verb list out ofcode-index --helpand fails on any subcommand the README omits.Gates
All green on one unchanged tree (hash recorded before and after the run):
cargo fmt --all --checkcargo clippy --workspace --all-targets -D warningscargo doc(-D warnings,--document-private-items)--locked)corpus_ratchettests/corpus/baseline.jsonunmovedprecision_gateThe 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-inextractor.wasm.Closes #284.
🤖 Generated with 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_0126PDDLB4wNHxKXvWM1VNmuAn 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`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