093 — The package that installs cleanly and answers nothing: the MCP server is packaged, verified, and unpublished

2026-09-08, distribution lane (DISTRIBUTION D3, the ungated half). Yesterday's session ended this file with a sentence that was wrong within the day: "nothing ungated is left in this lane." D3 reads "the MCP server installable and listed", and the listing is genuinely gated — the npm publish, the registry submission and every third-party directory need Sam. But installable is not listed, and nothing about building the package required anyone's permission. It was a session's work, and it was sitting at the top of the backlog being read as blocked.

The distinction worth keeping: a gate blocks the act, not the preparation. When Sam answers D2's four questions, the difference between a yes and a live package is now one command instead of a session — and the command refuses to run without him.

What was actually built

The failure this package is shaped like, and the test that exists to catch it

The MCP server reads the dataset by path: data/index.json, data/<lib>/facts.json, corrections/<lib>.md, resolved relative to its own file. In a clone those files are simply there. In a published package they exist only if files happened to name them — and files is exactly the kind of list that is correct when written and wrong three libraries later.

The failure it produces is the bad kind. Nothing errors at pack time. npm install succeeds. The server starts, answers initialize, lists its five tools — and then list_libraries returns nothing, or throws inside a stranger's agent. The selftest cannot see it: the selftest runs in the clone, where the files are always present. The pre-commit chain cannot see it. Everything green, package broken.

So tools/audit/pack-test.mjs never asks the whitelist what should be in the tarball. It computes the required set from data/index.json — one facts.json and one correction pack per library the dataset lists — packs the tarball npm would upload, installs it into a scratch directory as a dependency, and then drives the installed copy over a real pipe and requires its answers to be byte-identical to the same six requests answered here. A library added without a whitelist change fails as a missing file; anything the server needs and the tarball lacks fails as a different answer.

Both refusals were exercised against the real package, not against a fixture:

Two more things it checks that a clone can never test. The bin shim — the file npm generates from bin, which is what npx stale-priors-mcp actually executes, and the one part of the install this repository does not write: it is spawned and asked to initialize, and answers stale-priors-index@0.1.0. And the two manifests, which state the same name, version and description in two files — the shape of every drift this studio has caught (JOURNAL/005, /010). Agreement is pure JSON with no npm in it, so that check went into the MCP server's selftest, which runs on every commit; the packaging check needs npm and a temp directory and stays out of the pre-commit chain.

The gate belongs in the artifact

The uncomfortable observation: an operator session that has just packaged an npm package has every technical ingredient needed to publish it. The only thing between the tarball and the registry is a rule in CLAUDE.md and whatever the session remembers of it. That is the wrong place for a hard rule to live.

npm publish now runs tools/audit/publish-gate.mjs as prepublishOnly, and it refuses unless STALE_PRIORS_PUBLISH_APPROVED names the exact version being published. The token cannot be manufactured inside the process; it has to come from the person holding the npm account, and an approval given for 0.1.0 cannot carry 0.2.0 out. It also refuses to publish a server whose selftest is red, so a release cannot be greener than the working tree. Both refusals are exercised by the pack test.

A shipped copy is a snapshot, and it now says so

The product is freshness. An installed copy stops being fresh the moment it is installed — from npm, from a clone, from a vendored directory — and until today the server said nothing about how old its answers were. data/index.json deliberately carries no build timestamp: a generated surface with a clock in it can never pass --check. But the dataset already states three dates, so the age is derived rather than stamped. dataset_stats gained data_as_of — when each library's facts were last verified, when the release timelines were fetched, the newest run — and the sentence saying this copy does not update itself and where the current one lives. initialize now carries the verification date, so an agent is told the snapshot's age before it asks anything.

One drift removed while there: SERVER_VERSION was a constant beside package.json's version, which is two places stating one fact. It is read from the manifest now, and the selftest checks that what a client sees is what npm would install. Selftest 44 → 54 checks; pack test 28.

What Sam is being asked

Nothing has been published, listed, or sent. Three asks, all downstream of D2's repo flip:

  1. Publish stale-priors-mcp@0.1.0 (npm account; the repo must be public first, because the package points at it): STALE_PRIORS_PUBLISH_APPROVED=0.1.0 npm publish.
  2. Submit server.json to the MCP registry, which authenticates the io.github.SamAndrzejewski namespace against the GitHub account.
  3. The third-party directories (Smithery, Glama, PulseMCP, mcp.so) afterwards, each its own ask — nothing is listed before it is installable.

Left ungated in this lane, and next: the Claude Code plugin manifest and the per-library Cursor / AGENTS.md rules files — both generated from corrections/, neither written by hand.