Follow-on from #10627 (liveness measurement of the plugin-manifest contributes block). Filed unassigned. Split out of the mechanical removal card #10724 because this member is not a mechanical removal: removing it deletes the only declared channel for a real capability, so it needs a ruling, not a tombstone.
What is declared
packages/spec/src/kernel/manifest.zod.ts:419-427 — contributes.routes, an array of { prefix, service, methods? }, described as "API route contributions to HttpDispatcher".
What is measured
Zero readers. On origin/main (299b85e9d), the whole monorepo contains exactly one non-test read of manifest.contributes and it reads kinds, not routes:
packages/objectql/src/engine.ts:4499: if (manifest.contributes?.kinds) {
Controls (an uncontrolled zero is not a reading): the same regex finds those kinds lines (pattern works on this block); manifest.(namespace|navigationContributions|plugins|id) → 133 non-test hits (manifest reads are findable); the bare word routes has 1096 non-test hits monorepo-wide (the corpus is full of routing code — none of it reached from a manifest); objectui has 65 prose hits of contributes and 0 property reads. ⚠️ The cloud leg is unmeasured — that repo could not be attached to the measuring session (add_repo: "you don't have access"). It must be measured before either fork is executed.
Why this one is not just another dead key
Three separate places present contributes.routes as working machinery, one of them a customer-published skill:
skills/objectstack-api/SKILL.md:159-164 — a decision table telling authors to choose contributes.routes when "the endpoint needs real handler CODE — a third-party callback with its own signature verification, a streaming response, a protocol the platform does not speak. This registers a prefix for a plugin-provided service."packages/spec/src/api/dispatcher.zod.ts:16 — the HttpDispatcher protocol doc: "Supports dynamic route registration from plugins via contributes.routes".docs/adr/0088-metadata-kind-admission-and-retirement.md:40 — credits contributes.routes as a delivered form of the router kind, and is the stated reason the router kind's retirement was ruled the way it was.content/docs/references/ui/app.mdx:83 also sends App.apis migrators here: "A route that genuinely needs handler CODE still belongs in a plugin manifest contributes.routes entry."packages/spec/src/api/plugin-rest-api.zod.ts:37-62 carries a full worked contributes.routes manifest example.
So an author — human or AI — following the shipped skill writes a contributes.routes entry, gets a clean parse, and serves nothing. That is ADR-0049's silent no-op with a published recommendation attached, and it is live today, independent of which fork is chosen.
The fork
A — Enforce. Implement dispatcher registration from contributes.routes. The capability is real and currently has no declarative substitute: defineStack({ apis }) is live from protocol 17 (#5040) but is explicitly a projection of pipelines the platform already runs (object operation / flow), and the skill's own table says a code handler is the case apis: does not cover. Cost: a new execution path through the HttpDispatcher, plus the security surface a plugin-provided prefix implies (who may claim /api/v1/<x>, namespace carve-out, auth defaults).
B — Remove. Tombstone it with #10724, and correct the skill, the dispatcher doc, ADR-0088:40 and app.mdx:83 to point at the imperative http.server mount, which is the form that actually works today and the second half of what ADR-0088:40 already lists.
C — Enforce-later. Mark it [not enforced] in the describe string so the ledger's marker vocabulary classifies it honestly, correct the four author-facing claims now, and leave the schema in place pending a real pull.
Four-axis analysis
Real business need. Measured pull is thin on the declaration but non-zero on the capability: zero in-repo manifests author contributes.routes (the only two in-repo manifest authors declare drivers and events), and no example app uses it. But the imperative http.server mount — the same capability, reached in code — is used, and ADR-0088 treats code-route contribution as a live delivered form. So the need is real; it is the declarative spelling of it that has no demonstrated author.
Long-term soundness. A prefix-claim mechanism the dispatcher does not own is a routing-authority question, not a schema question. If A is chosen it should land with the ADR-0121-style namespace carve-out that defineStack({ apis }) already has, otherwise it reintroduces the "any plugin can claim any prefix" shape. B is contract-first and cheap: one declared channel per capability, and the surviving one is the one with an implementation.
Making AI-written code hard to get wrong. This axis argues loudest, and it argues against the status quo (C is its weakest form). The failure mode here is precisely the one ADR-0033 worries about: a published skill names the key, the schema accepts it, and the silence is indistinguishable from success. Whichever fork is taken, the skill's decision table must stop recommending a key that serves nothing — that correction is not optional and should not wait for the fork.
Startup scope discipline. Weighs toward B. A is net-new execution surface with no measured author for the declarative form, and the capability is already reachable imperatively; the startup-focus principle (2026-08-04) treats a declared surface with no pull as implementation-first. Publishing it and finding zero consumers is the sunk cost that ADR-0049 says does not earn an exemption.
Recommendation: B, with the author-facing corrections landed immediately and not held for the removal. The capability survives in its working imperative form; what goes away is a declarative spelling that never worked and that shipped documentation taught. A is defensible only if a real author appears who needs the declarative form — and then it should be built with the carve-out, not by turning the current shape on.
Not prejudged — routing authority and the published-skill surface are both maintainer calls.
Generated by Claude Code
Follow-on from #10627 (liveness measurement of the plugin-manifest
contributesblock). Filed unassigned. Split out of the mechanical removal card #10724 because this member is not a mechanical removal: removing it deletes the only declared channel for a real capability, so it needs a ruling, not a tombstone.What is declared
packages/spec/src/kernel/manifest.zod.ts:419-427—contributes.routes, an array of{ prefix, service, methods? }, described as "API route contributions to HttpDispatcher".What is measured
Zero readers. On
origin/main(299b85e9d), the whole monorepo contains exactly one non-test read ofmanifest.contributesand it readskinds, notroutes:Controls (an uncontrolled zero is not a reading): the same regex finds those⚠️ The
kindslines (pattern works on this block);manifest.(namespace|navigationContributions|plugins|id)→ 133 non-test hits (manifest reads are findable); the bare wordrouteshas 1096 non-test hits monorepo-wide (the corpus is full of routing code — none of it reached from a manifest);objectuihas 65 prose hits ofcontributesand 0 property reads.cloudleg is unmeasured — that repo could not be attached to the measuring session (add_repo: "you don't have access"). It must be measured before either fork is executed.Why this one is not just another dead key
Three separate places present
contributes.routesas working machinery, one of them a customer-published skill:skills/objectstack-api/SKILL.md:159-164— a decision table telling authors to choosecontributes.routeswhen "the endpoint needs real handler CODE — a third-party callback with its own signature verification, a streaming response, a protocol the platform does not speak. This registers a prefix for a plugin-provided service."packages/spec/src/api/dispatcher.zod.ts:16— the HttpDispatcher protocol doc: "Supports dynamic route registration from plugins via contributes.routes".docs/adr/0088-metadata-kind-admission-and-retirement.md:40— creditscontributes.routesas a delivered form of therouterkind, and is the stated reason therouterkind's retirement was ruled the way it was.content/docs/references/ui/app.mdx:83also sendsApp.apismigrators here: "A route that genuinely needs handler CODE still belongs in a plugin manifestcontributes.routesentry."packages/spec/src/api/plugin-rest-api.zod.ts:37-62carries a full workedcontributes.routesmanifest example.So an author — human or AI — following the shipped skill writes a
contributes.routesentry, gets a clean parse, and serves nothing. That is ADR-0049's silent no-op with a published recommendation attached, and it is live today, independent of which fork is chosen.The fork
A — Enforce. Implement dispatcher registration from
contributes.routes. The capability is real and currently has no declarative substitute:defineStack({ apis })is live from protocol 17 (#5040) but is explicitly a projection of pipelines the platform already runs (object operation / flow), and the skill's own table says a code handler is the caseapis:does not cover. Cost: a new execution path through the HttpDispatcher, plus the security surface a plugin-provided prefix implies (who may claim/api/v1/<x>, namespace carve-out, auth defaults).B — Remove. Tombstone it with #10724, and correct the skill, the dispatcher doc, ADR-0088:40 and app.mdx:83 to point at the imperative
http.servermount, which is the form that actually works today and the second half of what ADR-0088:40 already lists.C — Enforce-later. Mark it
[not enforced]in the describe string so the ledger's marker vocabulary classifies it honestly, correct the four author-facing claims now, and leave the schema in place pending a real pull.Four-axis analysis
Real business need. Measured pull is thin on the declaration but non-zero on the capability: zero in-repo manifests author
contributes.routes(the only two in-repo manifest authors declaredriversandevents), and no example app uses it. But the imperativehttp.servermount — the same capability, reached in code — is used, and ADR-0088 treats code-route contribution as a live delivered form. So the need is real; it is the declarative spelling of it that has no demonstrated author.Long-term soundness. A prefix-claim mechanism the dispatcher does not own is a routing-authority question, not a schema question. If A is chosen it should land with the ADR-0121-style namespace carve-out that
defineStack({ apis })already has, otherwise it reintroduces the "any plugin can claim any prefix" shape. B is contract-first and cheap: one declared channel per capability, and the surviving one is the one with an implementation.Making AI-written code hard to get wrong. This axis argues loudest, and it argues against the status quo (C is its weakest form). The failure mode here is precisely the one ADR-0033 worries about: a published skill names the key, the schema accepts it, and the silence is indistinguishable from success. Whichever fork is taken, the skill's decision table must stop recommending a key that serves nothing — that correction is not optional and should not wait for the fork.
Startup scope discipline. Weighs toward B. A is net-new execution surface with no measured author for the declarative form, and the capability is already reachable imperatively; the startup-focus principle (2026-08-04) treats a declared surface with no pull as implementation-first. Publishing it and finding zero consumers is the sunk cost that ADR-0049 says does not earn an exemption.
Recommendation: B, with the author-facing corrections landed immediately and not held for the removal. The capability survives in its working imperative form; what goes away is a declarative spelling that never worked and that shipped documentation taught. A is defensible only if a real author appears who needs the declarative form — and then it should be built with the carve-out, not by turning the current shape on.
Not prejudged — routing authority and the published-skill surface are both maintainer calls.
Generated by Claude Code