Volter World

Adding a new twin pack — from zero

The packaging guide — the Formula Cookbook — for a @volter/twin-<vendor> PACKAGE: one self-contained implementation of the platform's protocol aimed at one external upstream (./architecture.md "Two kinds of thing"). A package is born by the scaffolder, verified by its own T0, admitted by §9, and then lives a lifecycle recorded in policy/MAINTAINERS.md — experimental → Maintained → Supported → deprecated (Obsolete) → removed — kept current by catalog-wide upkeep sweeps (spec drift, SDK/PyPI currency, a live probe) whose per-package results the status column dispatches: Supported is fixed in the wave, Maintained is queued, Odd Fixes when it breaks, Orphan is flagged at risk. The operational recipe for creating a pack follows. The bar is in ./architecture.md (pack shape, dep hygiene) and ./conformance.md (acceptance criteria); this is the step-by-step.

Every pack moves to Protocol 3, and a new pack is built to it from the start: its migration steps are the recipe for a derived pack, with openai, github, stripe, slack, aws's Secrets Manager lane and smtp as its examples. What follows governs a protocol 2 pack until it moves.

Your task (if you were told "add the <vendor> twin")

This guide is self-driving — a one-line task ("add the <vendor> twin") is enough; everything you need is below. Execute exactly this, end to end, without waiting for further instruction:

  1. Isolate. Work in your own git worktree. git rebase it onto current local main and bun install BEFORE any work — worktrees branch from a stale base (the last pushed commit), which silently causes parallel-rewrite conflicts. Confirm the exemplar packs exist. Then prove the gate itself is alive on the UNTOUCHED base — bun test ./scripts/mutation-test.ts must parse and actually run saboteurs — before building anything: one builder inherited a base where scripts/mutation-test.ts didn't parse, so the mutation gate ran ZERO saboteurs while looking ordinarily red, and every later red got blamed on the new pack. A red you inherit is a rebase problem, not your bug — resolve it first. Never touch other packs or uncommitted user WIP.
  2. Understand the target — read "How a twin should work" (next section). Trace the motivating application's actual clients and connection paths before choosing the implementation surface. A vendor can expose several interfaces to the same state. Record demanded interfaces and omissions in the existing descriptor, capability manifest and census; a surface absent from those lists is still a gap. An installed adapter is not evidence that the app uses it.
  3. Build — follow steps 0→8: pick the exemplar by archetype, copy it (§0.5), adapt the layout/ package.json/index/cli (§1-4), build on the kernel (§5, incl. the subject-id note), author the manifest as the real vendor surface with failable verifies (§6), do the central wiring — §7, many central wiring points, not one — ask verify-pack.ts, do not trust a count, and start it with bun scripts/scaffold-pack.ts, which mechanizes most of them — and verify per §8. Scope every edit to packages/twin/<vendor>/ + the §7 wiring points (the manifest registry, the mutation PACKS entry, the four root censuses, the regenerated docs block, your pack descriptor's hosts/adoption/endpointEnv fields + the regenerated pack-facts.json, and — where they apply — the rate-budget rosters + the D8 mirror line, the journey registry).
  4. Self-audit your manifest (the "Self-audit" section) and fix what it surfaces.
  5. Adversarially review yourself — §9: spawn an independent read-only Explore sub-agent to refute every done; demote/fix anything it can't confirm, and record its verdict in your commit/report (this step is mandatory — the gate is blind to false-greens; an un-recorded review counts as not done). You orchestrate this yourself — it does not require a second human prompt.
  6. Check the acceptance checklist (§10), commit the final candidate in your worktree, and report any point where this doc was unclear/missing/wrong (cite the section) so the guide keeps improving. Verification is §8's per-merge set — your pack's suites, typecheck, and the meta-gates your wiring touched; the full twin-check.sh sweep runs only when the owner asks for one, so say plainly that your pack's claim is queued for that run rather than implying a sign-off you did not take.

That's the whole job. The rest of this doc is the detail for each step.

How a twin should work (the behavioral contract)

Read this first — it's the target the recipe below builds toward. A twin is a local, stateful, vendor-faithful replica of one SaaS API. Your unmodified vendor SDK (stripe, @octokit/rest, @linear/sdk, …) points at it and gets vendor-correct responses. Concretely it must:

  • Use the shared state kernel. State is a durable logical log and checkpointed tree — the shared @volter/world-core kernel, never an ad-hoc in-memory store. A write enters the kernel’s log; reads use its checkpointed tree.

  • Use the shared World operations. Refresh observes the vendor into a root's log. Writes use the simulated or real state system at the head. A branch holds its own entries over a parent position; push lands them in the parent, while deploy performs them against the vendor. Fetch extends cached parent history and rebase moves the branch position. See the model for the shared semantics.

  • Be vendor-faithful. Response shapes match the vendor; invalid input is rejected the way the vendor rejects it (same status + error code); an operation that isn't modeled yet fails like the vendor (404/422) — never a fake success.

  • Fake auth locally. No real vendor call except in the connector, over an injected client (the real SDK in prod, a fake in tests). The vendor SDK is a test-only dependency.

  • Keep the three kinds of credential apart. The fake vendor credentials an app presents are twin state: validated against the World's tree and seeded through the vendor's wire or an import, never derived from where the World sits. A real vendor credential lives only sealed beside a real-system root and reaches the vendor through the kernel executor, never pack code or the tree. A World token is transport: it grants access to the World and is never a vendor credential (data and keys).

  • A token never rides a deployable entry or a changeset's text. The fake tokens a twin issues are twin state behind the World's token — whoever reads the log can already mint them through /_twin/* — but an entry that is deployed is offered to the vendor, and a changeset is read by whoever approves it: no deployable entry's fields or subject id holds a token or a client secret. Keep the token's SHA-256 under a _-prefixed bookkeeping type (never deployed, never in a changeset), key rate and budget records by that hash, and sign upload URLs with a per-upload secret drawn at init rather than the caller's token. (Two packs put a raw bearer into deployable entries.)

  • Budget a rate-limited vendor, fail closed. If the vendor throttles a live token, every real call goes through one guarded client with a persistent rolling-window spend ledger that throws instead of calling past a conservative ceiling or during a Retry-After cooldown — and that survives a restart, so a fresh process gets no fresh allowance. Raw vendor API calls outside the connector are BANNED; a careless script bypassing it is exactly what a real ~4.5-day Figma token lockout was. Retries off by default. Prove all of it — cache, quota, backoff — against an injected fake, offline; never call the live vendor to "check". You do not write that machinery: it is the kernel's shared RateBudget (@volter/world-core). Your pack supplies only the numbers, as data — declareRateBudget(vendor, { windowMs, ceiling, defaultWeight, rules, reason }), also hung off TwinPack.rateBudget — and applies the guard at the one place a live call is issued. Omitting the declaration does not mean "no limit": you get the deliberately austere DEFAULT_RATE_BUDGET instead.

    Live-fetch the vendor's published limits page during this build before you pick a number, and put the figure, URL, and access date in reason. Do not copy a number from a design note, an old issue, or another pack's prose: vendor budgets change, and the Fly build found that even this repo's circulating notes had already drifted from the current first-party page. You may only be more permissive than the fallback when the live first-party source justifies it; if the vendor publishes no scalar limit (as Figma, OpenAI and Sentry do not), say exactly that in reason and stay at or under the fallback — never dress a guess up as a vendor fact. scripts/rate-budget-isolation.test.ts checks this across all declaring packs at once and holds the roster, so a new pack must be added there too.

    Prove the ceiling by counting calls on an injected fake: after N calls the fake recorded exactly N, and the next attempt throws with the count unchanged. "It threw" is not proof — the count is what shows nothing reached the vendor. Inject the ledger path in every test, too: a suite that spends against the operator's real ~/.volter/<vendor> file poisons later runs.

    See ./architecture.md D8, packages/world-core/src/rateBudget.ts, and three worked examples, one per shape: figma/src/figma-budget.ts (a pack that builds its own HTTP client), notion/src/notion-budget.ts (a pack that decorates an injected SDK client), and github/src/github-budget.ts (a vendor whose published scheme is already a weighted point budget, so the declaration reproduces its own numbers instead of proxying them).

  • Be offline + deterministic. Every capability verify() runs against the local twin with no network and a stable, repeatable result.

  • Classify the human surface explicitly. A vendor people work in gets a React mirror reading the same projection the API serves (API↔UI parity, not a hardcoded shell); a vendor people only call omits it. A third class exists: when the vendor's own browser page is a required leg of its protocol (OAuth consent), serve that page at the vendor route as part of the protocol, not as a second dashboard mirror. See "Does this vendor get a mirror?" below; this is the single most mis-applied rule in the repo, so decide it there rather than by feel.

Scope = "100% of what the vendor does." Every capability is done or todo; there is no exclusion list. The capability manifest is the real vendor surface as the denominator — coverage is honest and partial until the twin reaches it.

Relevant docs: README.md (the repo/world model: a twin is a repo, a world is a monorepo of twins) · ./architecture.md (kernel/pack boundaries + dep hygiene, mechanically enforced) · ./conformance.md (the acceptance ladder + the capture→check→report→gate cycle) · ../concepts/worlds.md (the world runtime + external self-managed services) · AGENTS.md (how the repo is grown: worktree-isolated builder agents, per-merge verification, the gate's cadence). Reference manifests: stripe-capabilities.ts / linear-capabilities.ts.

Does this vendor get a mirror?

Do not ask "does this vendor have a UI?" — almost every vendor has one, including OpenAI (a console) and Google Maps (a cloud dashboard), and asking it that way has produced wrong answers.

Ask instead: when someone does this vendor's core job, do they open a browser or write code? That question and the census.json ui slice's own ("does this twin need an agent-navigable UI sim?") are THE SAME question asked of the VENDOR'S product, never of the mirror you happen to have built: a read-only mirror is evidence of nothing (that argument would rule false for every read-only mirror — airtable §9, 2026-08-31). If people do the vendor's core job in a browser, needsUi: true, with journeys as recorded debt if the mirror isn't there yet.

  • A designer designs in the canvas. A writer writes in the page. A team talks in the app. A PM drags tickets on the board. → the UI is the product → ship a mirror.
  • An integrator calls the API; the vendor's console is incidental tooling for keys, billing, and logs, not where the work happens. → the API is the product → omit the mirror, and say so in the README ## Coverage + a comment in the manifest; coverage is API + connector.
  • A browser redirect is itself a required protocol participant: a human must see vendor-owned UI and make a choice before the protocol can continue. → the vendor UI is part of the protocol → serve it at the vendor's real route on the same server and state model, not as a separate dashboard mirror and not as an automatic redirect that bypasses the human leg. Google OAuth is the exemplar: the consent screen and /_twin/consent post complete the authorization-code flow. It still declares UI capabilities, journeys, and a mirrorMutations state-builder seam because the same data-coupling and visual-review bar applies; mirror is only a CLI alias for the same protocol server, never a second renderer.

The current split, for calibration — the rule above governs; this table is the state of the repo, not an authority, and two entries are known to contradict it:

packs
Mirror calcom clerk github jira linear posthog resend sentry slack stripe supabase — plus openrouter and aws, which are legacy outliers, not precedent (see below)
No mirror (API is the product) algolia anthropic elevenlabs fal googlemaps inngest livekit mapbox openai openweather pinecone upstash replicate sendblue stream svix twilio vital webrisk
Vendor UI is protocol googleoauth — consent is served by the protocol server itself; this is not a dashboard mirror

Three known inconsistencies — cite the rule, not these, when you classify:

  • openrouter mirrors but openai doesn't, though both are LLM-completion APIs that the rule puts in the same column. Its own census.json ui slice records needsUi:false ("the API/SDK is the real surface"), and §9 already demoted one of its UI verifies. It is a dev-console affordance that predates the rule.
  • aws ships an S3 bucket browser only (DynamoDB and Timestream have no UI capabilities at all). AWS is unambiguously code-first; the mirror is a debugging affordance, retro-fitted.
  • polar is in neither column. It ships no mirror, yet it is a merchant-of-record billing product with a real dashboard — the same shape as stripe, which does mirror — and its manifest already carries polar.mirror.ui as a live todo. Treat it as an open case to re-review, never as precedent for omitting.

Every no-mirror pack must state why in its README ## Coverage, under a ### No UI mirror heading. stream is the model to copy ("Stream's dashboard is an internal admin console, not a dev-facing data UI"). All no-mirror packs now carry this; polar's instead says the opposite — that its missing mirror is a tracked gap and must not be cited as precedent.

The trap: a thin or read-only REST API is evidence FOR a mirror, not against it. Figma's REST API is read + comments only precisely because the real work happens on the canvas that the API can't touch. Reasoning "the API is small, so this is an API-first vendor" inverts the rule and gets you a twin that omits the product. Notion and Figma both mirror for this reason.

What a mirror actually is. A second renderer over the same projection — never a second app with its own store, and never a client that keeps state the API can't see. Two files are invariant across all 13 mirror packs:

  • client/<vendor>-mirror.tsx (+ .css) — the React browser entry, bundled by Bun.build and memoized at module scope. The memoization is the reason a pack on its own does exactly one build. (§9 warns of a ~6-build per-process ceiling behind the meta-test's per-process isolation. Treat that as unquantified: a 2026-07-25 measurement ran 26 packs' verifies single-process and got byte-identical per-pack numbers, so the ceiling did not bite at that scale. Isolation is cheap insurance as the pack count grows — but re-measure before citing the ceiling as a hard constraint, and don't diagnose a failure as "the ceiling" without evidence.)
  • src/<vendor>-mirror-ui.ts — the server half: buildXxxMirrorClient(), createXxxMirrorServer ({ root, port }), xxxMirrorHtml(), plus any pure helpers the client imports (Bun tree-shakes the server-only exports out of the browser bundle — this is what lets a UI verify render the mirror's own tone/format functions; see sentry-mirror-ui.ts).

What varies is how the browser gets data, and there are two real archetypes. Pick by transport, not by taste:

packs how the client reads
A. API passthrough (default) sentry stripe clerk calcom posthog supabase openrouter … non-asset requests fall through to handleXxxTwinRequest; the client fetches the vendor's real API paths (sentry-mirror-ui.ts:128-134)
D. store door resend mailgun slack github jira linear figma aws (S3) the server declares named deterministic projections (stores: { mirror: () => … } on the twin fetch adapter); the client reads GET /twin/store/<name> — the ONLY door a mirror may read twin state through — and writes through the vendor's real API. The console never imports the handler (R3).

Prefer A. It is the strongest parity claim available — there is only one code path, so parity isn't "kept in sync", it cannot diverge. Copying stripe (§0) gives you A for free. Reach for D when the screen needs an aggregate that would otherwise cost the client a dozen round-trips, or when the vendor's wire format can't be parsed client-side at all (S3's XML — aws declares buckets/objects and the browser never sees an XML document); declare it as one or more named stores, and the client still writes through the vendor's wire. A bespoke /__mirror/* route computed in the console's own process is not a third archetype, it is the impurity D replaces.

Whichever you pick, the write path is non-negotiable: UI writes go through the same applyXxxWrite the API handler uses, addressed by the vendor's real endpoint. A mirror with its own write path has already failed the bar. D is worth studying for how far this is taken — slack's mirror store deliberately refuses to read twin internals and instead re-enters its own served Web API for members/pins/thread-meta (slack-mirror-state.ts), so even the aggregate is composed out of real API responses.

Size the mirror to the real product, and only to screens you can data-couple (§6). A small honest mirror beats a broad fabricated one — an undemonstrable screen is a todo, not a done.

Your UI verifies WILL be mutation-tested — write them expecting it. scripts/mutation-test.ts runs API/write and connector phases, an isolated mirror phase where applicable, and any named isolatedMutations groups. The mirror phase (mirrorMutations, TWIN-20/B9) is the sharp one: it sabotages your mirror's own state-builder export (jiraMirrorState, githubMirrorState, slackMirrorState, …) while leaving the write/handler seam fully real, so each UI verify's seed() genuinely writes and only the render/projection step is dead. A verify that greps the built bundle survives that and is reported as a SURVIVOR; a data-coupled one goes red. Declare a mirrorMutations seam when your UI verifies READ AROUND the handler (a direct twin-state reader like *MirrorState(root)) — that bypass is what the seam sabotages. A mirror whose UI verifies all drive the handler (uiDataCoupled) needs NO extra seam: the handler saboteur already reddens them, and an extra seam there proves nothing (this matches mutation-test.ts's own header; the hubspot build followed the header, correctly). What is NOT enforced mechanically is the mirror decision itself — architecture.test.ts never looks at client/, so whether your vendor should have a mirror at all remains a §9 judgment.

A SEAM MUST SABOTAGE THE ENTRY POINT THE PACK SERVES. Not merely an executor of the right shape — THE one serveExport mounts and the one your manifest drives. If those three ever name different code, every green you have is about a lane nobody reaches, and nothing will tell you: linear ran for months with its manifest and its saboteur both pointed at a hand-written subset schema that serveExport did not mount, and 16 done capabilities were false while a real @linear/sdk got "not supported by this Linear twin". Repointing the manifest alone turned 16 capabilities red; repointing the SEAM turned up 9 mutation survivors out of 56 that the old seam had never been able to see. Check the three agree before you trust a single green. A pack should publish exactly ONE served surface; if you believe yours needs two, that is a §9 argument to make out loud, not a factory to add quietly.

How far down the manifest may drive — RULED 2026-09-03, and the answer is "the handler". Most packs' verifies call the pack's own request handler; plain and stigg call it through one more layer, and linear used to call a private executor beneath it. Those are three different distances from the wire, and only the third is wrong. The rule is: a verify drives the handler the fetch adapter wraps — never a module beneath it. A private executor can diverge from the served surface and nothing will say so, which cost linear 16 false done capabilities.

Driving the FETCH CLOSURE instead is one notch closer again, and it is deliberately NOT required in general. What lives only in the adapter — body decoding, status mapping, the manifest door, host routing — is already exercised for every pack by the invariant matrix, which replays through the fetch closure WORKER_VENDORS builds per vendor, and deeply for five packs by their behavioural journeys. Migrating ninety manifests to the wire would buy a third copy of that coverage at enormous cost, and the adapter-level defect this catalog has seen — expo-server-sdk gzipping a request body that createTwinFetchFromHandler's await request.text() corrupts — is a §3 grounds for a hand-written fetch rather than an argument for moving every verify.

But there is ONE SHAPE where the wire is REQUIRED, and it is not the adapter. When a pack RENDERS A RESPONSE TWICE — an in-process collector that assembles the whole answer, and a socket renderer that emits it as frames — those are two surfaces, and a verify driving the collector is structurally blind to what the listener sends. Azure's Responses stream emitted data: [DONE] on the wire while its collector dropped that frame; a capability verify could not see the difference, and the lane's own fix believed it had removed the guard but had not, so the pin stayed GREEN BY CONSTRUCTION. Only a test against the real listener caught it. A pack with both paths owes a pack test on the real server — this is "one served surface" one level down, because a second rendering nobody measures is a second answer that can lie. Measured 2026-09-03: fourteen packs render a stream in both places (ai-gateway, anthropic, cohere, deepseek, gemini, groq, mistral, npm-registry, openai, openrouter, perplexity, snowflake, tunnel, xai). Linear's own second surface was deleted on 2026-09-03: once the manifest and the saboteur were repointed, the subset lane had no consumer, no verify and no saboteur, so nothing in the repo would have noticed it rotting — and it had already drifted (Notification typed as a concrete type where the SDL makes it an interface, metadata as String where the vendor has JSONObject!, invented input fields real Linear rejects), which means a developer building against it wrote code the vendor refuses. AN UNSERVED LANE IS NOT FREE: it is a second answer to "what does this vendor do", and the one nobody measures is the one that lies. Delete it; keep the state and behaviour half it shares with the served lane.

0. Pick the closest exemplar (copy-from BY ARCHETYPE)

Don't start from a blank file — clone the pack whose shape matches your vendor, then adapt:

Pick the LEANEST exemplar that has your shape. stripe is ~14k lines; a 2026-07-25 notion build copied it and deleted ~95%, which is slower and riskier (leftover Stripe* identifiers, dead domain files) than starting from a small pack. Reach for stripe only when you genuinely need its breadth (search, expand, deep state-machines). For a lean REST + UI pack, calcom is the better clone.

"OpenAI-compatible" vendors: the REJECTIONS are the fidelity surface. For a vendor whose API mimics another's (groq, together, openrouter-style surfaces), copying the exemplar's permissive validation is precisely the bug: what distinguishes the real vendor is what it REFUSES (groq 400s n>1 and logprobs; closed service_tier sets; missing fields the template vendor has). Read the vendor's own compatibility page FIRST and model its refusals; two of the groq build's three §9 blockers were compatibility-assumption false-greens — the twin served n>1 and logprobs that real Groq refuses (OpenAI's permissiveness assumed to hold), with verifies cementing both (2026-08-31; the third blocker was a connector id-mapping defect, a different class).

Vendor archetype Copy from Why
REST CRUD + a mirror (lean) calcom the usual starting point — REST + a small data-coupled mirror, without stripe's bulk
REST CRUD resources (rich) stripe (list/expand/search) or clerk (CRUD + real crypto: RS256 JWT + JWKS) when you need the breadth
GraphQL linear dual schema: hand-written + SDL-derived BEHAVIOR map; honest unmodeled-mutation errors
Generative (LLM, OpenAI-COMPATIBLE surface) groq / mistral / deepseek the anthropic template already adapted to the OpenAI-shaped wire — copy within the protocol family, not across it (deepseek rebuilt the protocol because this row was missing, 2026-08-31); the REJECTIONS remain the fidelity surface
Generative (LLM — can't run the model) anthropic / openai deterministic [twin-stub:…] content + faithful protocol envelope; the stub IS the answer, filed nowhere as a gap
Generative (audio — can't run the voice/acoustic model) elevenlabs the lean generative-audio exemplar (~1.4k lines, no mirror): real CRUD resources (voices, dictionaries, dubbing, history) + [twin-stub:tts]/[twin-stub:stt] deterministic stubs under a faithful envelope. For transcription-shaped surfaces (submit→poll lifecycles), deepgram / assemblyai
Protocol-quirky (XML / AWS-JSON + signed requests) aws (its aws-s3 / aws-dynamodb / aws-timestream service-areas) XML + AWS-JSON render + real SigV4 signing/verify + presigned URLs, over ONE shared aws-sigv4/aws-error/aws-credentials core. NB: AWS is one vendor twin with service-areas, not three packs — add a new AWS service as an area here, don't clone a new pack
Email / webhooks / local inbox resend svix HMAC signing + injected offline deliverer + inbox mirror
Ingestion → grouping + Web API sentry SDK ingest endpoint folds into queryable resources
Management API + real local engine supabase twin the control plane; run the real data plane via the world-runtime external service (don't reimplement)
Control API + real execution plane behind its handler fly; also study github's git plane inject a runtime/engine seam so the twinned control plane can drive a real local engine without making that engine a gate prerequisite

Real-execution-plane doctrine. First decide where the real engine belongs. Supabase delegates a whole independently managed data service to a world-runtime external service. Fly cannot: a Machines API request must launch, inspect, stop, and destroy the engine behind that request, so FlyContainerRuntime is injected into the handler. GitHub's git plane is a lighter variation: the handler drives the local reference git implementation and exposes its smart-HTTP seam separately.

For the injected-runtime shape, copy all of Fly's invariants, not just its interface:

  • Default to a pure virtual runtime so the ordinary gate needs no daemon. Capability verifies inject a recording/failing fake and assert the calls and their arguments; a virtual no-op is not proof that the handler drove the plane.
  • Reflect engine facts back into twinned state. Fly calls runtime.inspect() on reads and folds a real exit into the machine state/event ledger. If the engine cannot affect the control-plane projection, the two planes can silently disagree.
  • Give engine artifacts deterministic, pack-namespaced names, and reap the prior artifact before a relaunch/update. Fly's fly-twin-<machineId> naming makes cleanup possible, but also means a relaunch without runtime.remove() fails on a name collision.
  • Put the real-daemon proof in a dedicated integration test that checks the daemon, skips loudly when absent, and states that the rung was not verified. It proves the adapter only and must never be counted as a capability done; all capability proof remains offline over the injected fake.
  • If the local reference engine is a deliberate gate prerequisite, as git is for GitHub, fail loudly when it cannot spawn and give its protocol plane its own mutation seam and isolated kill-list. Do not silently fall back to a fabricated success.

0.5 Copy the exemplar safely (mechanical)

Don't hand-create files — clone the chosen exemplar and rename. From the repo root (sed -i '' is macOS; on Linux use sed -i):

VENDOR=polar        # your new vendor (lowercase)
EXEMPLAR=stripe     # the archetype you picked in step 0
Cap() { printf '%s' "$1" | awk '{print toupper(substr($0,1,1)) substr($0,2)}'; }   # polar -> Polar
Up()  { printf '%s' "$1" | tr '[:lower:]' '[:upper:]'; }                           # polar -> POLAR

cp -r "packages/twin/$EXEMPLAR" "packages/twin/$VENDOR"
cd "packages/twin/$VENDOR"
rm -rf node_modules                                   # don't copy the exemplar's installed deps
# rename files: stripe-foo.ts -> polar-foo.ts (src + client)
for f in src/$EXEMPLAR-* client/$EXEMPLAR-*; do [ -e "$f" ] && mv "$f" "${f/$EXEMPLAR-/$VENDOR-}"; done
# rewrite identifiers inside every file -- FOUR case-form pairs, not two: stripe->polar,
# Stripe->Polar, STRIPE->POLAR (the manifest export `STRIPE_CAPABILITIES` matches neither of the
# first two), and -- for multi-word vendors only -- the lower-camel mid-identifier form
# (`scrapeCreators`, `elevenLabs`), which no shell helper derives; see the multi-cap bullet below.
# `grep -rIli` (case-insensitive), or a file whose only mention is the UPPER form is skipped.
grep -rIli "$EXEMPLAR" . | xargs sed -i '' "s/$EXEMPLAR/$VENDOR/g; s/$(Cap "$EXEMPLAR")/$(Cap "$VENDOR")/g; s/$(Up "$EXEMPLAR")/$(Up "$VENDOR")/g"
cd -

Then immediately sanity-check the rename before writing any new behavior:

cd packages/twin/$VENDOR && bun install && bunx tsc --noEmit   # must be clean

This catches missed renames (collided imports, leftover Stripe* identifiers) at the type level before you build anything. Now adapt: the pack descriptor (vendor/bin/resources/specSource), the package.json description + the vendor SDK devDep (verify the SDK's real npm name — the mechanical rename guesses wrong, e.g. PostHog's is posthog-node, not @posthog/node), and start replacing the exemplar's domain logic with yours. (Caveat: a vendor whose name is a substring of a common word can over-match — eyeball the grep -rIl list first.)

Pin the SDK's major deliberately — a vendor SDK's major may track an API major. latest can speak a different API version than the one you're modeling: @mendable/firecrawl-js 4.x speaks /v2, so modeling v1 required pinning ^1. Check which API paths the SDK's default client hits before accepting the rename's carried-over version range.

The clone+sed rename also leaves landmines the tsc check CANNOT catch — grep the copied pack for the archetype's name and hosts after renaming (grep -rI 'stripe\|api\.stripe\.com' packages/twin/$VENDOR) and read what survives:

  • Order rename seds LONGEST-MATCH-FIRST. A vendor name that is a substring of a longer literal gets partially rewritten, and the hybrid reads plausibly enough to survive skims: a build renaming off the openweather exemplar turned api.openweathermap.org into api.scrapecreatorsmap.org — a host neither vendor has ever used — and it survived several reads before a grep caught it. Rewrite the long forms (hosts, compound literals containing the vendor name) before the bare vendor name, then grep for surviving fragments of the exemplar's compounds.
  • A vendor name that is PUNCTUATED in its host does not match the bare name at all. The longest-match rule above assumes the literal contains the vendor string; calcom does not appear in api.cal.com, so renaming off the calcom exemplar leaves every api.cal.com literal untouched and tsc stays silent. Before renaming, write down the exemplar's HOSTS as well as its name, and grep for each host separately afterwards.
  • sed rewrites host literals into nonsense. api.stripe.com inside a copied test becomes api.polar.com — a string, so tsc is silent, and the test now asserts against a host neither vendor uses. Fix every surviving host/URL literal by hand.
  • Copied budget tests may still assert the ARCHETYPE's documented rate limits and drive its code paths — a green run proves the exemplar's numbers, not your vendor's. Rewrite them against your vendor's published limits ("Budget a rate-limited vendor" above), or delete them until you do.
  • A command-style API (ONE path, the operation named by a query param or body field) must encode the operation in its budget price key. Pricing by METHOD /path alone charges a domain PURCHASE the same as a balance read when every command shares /api3.json — which is the entire thing the weights exist to prevent. Key the priced call as e.g. GET /api3.json command=<command>, and NORMALIZE the caller-supplied value (lowercase + trim) before the anchored rules see it, so an input variation can never override the priced op: command=REGISTER and command=register both pass the vendor's own dispatch, and either would slip a money-moving call past a command=register$ rule to be priced as a 1-unit read (dynadot-budget.ts, dynadotCallWeight).

⚠️ Scripting edits with JS String.replace instead of sed has its own landmine: $ is special in the replacement string. $', $`, $&, $n are substitution patterns, so a replacement containing one — a bash $'…' quote inside the code you're inserting is the classic — silently corrupts the output ($' splices in everything after the match; one builder's scripted edit was silently truncated this way), with no error and tsc none the wiser until the mangled file is compiled. Escape literal dollars as $$, or pass a function — .replace(pat, () => text) — which disables pattern expansion entirely.

⚠️ Prefer line-oriented tools (sed/awk/perl -pe) for mechanical renames, and run file over the touched sources afterwards. One build's scripted replaceAll pass wrote a source file back corrupted to BINARY — and every later text check went quietly blind: grep silently matches nothing in a file it classifies as binary, so even the verification greps read as "no survivors". After any scripted rewrite, file packages/twin/$VENDOR/src/* packages/twin/$VENDOR/client/* | grep -v text must print nothing before you trust a single grep over the tree.

Three vendor shapes the mechanical recipe handles badly — know them before you run it:

  • Multi-cap vendor names. Cap() uppercases only the first letter, so elevenlabs → Elevenlabs and assemblyai → Assemblyai — but the packs' real identifiers are handleElevenLabsTwinRequest / handleAssemblyAITwinRequest. For these vendors, do the identifier half of the rename with explicit sed pairs (or fix it up immediately after) rather than trusting Cap(); the tsc --noEmit sanity check will not catch a consistently wrong casing, only a mixed one.
  • Hyphenated vendor ids. ai-gateway → Ai-gateway is never an identifier you want, and the bigger break is the caps-export convention: scripts/mutation-test.ts's http() helper derives the manifest export as <NAME>.toUpperCase()_CAPABILITIES, and AI-GATEWAY_CAPABILITIES is not a valid identifier. The precedent is packages/twin/ai-gateway: export the array under a normal identifier and add an ES2022 arbitrary-module-namespace string alias — export { AI_GATEWAY_CAPABILITIES as 'AI-GATEWAY_CAPABILITIES' }; (ai-gateway-capabilities.ts) — or wire the pack in PACKS as a hand-written entry with an explicit capsExport instead of via http() (ai-gateway does both; see the comment above its entry).
  • The generative-LLM archetype fits clone+sed poorly. Most of an LLM pack is domain files — stub content, streaming/SSE, scenario scripting, token accounting — that share nothing with a different vendor's protocol. Copy the shell (package.json, tsconfig, cli, index, the capabilities test) and write the domain files fresh, rather than sed-renaming the whole exemplar and deleting your way out.

⚠️ If the vendor has NO canonical SDK, DELETE the devDep line — don't invent one. The mechanical rename rewrites the exemplar's "stripe": "^x" into "<vendor>": "^x", which for Figma produced a real-but-unrelated npm package ("figma": "^22.2.1") and broke bun install outright. Vendors with no official client are normal here (figma, most REST/XML vendors). Your fidelity test then drives the twin over real-transport fetch instead of an SDK — §10 explicitly allows this, and it proves the same thing: an unmodified client speaks to the twin.

1. File layout

A pack lives at packages/twin/<vendor>/. Required files (names are conventional and the gates key on them):

packages/twin/<vendor>/
  package.json
  tsconfig.json                 # copy from any pack verbatim
  README.md                     # MUST have a `## Coverage` section
  client/                       # omit ONLY if the API is the product ("Does this vendor get a mirror?")
    <vendor>-mirror.tsx         # the React mirror UI (browser entry)
    <vendor>-mirror.css
  src/
    index.ts                    # runtime exports + the `pack` descriptor (NO conformance export)
    cli.ts                      # bin: serve | conformance (+ mirror only if the pack has a UI)
    <vendor>-twin.ts            # the request handler (routes → kernel writes/reads)  [+ handleXxxTwinRequest]
    <vendor>-server.ts          # FETCH-FIRST: createXxxTwinFetch (the serve path as a value) + createXxxTwinServer = an async serveHttp around it (the kernel's seam)
    <vendor>-mirror-ui.ts       # (UI vendors only) buildXxxMirrorClient + createXxxMirrorServer
    <vendor>-connector.ts       # pull/push over an INJECTED client (real SDK in prod, fake in tests)
    <vendor>-conformance.ts     # spec/recorded-diff check (dev-only; imported lazily by cli)
    <vendor>-capabilities.ts    # the MANIFEST (the real vendor surface) + xxxCapabilities()
    <vendor>-capabilities.test.ts   # baseline gate (REQUIRED — the meta-test checks it exists)
    <vendor>-twin.test.ts       # unit/integration tests
    ...domain files as needed (e.g. <vendor>-events.ts, <vendor>-ingest.ts, <vendor>-sigv4.ts)
    <vendor>-sdk.integration.test.ts   # drive the REAL vendor SDK at the twin (devDep) — proves fidelity

2. package.json (copy this shape)

{
  "name": "@volter/twin-<vendor>",
  "version": "0.1.0",
  "description": "Local <Vendor> twin … built on @volter/twin.",
  "author": "Volter (https://github.com/volter-ai)",
  "license": "Apache-2.0",
  "type": "module",
  "exports": { ".": "./src/index.ts" },
  "bin": { "world-<vendor>": "src/cli.ts" },
  "scripts": { "test": "bun test src/*.test.ts", "typecheck": "tsc --noEmit" },
  "dependencies": { "react": "^19.2.7", "react-dom": "^19.2.7" },
  "peerDependencies": { "@volter/world-core": "workspace:*" },
  "devDependencies": {
    "@volter/world-core": "workspace:*",
    "@volter/world-tooling": "workspace:*",
    "<vendor-sdk>": "^x",          // TEST-ONLY — never a runtime dep (a twin fakes auth locally)
    "@types/bun": "^1.2.20", "@types/node": "^24.0.0",
    "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3",
    "typescript": "^5.9.0"
  },
  "engines": { "bun": ">=1.2.0" }
}

Rules the architecture guardrail enforces: the vendor SDK is a devDependency only; the pack peerDepends on @volter/world-core; @volter/world-tooling is a devDependency (conformance is dev-only).

3. index.ts — runtime exports + the pack descriptor

Export the runtime surface (handleXxxTwinRequest, createXxxTwinFetch, createXxxTwinServer, connector fns, events, mirror builders) and a pack: TwinPack descriptor so tooling can discover it. Do NOT re-export the conformance module (it's dev-only — the guardrail fails if runtime entrypoints pull it in).

The serve path is FETCH-FIRST (R12b; the wire). The pack's whole HTTP surface is a plain (Request) => Promise<Response> named createXxxTwinFetch — the catalog's walkers (scripts/vendor-fetch.ts), the branch round-trip and shape parity drive it in-process — and createXxxTwinServer is nothing but await serveHttp({ port, idleTimeout, fetch }) around the SAME closure: the kernel's HTTP seam (architecture), so the factory is async and returns the seam's { hostname, port, url, stop }, its url a URL ending in /. Never Bun.serve: the bin runs under node, and scripts/pack-hygiene.ts refuses a Bun.serve( call in a pack that never uses the seam. Build the fetch with the kernel's ONE adaptation, createTwinFetchFromHandler (the manifest door, body read, header map, worldNow() stamp, and JSON/empty-body reply live there once; your pack contributes VALUES — its manifest, header extras, static handlerOptions, extra responseHeaders). See resend-server.ts for the canonical adapter form and groq-style generative packs for the manifest-as-thunk + scenarioStatus form. A wire that genuinely exceeds the common shape replaces the adapter with a hand-written fetch (openai-server.ts is that reference); the grounds are: SSE streaming, multipart adaptation, raw-bytes/non-JSON responses, a COMPRESSED REQUEST BODY (the adapter does await request.text(), which corrupts gzip bytes — and a real SDK may gzip above a size threshold without telling you: expo-server-sdk does it above 1024 bytes, which is invisible in every small test and appears the moment a payload grows), and glue-level orchestration — auth doors in the glue, method-dependent routing (POST-search-as-read), multi-handler write paths with event emission, or bespoke status special-casing (jira-server.ts is that reference). Two conversion rules with teeth: a pack whose server serves /twin/scenario MUST pass scenarioStatus (forgetting it silently swaps the status JSON for the handler's 404 — no compile error will tell you); and a handler returning body: null/undefined on a 2xx served the literal null through old glue but serves EMPTY through the adapter — check before converting, don't discover it on the wire. Exactly ONE export matching create*TwinFetch per index: the catalog's walkers (scripts/vendor-fetch.ts) and the kernel's gates take the first such export to drive the pack in-process. Everything reachable from the fetch — the serve path is that import graph, transitively — must be runtime-neutral: no Bun-only APIs, no fs, state only through the kernel's tree readers and write path, never a log row (scripts/protocol-2.test.ts reads the graph and refuses listEvents/listActions/ pendingActions/appendEvent on it).

The real-system adapters are NAMED on the descriptor and anchored by convention (create<Name>TwinFetch pairs with perform<Name>Action, sync<Name>FromRemote and ingest<Name>Event, same <Name> exactly; jira and slack are the references; architecture is the law):

  • perform<Name>Action(execute, entry, ctx) → PushOutcome — perform ONE entry against the vendor over the kernel's RemoteExecute (the credential applied by the kernel; the pack never holds one) and return the id (and url) the vendor minted. ctx.resolve(type, localId) is the kernel's alias resolver for a reference to a locally minted subject. The kernel's head runs the loop — checks, the perform, the landed copy with its receipt — and a pack never confirms by hand.
  • sync<Name>FromRemote(execute, { root, origin? }) — the refresh adapter: read-side only, every push-side executor method refusing loudly. It OBSERVES through observeResource and returns a count; it never appends (the kernel folds). A credential the vendor refuses is a thrown refusal, never an empty account folded over observed state.
  • ingest<Name>Event(request, { root, secret }) — verify the vendor's signature with the sealed signing secret and observe what it verified. It MUST fail closed: with a secret present, a missing or invalid signature answers 401 and folds NOTHING — the ingest door is keyless and internet-facing, so an adapter that ignores the secret re-opens an unauthenticated write lane into the observed copy. A loose ingest* name is vendor surface, never an adapter (datadog's ingestRumEvent is RUM wire). §9 reviews check this line item on every new ingest adapter.

The descriptor names them — stateSystem: { perform, refresh, ingest } — and index.ts calls registerPack(pack) on load; a protocol-2 pack that never registers has no perform adapter, so push and deploy are dead surface, and scripts/pack-hygiene.ts says so. One more wire rule the kernel enforces for adapter-form packs: a body: '' on 204/205/304 serves EMPTY.

The descriptor is not a nameplate — it is where this pack's world-facing vendor knowledge LIVES. The central per-vendor tables (SDK_TWINS, SDK_SCOPE_VENDORS, ENV_STEM_VENDORS, VENDOR_WORLD_IDS, VENDOR_HOSTS, APP_READ_ENDPOINT_ENV) are overlay targets populated FROM the descriptors, empty or shrinking toward it; a pack declares its facts here and never in them. bun scripts/pack-facts.ts compiles every pack's descriptor into the committed artifact packages/world-core/generated/pack-facts.json, which inject.cjs requires and world-runtime overlays onto those maps. Declaring a fact in both homes is not a merge — it throws (§7 points 8, 11, 12). Read the field docstrings in packages/world-core/src/packRegistry.ts before filling these in; they carry the doctrine, and architecture holds the protocol-2 half. This is the shape:

import { referenceField, registerPack, type TwinPack } from '@volter/world-core';
export const pack: TwinPack = {
  vendor: '<vendor>',                     // MUST equal the directory name (pack-facts throws otherwise)
  transport: 'rest',                      // 'rest' | 'graphql' | 'web-api' | 'raw-tcp'
  protocol: '2',                          // the platform protocol major (PROTOCOL_VERSION is 2.0); a pack is born on it
  archetype: 'crud',                      // 'crud' | 'generative' | 'signed-protocol' | 'engine-control' | 'proxy' — pack-facts refuses silence
  bin: 'world-<vendor>',
  resources: [/* the subject types the twin projects — every type a write names */],
  specSource: '<where the manifest's surface came from>',
  description: '…',
  browserRouting: { apiPathPrefix: '/v1/', loaderHost: 'https://api.<vendor>.com' },  // browser SDK only

  // PROTOCOL 2 — the pack is a plugin (architecture.md, "Protocol 2: the pack is a plugin"):
  refresh: { every: '5m', onDemand: { atMost: '30s' } },   // + webhook: true when the vendor pushes to the ingest door
  stateSystem: { perform: perform<Name>Action, refresh: sync<Name>FromRemote /* , ingest: ingest<Name>Event */ },
  roundTrip: { method: 'POST', path: '/v1/things', body: { name: 'round trip' } },   // or a sequence whose LAST write creates
  references: [referenceField('comment', 'thing_id', 'thing')],   // a field that holds another subject's id …
  referenceTrip: { method: 'POST', path: '/v1/things/{{id}}/comments', body: { text: 'round trip' } },  // … and a write that uses it
  shapeParity: 'held',                    // once the write handler and the refresh adapter store the same shape (parityOrigin when the adapter reads its scope from the origin)
  // auth: { in: 'query', name: 'key' },  // only when the vendor does not read a replaced header (architecture D4)
  // engine: { module: 'src/<vendor>-engine.ts' },  // only when state has a second half beside the tree (a git plane, bytes, SQL)

  // Vendor knowledge the kernel must never hardcode, carried as DATA:
  rateBudget: <VENDOR>_RATE_BUDGET,       // the SAME object <vendor>-budget.ts declares (§"Budget a…")
  pullPosture: 'on-demand',               // omitted ⇒ 'on-demand'; 'continuous' needs pullPostureReason
  emitter: <vendor>Emitter,               // DELIVER support, if the pack synthesizes signed events
  conformanceFields: { /* object: { field: type } */ },

  // ADOPTION — how an app repo betrays that it talks to this vendor (§7 point 11).
  adoption: {
    sdks: ['<npm-client>'],               // every official npm client of the surface you model
    pypi: ['<vendor>'],  // the vendor's OFFICIAL Python SDK distribution names (PEP 503) — the Python half of adoption discovery and coverage; [] with a reason when no Python client exists
    scopes: ['@<vendor>/'],               // npm scope prefixes wholly owned by this vendor
    envStems: ['<VENDOR>'],               // credential-env stems: '<VENDOR>' for <VENDOR>_API_KEY et al.
    worldIds: ['<other-world-id>'],       // extra world service ids this vendor answers to
  },

  // INTERCEPTION — the vendor hosts the injector routes here (§7 point 8). `hosts` XOR `hostsNone`.
  hosts: [
    { host: 'api.<vendor>.com' },                        // exact host
    { suffix: '.<vendor>.com' },                         // per-resource-host vendor
    { host: 'www.googleapis.com', pathPattern: '^/youtube/' },  // RegExp SOURCE over the pathname
    { hostPattern: '^api\\.[a-z0-9.-]+\\.<vendor>\\.co$' },  // RegExp SOURCE over the hostname (regional families)
    { host: 'other.<vendor>.com', exclude: true },          // carve a host OUT of the includes above
    { key: '<area>', hostPattern: '^<area>\\.[a-z0-9-]+\\.<vendor>\\.com$' },  // a second routing key (<AREA>_TWIN_URL) this one twin answers under
  ],
  // hostsNone: 'why this pack deliberately has no injector entry',
  // hostsClaimed: { door: '/twin/hosts', note: 'names a person makes answer to the vendor' },  // alongside `hosts`

  // WORLD WIRING — the app-read endpoint env `volter-world init` injects (§7 point 12).
  // `endpointEnv` XOR `endpointEnvNone`.
  endpointEnv: {
    name: '<VENDOR>_BASE_URL',
    templates: { <VENDOR>_EVENT_API_BASE_URL: '${url}' },  // extra vars, optional
    note: 'grounded in <the SDK source/doc that documents this var>',
  },
  // endpointEnvNone: 'why this pack deliberately injects no endpoint env',
};
registerPack(pack);   // on load: the head resolves stateSystemFor('<vendor>') in the process that serves the twin

Every field above is optional except vendor, transport, resources — and archetype, which the type marks optional and scripts/pack-facts.ts REFUSES to compile without (the serve-family ratification is deliberately unskippable, so an undeclared archetype is RED; only the scaffolder's closing output used to say so) — and, at protocol 2, protocol, stateSystem.perform and roundTrip, without which scripts/protocol-2.test.ts is RED — but the two XOR pairs are rulings, and silence is not one: fill in exactly one of hosts/hostsNone and exactly one of endpointEnv/endpointEnvNone. Declaring both sides of a pair throws in scripts/pack-facts.ts.

The two exemplars to copy from, one per half:

// packages/twin/stripe/src/index.ts — ADOPTION + HOSTS (the migration exemplar).
export const pack: TwinPack = {
  vendor: 'stripe',
  rateBudget: RATE_BUDGET,
  transport: 'rest',
  bin: 'world-stripe',
  resources: ['charge', 'customer', 'payment_intent', /* … */],
  specSource: 'test-fixtures/stripe-schemas.json (per-object JSON Schemas from Stripe OpenAPI)',
  description: 'Stripe REST twin — spec-correct shapes, events, dashUI mirror.',
  browserRouting: { apiPathPrefix: '/v1/', loaderHost: 'https://api.stripe.com' },
  adoption: { sdks: ['stripe'], envStems: ['STRIPE', 'NEXTPRIVATESTRIPE'] },
  hosts: [{ host: 'api.stripe.com' }],
  emitter: stripeEmitter,
};
// packages/twin/inngest/src/index.ts — ENDPOINT ENV (the migration exemplar).
export const pack: TwinPack = {
  vendor: 'inngest',
  rateBudget: RATE_BUDGET,
  transport: 'rest',
  bin: 'world-inngest',
  resources: ['event', 'function', 'run', 'step', 'signing_key'],
  endpointEnv: {
    name: 'INNGEST_BASE_URL',
    templates: { INNGEST_EVENT_API_BASE_URL: '${url}' },
    note: 'no injector entry: the inngest SDK is base-URL-configured through INNGEST_BASE_URL / INNGEST_EVENT_API_BASE_URL.',
  },
  specSource: '…',
  description: '…',
  browserRouting: { apiPathPrefix: '/', loaderHost: 'http://localhost:8288' },
};

Read stripe's in-line comments before writing your adoption: they record what it deliberately does not claim (stripeconnect, whose Connect area is unmodeled) and why — a credential this twin cannot honor is build-surface, not coverage. Over-claiming an env stem makes covers report a world covered that isn't.

Whenever you edit any of these fields, re-run bun scripts/pack-facts.ts and commit packages/world-core/generated/pack-facts.json — see §7 point 13.

4. cli.ts — serve | mirror | conformance

serve → createXxxTwinServer, mirror → createXxxMirrorServer. The conformance command must dynamically import() the conformance module so it never enters the runtime entrypoint graph:

} else if (cmd === 'conformance') {
  const { checkXxxConformance } = await import('./<vendor>-conformance.ts'); // lazy — dev-only
  …
}

5. Build on the shared kernel — DON'T reinvent

Reuse @volter/world-core: applyTwinWrite, twinResources, ownFields, subjectHistory and observeResource (protocol 2), and from @volter/world-tooling: checkCapabilities / assertManifestBaseline / spec & recorded-diff harnesses. readOnly must forbid writes (405). Unmodeled ops must fail like the vendor (4xx) — never fake success. The tree's rules for a pack — declared resources, _-prefixed bookkeeping, // cache:/// counter: for the one map a serve module may hold, ownFields, tombstones, a sequence as subjectHistory, a set as one subject per member, ids minted from the tree — are the tree contract; a lookup by a local id after adoption goes through resolveSubjectId once at the request boundary (alias-aware lookup); a derived field the wire serves is stored on the write that changes it, never computed at read (shape parity). The notes below are the working versions.

Note on subject ids. The kernel's applyTwinWrite resolves the resource it returns by (type, id) (packages/world-core/src/serve.ts), so two resource types sharing an id do NOT collide — the kernel does not require type-prefixed ids. Still prefix your ids wherever the real vendor does (flag_1, key_1, evt_<hash> — e.g. sentry rule_, key_; stripe ch_): vendor-faithful ids are part of the surface. Either way, a failable verify (assert the returned id/values, §6) is what catches a wrong-resource bug; a status-only one (status === 201) does not.

How you MINT an id matters as much as its shape: derive the next id from the id-SET already in state (scan the projection) or use entropy (crypto.randomUUID) — NEVER a module-level counter or a row count. Three packs' adversarial reviews independently found collision/ data-loss bugs of exactly this class: a count-mint collides the moment a pulled vendor id sits in a gap above the row count — local creates count upward until they reach the pulled id and silently clobber it.

Read vendor fields through ownFields. The tree's resource envelope has id, type and updatedAt for the subject. Vendor fields with the same names are retained separately by treeResources; ownFields(resource) restores them. Do not assume a spread or JSON serialization of the resource envelope returns the vendor payload. See packages/world-core/src/log.ts.

R12b is module-level. Keep filesystem and process helpers outside the serve module; an unused import still enters that module's dependency graph.

A vendor whose only READ verb is POST is INVISIBLE to the invariant harness and must declare a store door. scripts/invariants.ts decides what a read is with st.method === 'GET' and nothing else, so a replay whose reads are POSTs (Expo's getReceipts, a POST-search-as-read API) presents an all-empty read set and the row fails. The store door is not only for write-only ingestion twins — it is for any pack whose reads do not travel by GET.

A local write is an occurrence. Two calls are two entries even when their content and frozen timestamp match. Kernel write IDs include an occurrence ordinal. At-most-once behavior is opt-in through idempotencyKey, scoped to service, operation, subject and key. A vendor that supports idempotent creates must resolve that key before minting a new subject ID.

Observation and local writes share one tree. Write only the fields the operation owns; an explicit null is a real update. Refresh uses observeResource, with complete vendor reads and vendor-specific handling of missing resources.

Landing replaces confirmation suppression. A landed entry is held by the parent with its receipt; reads avoid applying the branch copy twice. Preserve compound projections and vendor-ID aliases through the kernel. Verify the tree after landing, including related resources and reads addressed by the original local ID. Let the kernel own landing rather than rebuilding suppression with a pack-local log fold. The compatibility name confirmAction now performs landing; its name is not a description of the old confirm-and-suppress model.

6. The capability manifest (the heart of it)

<vendor>-capabilities.ts is the real vendor surface as the denominator (not what you've built — authored top-down from the vendor's API). Each entry: { id, area, title, dimension, tier, expected, verify? }. Tier honestly (core → common → niche). A capability counts done only when its verify() passes; verify() is offline + deterministic + FAILABLE — a real create→read→assert (and assert the vendor's negative 4xx paths). UI capabilities: assert the built mirror's markers AND that the screen is data-coupled to real twin state (start the mirror server / read the same projection — not marker-only). Anything not modeled yet → todo. There is no other status: a twin is a deterministic, offline model of the vendor's API contract, and "the real thing" (live model output, real delivery, live data, infrastructure physics, hosted pixels, real external trust) is never a capability the twin lacks — the labeled stub, the offline lifecycle, the seeded credential ARE the twin's answer. Never file those as gaps.

<vendor>-capabilities.test.ts (REQUIRED) gates honesty via the shared baseline:

import { assertManifestBaseline } from '@volter/world-tooling';
import { <vendor>Capabilities, <VENDOR>_CAPABILITIES } from './<vendor>-capabilities.ts';
// asserts: 0 regressions, done>0, total>=50, unique ids, todo>0 (never 100%).

Discovering the real vendor surface (so the denominator is honest)

The manifest is the vendor's real surface, not a list of what you built — so you have to go find that surface, top-down:

  1. Read the vendor's API reference / OpenAPI / GraphQL SDL. For REST vendors, list every resource type × every method (Stripe ≈ 80 resources × 4-6 methods). For GraphQL, read the SDL's Query + Mutation + the object fields. Skim the changelog for recent additions. When the vendor publishes a first-party OpenAPI document, fetch it and ground the denominator in it top-down — then mechanically diff every status code your manifest asserts against the spec's responses (a one-off script is fine); that diff caught five wrong asserted codes in one pack. Run that diff against the FRESHLY FETCHED spec: the committed census fixture carries no status codes at all. It used to carry the first one per operation, which read as the operation's whole response set — a 2026-09-03 build diffed against it and called four correct statuses mismatches. Carrying one of several is worse than carrying none, because it looks complete, so the derivation now carries none and every fixture says so. The fixture is an operation INVENTORY by design (path × method is the census denominator); status codes are not what it is for, and the vendor's own document is the only oracle for them.
  2. Map to capabilities at ONE granularity and stick to it — fine-grained per-operation (Stripe: customers.create, customers.search) or per-domain (Linear: Issues: create). Match your exemplar's style.
  3. Tier by real usage, not by what's easy: core = first-week-of-every-integration; common = frequent; niche = admin/long-tail/power-user. Don't tier up to inflate coverage — if <~50% of integrations touch it, it isn't core.
  4. Most entries start todo. It is correct and expected that a new pack reads LOW (e.g. 15 done / 120 total). Better a broad honest denominator at 12% than a thin self-portrait at 90%. Growing the denominator later (coverage % dropping) is success, not regression.
  5. Every entry is done or todo. There is no exclusion. A twin is a deterministic, offline model of the vendor's API contract: the app cannot tell a simulated head from a real one, and a manifest verify runs offline. So "the real thing" — live model output, real delivery or egress, live or global data, infrastructure physics, vendor-hosted pixels, real external trust, real human review — is never a capability the twin lacks; the labeled stub, the deterministic value, the offline lifecycle and the seeded credential ARE the twin's answer, and the endpoint that serves them is done. Never file "not real" as a gap. Everything a local twin could serve and does not — any wire protocol included — is a todo. Nothing is ever "chosen away".

If your pack commits a <VENDOR>_AREAS census gated by assertAreaCensus (the TWIN-87 pattern — github, openai, polar, … ), know its limit: it asserts a two-way bijection between the census array and the areas your manifest mentions — a vendor area missing from BOTH passes silently. The census has teeth only if the array itself is enumerated top-down from the vendor's docs nav / OpenAPI tags; deriving it from your manifest's areas makes the check a tautology. The denominator audit is the enumeration, not the assertion.

Writing a FAILABLE verify() (so "done" means done)

verify() must FAIL if the capability is broken. The test: imagine the handler were deleted/returned {} — would this still pass? If yes, it's a false-green. Rules:

  • A READ-ONLY vendor still has to be seedable — add twin-only control routes. Several vendors expose no write endpoint at all (figma is read + comments; openweather/googlemaps/mapbox/webrisk are pure lookup), so "seed through the twin's write path" has no vendor path to use. The convention is a small set of twin-only routes outside the vendor's surface, which the verify seeds through and a real client never calls: POST /twin/observation (openweather), POST /v1/_twin/... (figma, which also arms a deterministic 429 that way). Keep them clearly namespaced and out of the capability manifest — they are test scaffolding, not vendor surface, and counting them as coverage would be padding.
  • A WRITE-ONLY protocol has the inverse problem — do not invent a vendor read path. SMTP can accept a message but has no verb that returns submitted messages. Its twin-only /twin/messages inspect sidecar makes local outcomes usable, but stays outside the vendor capability manifest for the same reason the seed routes above do. Connector pull observes only what the real protocol actually reveals: SMTP's 220 banner and EHLO capability list. That is genuine remote state and makes the twin advertise the relay it mirrors without pretending the relay has a mailbox API. If a write-only target exposes no readable handshake, metadata, or receipt at all, record pull as a reasoned impossible connector gap; never fabricate resources merely to satisfy the usual pull shape.
  • Fresh state every time — and thread the root EVERYWHERE. Use the pack's withRoot/temp-root helper (or mkdtempSync) so the verify proves the capability from nothing — never relies on pre-seeded/dirty state. An omitted root does not error: the kernel falls back to the operator's real ~/.volter state dir, which is gitignored, so nothing catches the leak — the verify passes while poisoning later runs. Thread a fixed occurredAt (the exemplars use a pinned constant) so ids/timestamps are deterministic. A verify that REPEATS an identical transition no longer needs distinct instants to keep both writes: a local vendor write is an OCCURRENCE (two calls are two actions, even in one instant — see §5's kernel note), so the old coin flip between replayed and a second write is gone. Pin instants anyway, for ORDER: two events that should read in sequence deserve distinct occurredAt values, and a pinned one makes that chosen rather than rolled.
  • …but fresh-root-only verifies structurally CANNOT catch dirty-state bugs — behavior that only breaks over prior state. Every stateful pack must also carry at least one deliberate dirty-state verify (delete→recreate, interleaved minting) that builds up state and then asserts the invariant that survives it. This is not hypothetical: three packs' independent §9 reviews each found exactly this bug class — gcs re-issued a deleted object's generation on recreate (gcs.generations.overwrite_new_generation now pins the ratchet across delete+recreate), supermemory's sticky tombstone swallowed a re-add and its id mint had to count tombstoned rows (supermemory.documents.readd_after_delete), and assemblyai's deleted transcript regenerated content on a later poll (assemblyai-twin.ts, the redaction guard). A manifest whose every verify starts from a fresh root has proven nothing about any of these.
  • The kernel's content+millisecond dedupe (§5) needs different medicine per PATH: pin in verifies, MOVE in production defaults. Handler-side, a high-churn twin whose semantics revisit values folds a per-write ordinal into the write's fields (upstash-store.ts, rev). Connector-side, pull entry points need a MOVING occurredAt default — now, forced strictly increasing within the process (dynadot-connector.ts, pollTimestamp()) — never a pinned constant: the kernel hashes an observed event over (occurredAt + post-state), so under a fixed poll time a vendor value that REVERTS (A→B→A across polls) collides with its own earlier observation, and the kernel observation path reports deltasAppended: 1 while nothing lands — a phantom delta, with the projection still serving the stale value. Audit note: calcom-connector.ts still ships a pinned PULL_AT as every pull's default — a known live instance of this hazard, not a pattern to copy.
  • A REFUSED pull is NOT an empty account. A connector that maps a failed or refused vendor reply to an empty resource list hands the kernel observation path an empty account to fold OVER real observed state. Throw on refusal instead — and mind vendors that answer failures with HTTP 200 plus an error envelope, where a status check alone cannot tell refusal from genuine emptiness (dynadot-connector.ts, pinned by dynadot.connector.pull_refuses_a_200_failure).
  • Real create → read → assert VALUES, not just a status code. ✗ return res.status === 201 (passes on a malformed body). ✓ return ok(res) && field(res,'email') === 'a@b.test' && id(get) === id(res).
  • Assert the negative path the title implies — the vendor's 4xx for missing/invalid input (unknown id → 404, bad enum → 400, wrong state-machine transition → 400). Unmodeled-ops-fail-like- the-vendor is a testable property.
  • UI capabilities — unless the API is your vendor's product (settle that with "Does this vendor get a mirror?" above; a pack with no mirror has no UI caps at all). When there IS a mirror: every UI capability must be DATA-COUPLED — seed real state through the twin's write path, fetch the SAME api/projection the screen reads, optionally render the mirror's own component over it, and assert the seeded value survives (use the shared uiDataCoupled factory from @volter/world-tooling; see jira.ui.backlog, sentry.ui.issues, posthog.ui.events). This is not just for the main list/detail screens — TWIN-14 / B4 killed the old marker-grep escape hatch (uiHas(...), which only proved a screen's literal strings survived in the built bundle, not that data flows through the twin) for EVERY UI capability, including nav shells, search inputs, and tone/badge pills: a nav shell's honest data-coupling is proving every screen it routes to reads live data (see sentry.ui.nav); a tone/pill's is proving the mirror's own tone-mapping function produces the seeded tone (see sentry.ui.level_status_pills, calcom.ui.status_pill); a search input's is a real filtered fetch (see sentry.ui.search) or, when the vendor's search truly filters client-side only, asserting the seeded row is in the data source the search operates over (see stripe.ui.search). The strongest form of "render the mirror's own component" is literal: server-render the mirror's REAL exported component over the twin's projection (react-dom/server's renderToStaticMarkup) and assert the seeded value in the emitted HTML — the claim is then about the shipped component, not a lookalike, and many packs now do exactly this (dynadot's empty-state verify renders the real component AND proves the empty state gives way once state exists). If a screen truly has no seedable data at all (e.g. "does the client transpile", or a documented no-recordings replay shell), the honest fallback is a structural verify — dynamically import/render the REAL component and assert on its actual output — never a bare marker-grep and never a tautology (see aws.s3.ui.transpiles, posthog.ui.session_replay). The mirror's main view — the one showing the vendor's core modeled data (events, issues, models, …) — is a data screen and is NEVER "chrome"; don't lenient-classify your main screen as chrome to dodge data-coupling — a §9 review will demote it (it did for OpenRouter's ui.mirror_markers).
  • Markers must survive minification. A class built from a template literal (`event-${tone}`) is erased by the bundler — asserting event-pageview will fail. Assert static literal class names or the pure-function string outputs the component actually emits (dump the bundle once if unsure).

Copy a real exemplar verify rather than inventing the shape: clerk-capabilities.ts (CRUD + crypto), stripe-capabilities.ts (search/state-machines/negative paths), linear-capabilities.ts (GraphQL).

Scenario scripting (generative packs)

A generative twin's default stub is generic; a scenario lets a caller script the exact deterministic responses a flow under test needs — a JSON file of ordered match → response rules, first match wins, no match falls back to the normal stub — while the protocol envelope stays vendor-faithful. Four packs are the precedent: anthropic-scenario.ts (scripted assistant turns incl. tool_use, unary + SSE), gemini-scenario.ts, deepgram-scenario.ts, assemblyai-scenario.ts. The conventions they share:

  • Strict loud-fail load validation — an invalid scenario file throws at load with what's wrong, never a silent ignore-and-stub (a mis-typed rule silently falling back is a fake success).
  • Wired at server construction (createXxxTwinServer({ scenarioPath }) / an env var / a CLI flag), scoped to the generative endpoint only — never to CRUD/batch surfaces.
  • Kept OUT of the capability manifest. Scenario support is twin-only scaffolding for eval worlds, not vendor surface — counting it would pad the denominator (same rule as §6's twin-only seed routes; deepgram-capabilities.ts states this in-line). Gate it with a dedicated <vendor>-scenario.test.ts instead. (assemblyai counts a scenario area in its manifest — treat the manifest-free majority as the convention, not that outlier.)
  • Faults are the kernel's grammar, not the realizer's. Any handler may carry fault: { kind: 'slow', ms } holds the answer and then serves (it may still carry respond); { kind: 'status', status, retryAfterSeconds?, message? } serves a 4xx/5xx in THIS vendor's envelope — give the adapter a renderFault so a 429 wears the shape the vendor's 429 wears (openai-scenario.ts is the reference); { kind: 'drop', holdMs? } never answers. Same once/phase/scope as any handler, so a mid-story outage is { on, fault, once }. Decide through engine.serve(req) (it honors the fault; a hand-written path calls scenarioFaultResult), never next() alone — a realizer that reads next() serves content through a fault. The load is strict-loud: a status fault is 400..599, never a success.

The conformance check needs the same teeth: delete the handler, it must go RED

Hold <vendor>-conformance.ts to the failable-verify test — if the handler it certifies were deleted, would this check fail? Two packs' §9 reviews independently found deletable-API false-greens here, in two escalating shapes:

  • Two constants asserting about each other is not a check. tinybird's first version compared a hand-written implementedEndpoints array against hand-written expectations in the same file — deleting the entire router left it green. The trap generalizes well past conformance files: a verify() or test that IMPORTS the constant the handler itself serves and asserts equality is the same disease in miniature — both sides drift together, so the assertion can never catch the value going wrong. Write the expected value as a LITERAL in the check. firecrawl does this deliberately for its vendor-faithful response URLs and says why in-line: an assertion against the handler's own FIRECRAWL_API_HOST "would be a tautology that could not catch the host drifting" (firecrawl-budget.test.ts, allowlist notes).
  • A PARAMETER graduating from refused to modeled is its own hazard, and the doctrine below is stated per-OPERATION. Removing a name from assertModeledOptions does not open one hole, it opens one per route that reads it: a 2026-09-03 lane took resourceId off the refused list and silently created three wrong-subject holes at once. When you start honouring a parameter, walk EVERY operation that now accepts it and prove each one reads it, rather than accepting it and dropping it — which is a fake success wearing a parameter's clothes.
  • A conformance probe whose EXPECTED answer is a 404 may be indistinguishable from the router's catch-all. The trap below is about grading only the router's own miss on the DISPATCH side; this is its inverse, and it is a distinct trap. If your probe expects the miss envelope, it passes with the whole feature deleted. Make it assert something only the real path can produce.
  • Probing endpoints but grading only "not the router's own miss" has teeth exactly at the dispatch and nowhere deeper. tinybird's replacement checked each probe wasn't the router's Not found: response — but sub-handlers fall through to their own 405/404, so seven of the twenty-one claimed endpoints could lose their handler branch and conformance stayed green; the whole Tokens API was deletable branch by branch.

The bar: one real request per claimed endpoint, asserting the OUTCOME a live handler produces — an expected status set plus a predicate over the body — against a throwaway root. When a vendor resource carries a field name the kernel projection reserves (type, id, updatedAt — exactly those three, actions.ts), the probe must assert the field round-trips, not just the status: mistral's conversation entries carry a literal type the projection silently dropped, and every status-only check sailed past it. Check the probe table and claimed snapshot as a two-way bijection, then close the third direction with a hand-written ROUTER_SURFACE: enumerate every method/path pair the router branches on, exercise it, and fail when it answers anything other than the vendor-shaped not-found envelope without a matching snapshot claim. Probe⇄snapshot alone is blind to served-but-unclaimed surface; the ROUTER_SURFACE pass in googleoauth-conformance.ts caught a real extra endpoint. See also tinybird-conformance.ts (PROBES), upstash-conformance.ts, and the azureformrecognizer conformance.endpoint_probe pattern. A twin whose handler returns {} must fail this; that is the whole point. The router census is itself hand-authored, so §9 still compares it with the actual dispatch branches — and every census entry needs a LIVE request proving its branch is reached: a ROUTER_SURFACE that is a copy of the probe-table keys reads as compliant while asserting nothing (the groq build wrote exactly that tautology and §9 rejected it). The census earns its name only when deleting a router branch reddens the entry that names it.

When you cannot ground the vendor's error CODE, ground the class and say so. A refusal must still refuse — an unmodelable code is never a reason to answer 200. Use the nearest code you can actually cite from a first-party source, state at the seam what you grounded it on, and file a todo to pin the exact one. What you may not do is invent a number: a fabricated error_code reads as first-party fact to every consumer and to every later reader of your own pack, and unlike an invented ROUTE nothing will ever contradict it. This is the same rule as the surface one below, applied one level down.

And know the INVERSE false-green: serving surface the vendor doesn't have. There is usually no mechanical oracle for an invented operation, error code, or header — the twin can merely agree with itself — so the top-down denominator audit and a §9 skeptic must read the surface against the vendor spec in BOTH directions. But do not overstate that as "absence can never be asserted." When the vendor documents a closed parameter set, a literal allowlist is an oracle: assert the accepted keys/values biject exactly with that documented set and exercise a value outside it. That technique is what made Google OAuth's invented authorization-response iss revert cell redden. Keep the expected set literal in the check; importing the router's own constant makes the assertion a tautology. When first-party sources conflict, the oracle has a precedence order: an OpenAPI/Smithy-GENERATED type outranks a rendered docs example (a prose example looks exactly like a documented closed set and is not one — groq's ErrorObject declares ten keys where the docs example shows two), and where only prose exists, assert subset-of-generated-type rather than exact bijection.

Three other §9-caught precedents: ahrefs answered "twin does not model history='live'" on an endpoint the vendor never gave a history parameter (fixed per-endpoint — only an option the endpoint actually DECLARES can be an unmodeled option, ahrefs-twin.ts); figma declines to fabricate rate-limit response headers the vendor doesn't document (figma-twin.ts); plain refuses to invent a MutationError its SDL never defines (plain-twin.ts).

7. Central wiring — ask the gates, then read this list

The count in this heading used to be the point, and it was wrong every time it was written: "exactly ONE place / everything else auto-discovers", then seven, then ten, then eleven, then thirteen. The first cost two 2026-07-25 builds a red gate each; six builder runs then tripped over rosters the "seven" list omitted; tinybird/upstash surfaced an eleventh; the eleven omitted the endpoint env and the pack-facts artifact; and the 2026-09-02 expo build found the thirteen-list had gone stale in two places and short in four. A hand-maintained mirror of what the gates enforce drifts from the gates. So it is no longer numbered as a claim to completeness.

THE AUTHORITY IS bun scripts/verify-pack.ts <vendor>. It runs the drift gates and names what is unwired, in seconds. Run it early and often while building; treat a red from it as the list, and treat what follows as the map that explains each red rather than as the definition of the set. If you find a wiring point this section does not name, REPORT it — this recipe is doctrine-bearing and AGENTS.md reserves it for the orchestrator, who edits it in the same pass that reads your report. A build lane that patched it directly would be the one thing this section is not allowed to become: many hands, no ruling. (Corrected 2026-09-03 after a brief of mine told a lane to edit it and the lane rightly refused.)

The points below are committed censuses, rosters, and — for the descriptor points — fields on your own pack descriptor that a generated, committed artifact publishes:

Start here: bun scripts/scaffold-pack.ts

A scaffolder exists and it mechanizes most of this section. It is idempotent — every step is skip-if-present — so it composes with scripts/spec-compile.ts / scripts/smithy-compile.ts seeds and with hand-written files, and re-running it after you have edited is safe:

bun scripts/scaffold-pack.ts <vendor> --base https://api.<vendor>.com --host api.<vendor>.com \
  [--host <extra>]... --auth-env <VENDOR>_API_KEY [--auth-header <name>] [--sdk <npm-pkg>]... \
  [--spec-kind openapi|smithy|none] [--spec-url <url>]

It writes the pack shell (package.json, tsconfig, README, twin/server/connector/conformance/ capabilities + their tests, cli), the v2 pack descriptor in src/index.ts with your --host/--sdk/--auth-env compiled into hosts/adoption and a TODO block for the endpointEnv XOR endpointEnvNone ruling, the registrations for points 1-2, the censuses for points 3-5, and then regenerates point 13. It prints one line per wiring point (wrote / skip (exists) / FAILED).

What it deliberately does NOT do is decide anything: every judgment slot is a loud SCAFFOLD TODO(A1|A2) marker (grep 'SCAFFOLD TODO' after running), no capability is born done, and it writes no VENDOR_HOSTS and no SDK_TWINS entry — those are the legacy homes, and a fact in both places throws (points 8 and 11). It also does not touch points 6, 7, 9 or 10, and assertManifestBaseline stays RED until you earn a real done; that red is the honest state of a scaffolded pack, not a defect. Copy-from-exemplar (§0) is still the right move for domain code — the scaffolder gets you the wiring, the exemplar gets you the shape.

Four of the thirteen are CONDITIONAL — and "not applicable" is a decision you make against the stated rule, never a point you may skim past: the journey registry (10) binds iff your census.json ui slice declares needsUi: true; the WIRED budget roster + the D8 mirror line (9) bind iff your pack declares a budget (a budget-less pack takes the EXEMPT roster instead — one of the two, always); a hand-written VENDOR_BURST_ANCHOR figure (9) binds iff your declared ceiling admits a bigger burst than the fallback's; the mirrorMutations seam (2) binds iff you ship a mirror. Everything else is unconditional.

1. The manifest registry — NOTHING TO EDIT, and that is the point. scripts/capability-manifests.ts used to be a hand-written map and this list's one hand-authored logic edit. It now GLOBS packages/twin/* and hard-throws on a vendor dir whose manifest module or conventionally-named export is missing. So a pack is registered by EXISTING in the right shape: name the export <vendor>Capabilities (and <VENDOR>_CAPABILITIES) and you are wired. The failure mode moved with it — a wrong export NAME is now a throw from the registry, not a silently absent pack.

2. The gate file — packages/twin/<vendor>/gate.ts (the gate plane; the scaffolder births it). Dev-plane TypeScript, never imported by src/; scripts/mutation-test.ts SCANS every pack's gate.ts — that scan IS the roster (there is no central array). It exports one gate: PackGate (or areaGates: NamedPackGate[] for a multi-area dir like aws/azure) — the vocabulary is scripts/gate-kit.ts:

export const gate: PackGate = {
  ...httpGate('<vendor>', 'handle<Vendor>TwinRequest', [/* extra mutations */], { /* allow */ }),
  // mirrorMutations / connectorExtraMutations / isolatedMutations — see below
  // burstAnchor: the vendor's documented per-minute allowance, by hand (rate-budget-isolation)
  // conformanceBaseline: this pack's reasoned gap deferrals (twin-conformance)
  // budgetExempt: a budget-less pack's excused live call sites, exact counts
  replay: [ /* the R9 resource-level sequence — below */ ],
  // streamRefusal: a pack that renders text/event-stream declares one pre-frame refusal (R6b) — below
};

The streamRefusal declaration (invariant R6b — a refusal refuses on the wire). If any source under src/ renders text/event-stream (comments do not count; the manifest does not count), the matrix requires gate.streamRefusal (module-level export const streamRefusal in a multi-area dir, like replay): ONE streaming request your vendor refuses BEFORE its first frame — an unknown model, a missing required field — with status pinned only when your own tests pin the vendor's. The row sends it through the fetch adapter and holds the answer to a 4xx that is not an event stream. The defect it exists for: a server that opens a 200 text/event-stream and pushes the refusal out as a lone data: frame, which a client that would have raised on the status silently reads and continues past — seven packs shipped it before the row existed. Collect the events first, then decide the status, then open the stream (openai-server.ts is the reference shape); a vendor whose stream has no pre-frame refusal declares a string saying why, and the cell reads n/a. bun scripts/invariants.ts --check --explain <vendor> shows what the cell actually saw.

The replay sequence (invariant R9 at the resource level) — gate.replay, or a module-level export const replay in a multi-area dir (aws, azure: one fetch adapter serves every area). 3–6 steps the harness runs through your fetch adapter on two fresh roots under ONE pinned world instant: 1–3 realistic writes, then 2–4 reads; every read is byte-compared across the roots and re-served on one root. What the pilot sweep learned (2026-09-02, ten packs, four real defects):

  • Auth is per-pack and comes first. The harness sends authorization: Bearer twin_replay by default and FAILS the row on any step answering ≥300 — a pack with vendor-shaped auth (Postmark's server token, Mailgun's Basic api:<key>, Notion's Notion-Version) puts its own headers on every step. Read your <vendor>-server.ts for header threading before writing a step.
  • A redirect that IS the vendor's success is declared, exactly. An OAuth consent post answers a 302 to the app's callback carrying the code; the step declares redirect: '<the exact Location>' and the row fails on any other status or Location. The authorize GET that records the request the post settles is marked writes: true: it runs as a write, once and never compared (a re-serve after the settle mints a new request, as the vendor would). The code and the tokens it redeems into are then read off two fresh roots and named literally (tiktok, xidentity, googleoauth, paypal).
  • Content type is part of the wire: a form-encoded vendor (Stripe, Mailgun) overrides the JSON default and passes a body STRING.
  • An upload is raw bytes. A Uint8Array body is sent as it is (a video chunk, with the step's own content-type), and a binary read (the served video) is compared byte for byte, never as decoded text. A step's capture may read a dotted path into its answer (data.upload_url), and a filled path that is an absolute URL on the replay's origin (an upload URL the twin minted) is sent to it as is. shape checks a value drawn from entropy against a pattern. On a write's answer the shaped fields are the ONLY part checked, since writes are never compared (tiktok).
  • "Is there a read at all?" is the blocking question. A write-only ingestion twin (SendGrid, Mixpanel, Segment) has no read surface — which is exactly why its nondeterminism went unseen — so it declares a store door first (stores on the adapter; GET /twin/store/<name> is the read).
  • A _twin seeding route can be the only reachable write when the vendor's object graph has no create-from-nothing (Notion's root arrives by the kernel observation path): use what the pack CAN create from an empty world — often its webhook/subscription surface.
  • Make one write depend on another's state (a booking on a seeded event type, a payment intent on a customer) so a read carries derived fields, not echoed input. At least one read must be non-empty (an all-empty replay fails the row).
  • A GET-only command API (Dynadot's api3: register/set/delete are all GET) reads as 0 write(s) to the harness and every step is RE-SERVED, so the sequence must be state-neutral (open with a seed, close with the delete) or the second pass sees the world it left behind.
  • A vendor that refuses with HTTP 200 (a ResponseCode: -1 body) can pass the row on a sequence of refusals: read every reply once by hand — the reads must carry DERIVED state (a price drawn off a balance, a nameserver block that changed), never an error envelope.
  • A real-time timer is a determinism defect by construction (sendblue's setTimeout advancing a delivery 25 ms after the response): served content must be a function of the request and stored state, so an async lifecycle is a POLL-FOLD — each read folds the next step from the stored row + req.occurredAt (assemblyai, azureformrecognizer; svix-runtime's "no wall clock, no timers" is the doctrine), and a status callback fires as part of the fold.
  • A secret derived from the root's PATH (sha256(root) into an api key or signing key) is environment, not state: identical worlds in different directories disagree. The shape that keeps cross-root distinctness honest is a seed generated ONCE per root and PERSISTED as state (fal's signing_key; the contract's one R9 exception) — the path never enters a served byte.
  • An ordinal alone can resurrect a deleted subject (twelvelabs: delete frees the name and drops the live count, the next create re-mints the dead id and the kernel revives it with its old children): a mint probes every id the type has EVER held, soft-deleted included, and advances past any taken value — the count is the seed, the probe is the guarantee.
  • The declaration is a checked claim (R2 resource-level): a declared type nothing creates and nothing serves is dropped; a pull-only type the twin serves once pulled is adopted as debt, never faked with a seed; everything else the replay extends to.
  • Count-before-insert is safe within a process, not across them: applyTwinWrite has no await between the mint and its append, so mint→append is one synchronous tick in-process; two processes writing one root at one instant is the only race, and twins-host runs one worker per vendor. A seed with NO ordinal (${instant}:${name}) is a defect even in-process — a frozen clock re-mints the same record and resurrects a consumed token, and a replay-detection short-circuit keyed on such a seed silently drops the second same-instant action.
  • Every read comes after every write. The harness re-serves the READ SET ALONE on root A after the full sequence, so a read placed between writes sees two worlds (clerk's organization list moved by a later invitation's pending_invitations_count) and fails "differs between two serves of the same state" although nothing is nondeterministic.
  • Gating headers are per-route, not per-pack (anthropic: Managed Agents routes 400 without anthropic-beta: managed-agents-…, files/skills 400 WITH it) — read each route's guard.
  • A resource may exist only as a consequence of another write (cloudflare's durable_object_namespace is born from a script deploy's migrations): the covering step is the parent write, and the read proves the child exists.
  • "No literal can authenticate" has one answer: a credential the world MINTS from entropy (never the root path) cannot be named by a gate, so the virgin world adopts the first well-formed credential a caller presents over the wire and rejects every other afterwards (sendblue's credential resource, established under the actions lock; read-only worlds accept without persisting) — the replay's first step seeds it, every later step names it, a mismatch 401s and reddens the row. A permanently accepted default constant is a backdoor.
  • A GET that moves state is a write, never a read: a metered read that renders the meter it just moved (scrapecreators), a status poll that advances a job (twelvelabs), a key folded in on first touch (fal, replicate) — each is marked writes: true, runs once and is never compared; the types it creates are reached, not adopted.
  • Resource coverage is a finder of R9 defects: replicate's webhook secret — the world's path in a served byte — sat green until the R2 declaration forced its read into a replay.
  • Writes that arm a delivery path go LAST among the writes (postmark's webhook registered after the sends): "all reads after all writes" is necessary, not sufficient — a covering write after the arming one makes the harness deliver to the open internet.
  • Gating is per route family AND per credential position: postmark's account routes take a different header.
  • Platform-authored types are (b), not (a) (supabase's backups, network bans, access tokens): no route and no pull creates them — only the real vendor's own scheduler, abuse detector or dashboard — yet a serve path reads the stored rows back. The (a) test is two-sided: nothing creates it AND nothing serves it. What separates (b) from (c) is whether the pack already OWNS an honest _twin door for the act (xidentity's rate window → (c)); a door that would have to be invented to fabricate the artifact the twin's own gate exists to enforce is not honest, so the type stays (b).
  • Disposition (b) by construction: a type the connector observes through the kernel and a served list merges, whose create-mutation the vendor SDL does not model (linear's project/cycle/ user/workflowState throw UnsupportedOperation; groq's and hubspot's pulled overlays) — a true declaration the replay cannot reach; report it, never fabricate a seed for it.
  • The fastest loop is a throwaway probe mounting WORKER_VENDORS[vendor] on two fresh roots under a pinned TWIN_WORLD_CLOCK_FILE and diffing the bytes — it names the diverging field; invariants.ts --check only says which read differs. Name the probe after your lane (probe-<worktree suffix>.ts): the session scratchpad is shared across parallel lanes and a role-named file gets overwritten mid-run by a sibling pointing at ITS worktree.
  • twin-capabilities.ts --write-baselines is catalog-wide and ratchets every pack whose done count moved (a parallel lane's fly and resend rode along); a lane writes ONLY its own pack: --write-docs <vendor> for the generated docs and the one baseline line in its census.json by hand, or revert the collateral in the same commit.
  • A red is a real defect, fixed the resend way: ids minted from ${type}:${occurredAt}:${count-before-insert} through a stable hash (mint INSIDE the actions lock from the ordinal the write takes when two identical events in one millisecond must both land); timestamps from req.occurredAt (the adapter stamps it from the world clock — a handler that reads new Date() or Date.now() is the defect). Never weaken the harness; never --adopt.

One field the seams key on lives in YOUR manifest, not here: each saboteur phase filters done specs by the capability's dimension. Declare dimension: 'connector' on connector capabilities (and 'ui' on mirror ones) — a connector verify declared as 'api' leaves the connector phase with a zero-done denominator, so its summary reads 0/0/0 and the seam is green having proven nothing (scripts/mutation-test.ts, per-dimension summaries).

Treat the entry's allow map as a bijection with its legitimate survivors, not a bag of old exceptions. The runner currently tests only candidate.id in allow; a stale key is silently ignored. Renaming a capability can therefore leave both a dead exception and a newly unallowlisted survivor. For every key, confirm the capability still exists as expected: 'done', remove that key temporarily, and confirm the mutation report names that exact id as a survivor; every survivor must then be hardened or have exactly one specific reason restored. If removing an allow entry changes nothing, delete it. Perform this audit whenever ids or dimensions change.

The mutation seam can also be an isolated execution-plane seam via isolatedMutations, not only the main mutations, mirrorMutations, or auto-derived connector seam. Use a named group with a mustRegress kill-list when killing all main seams together proves only their union and cannot attribute teeth to one plane; GitHub's git-plane group is the exemplar. Its green control must run first, while the module graph is pristine, before any mock/restore cycle. A capability already red in that control has proved nothing by staying red under sabotage. runPack deliberately runs this control at its top and only then performs mirror, connector, isolated, and combined phases; preserve that order when extending the harness.

3-6. The committed census facts live in YOUR PACK's packages/twin/<vendor>/census.json (the gate plane) — FIVE unconditional slices in order: baseline, pullAudit, ui, spec, invariants (the R1–R14 matrix row — machine-written by scripts/invariants.ts --ratchet, adopted only by --adopt). A sixth, shape (the sibling-shape ratchet — scripts/pack-shape-check.ts --ratchet), is CONDITIONAL and records debt: it exists only while the pack carries grandfathered shape misses, and --ratchet DELETES it when the last one clears ("slice dies with its last miss"). A seventh, hygiene (scripts/pack-hygiene.ts --ratchet), is conditional by the same law and holds the pack's adopted hygiene misses — a hand-rolled args helper, a server option the CLI never parses, a bare Bun.serve behind a node bin, an edit outside the pack; a NUL byte, a scaffold marker and an unregistered protocol-2 pack are never adoptable, because each means the pack is unfinished rather than differently shaped. A clean pack has five slices and its census.json is correct without a shape or hygiene key — do not go looking for one, and do not write an empty one. The scaffolder births the file; the checkers (scripts/spec-census.ts, scripts/ui-scope.ts, scripts/pull-audit.ts, scripts/twin-capabilities.ts) scan every pack's census.json and gate a two-way bijection with the packs on disk — a missing UNCONDITIONAL slice is RED, and so is a census.json in a kernel dir.

Two fields of the spec census are hard-required and are named nowhere else in this recipe: scopePaths (a non-empty array recording the committed curation boundary — every fixture operation must fall under one of its prefixes) and fullSpecEstimateOps (a positive honest estimate of the vendor's full spec size, for the sampled-fraction disclosure, and it may not be smaller than the census's own operation count). spec-census.ts --check errors by name on both. Note the spec slice's kind is a closed enum — openapi | graphql | smithy | protobuf | discovery | none (kind: 'none' REQUIRES a reason AND FORBIDS a url — the checker rejects both together) — and "the spec exists but only inside a generated SDK client" is none with that stated as the reason, not an invented kind ("sdk" went red for the mistral build). A published spec that describes a DIFFERENT HALF of the vendor than your pack serves is also none, with the spec named in the reason. Expo publishes a complete first-party GraphQL schema, and it covers EAS — the platform half the push twin deliberately does not serve. Recording it as the pack's spec source would enrol the pack in a census campaign against a denominator it can never satisfy, so the rule is: the spec slice names the spec for the surface THIS PACK SERVES, or none. Name the other spec in notes so the next builder does not re-derive the search — but check the SDK repo first: Stainless-generated SDKs commit a .stats.yml whose openapi_spec_url names the current first-party spec (anthropic's "none" was wrong for this reason), and that pointer is a real openapi source. A recorded source is only half of Supported; the other half is a COMMITTED CENSUS. Five steps, in this order, and each one answers a different question:

  1. Get the spec as an OpenAPI-shaped document. OpenAPI or Swagger: fetch it (.yaml parses via Bun.YAML). A Google Discovery document converts — bun scripts/spec-discovery-to-openapi.ts <discovery.json> <out.json> --fetched <date> (one operation per Discovery method, mediaUpload paths included — Gemini's real upload endpoint is /upload/v1beta/files while the plain path 404s). AWS Smithy models convert — bun scripts/spec-smithy-to-openapi.ts <out.json> --fetched <date> --area s3=<s3.json>@<host> … (REST operations become paths; JSON-protocol operations share POST / and are listed under x-discriminated by their X-Amz-Target value). Protobuf services convert — bun scripts/spec-proto-to-openapi.ts <out.json> --style twirp|grpc --host <host> <a.proto> … (one operation per rpc, addressed by the wire convention: twirp's POST /twirp/<package>.<Service>/<Method>, or the bare POST /<package>.<Service>/<Method> gRPC, gRPC-web and Connect all use). A pack whose areas publish SEVERAL documents passes them together: --from-file 'blob.json@host,inference.json@host/base', each document's paths carrying their own server and area tag. Azure's x-ms-paths are paths like any other.

  2. Birth it: bun scripts/spec-census-bootstrap.ts --vendor <v> --from-file <spec> --scope-paths <the prefixes the twin serves> --apply writes the scoped fixture (+ SOURCE.md), the <v>-spec-census.json and the census.json registration. It maps against DONE ids only, by the rules in scripts/spec-census-map.ts, behind a static served-surface probe; it is blind to 3-letter abbreviations like msg. A scope prefix is a PLAIN prefix, so record /chat/ to keep /chatkit out. A route several operations share behind a header value gets one entry each and the census is keyed method+path+header-field. --remap REPORTS what the current rules would map differently in a landed census, split by whether the committed id has recorded evidence — it writes nothing, because evidence, probe verdicts and hand rulings all outrank these rules and a tool that could invert that ranking once moved 52 of stripe's recorded claims onto ids their own evidence excludes. --rebuild starts over and LOSES every hand ruling. THREE KNOWN BLIND SPOTS IN THE RECORDER, so you can tell a tooling limit from a pack fact. It reconstructs an operation coordinate only for HEADER-discriminated entries, so (a) a graphql-root-field census records NOTHING — every verify's POST keys as the bare endpoint (plain: 0 of 532, slack: 0 of 174); and (b) it never reads a requestKey: "path" census. Both UNDER-record rather than over-claim, which is the safe direction, but write the limit into $recordComment so the next reader does not mistake an artifact for a gap.

    A MULTI-SEGMENT path parameter is supported, but you must DECLARE it. If a parameter's value may contain a slash — bitly addresses a link as bit.ly/000001, so /bitlinks/{bitlink}/clicks is reached at /bitlinks/bit.ly/000001/clicks — name it in the census's multiSegmentParams array and the recorder will absorb the extra segments. It is declared rather than inferred on purpose: whether a parameter may hold a slash is a fact about the VENDOR, and inferring it from path shape was measured minting false evidence in both directions (azure's all-parameter /{containerName}/{blob} swallowed /v1/models/gpt-4o and a _twin door; stripe's POST /v1/customers/{customer} swallowed a cash-balance-transaction create). Undeclared is the default and absorbs nothing.

  3. Take evidence: bun scripts/spec-census-record.ts --vendor <v> --apply runs every done capability's verify() in a worker whose handler exports are wrapped and records the method+path each verify issues and how the twin answered. An operation a passing verify answers 2xx/3xx is capability by evidence (recordedBy names the ids; an unmapped one is promoted, a claim whose id never issues it is remapped to an API-dimension owner that does). Its ORPHANS are routes the twin serves that the spec does not name — the reverse census, pack→spec drift for the pack's next revision. Runs append to $recordRuns; --reset re-derives under current rules.

  4. Rule what is left, by hand — and AUDIT WHAT THE BOOTSTRAP GUESSED. The sharp instruction first, because it is the one builds keep missing: every capability / operationCapability row with no recordedBy is a GUESS, not evidence. The bootstrap maps by segment match and makes confident false mappings, which the probe reports as AMBIGUOUS rather than wrong, so they survive a clean-looking probe run — one 2026-09-02 build hand-ruled 27 of them back. Read every such row against the handler before you keep it. Then: a todo( id is not a claim — an unbuilt operation stays unmapped (a $ruled marker records a human ruling). mapping records what the twin SERVES, never what the manifest INTENDS; the plan already lives in the manifest, which is where a plan belongs, and a census that counts planned rows as covered inflates its own denominator. So unmapped holds TWO kinds and $unmappedComment must separate them: operations the manifest names NOTHING at all (the sharpest signal a census carries — surface nobody has noticed), and operations it names only with a todo( id. spec-census --check now FAILS a mapped row whose owner is a todo( id, so this is enforced rather than remembered. unmappedBaseline is a RATCHET: the tools lower it and never raise it, so when a spec refresh adds rows the count exceeds the baseline and spec-census --check warns until a human triages them. Raising it by hand is how a coverage regression becomes normal — map the new rows, rule them, or leave the warning standing. A ruling has a machine form: $ruled on the row ({ kind: 'false-claim' | 'planned', was, date, by }) — the record and probe tools write it when they demote a claim, and a row the manifest names only with a todo( id is ruled planned by hand. The checker warns on UNTRIAGED growth, unmapped minus ruled, so a correction stops shouting once it is recorded and a real regression still does.

  5. Ratchet — and FIRST, the assumption that costs a run: your gate.ts replay step paths must live in the CENSUS's path space, i.e. relative to the fixture's servers[0].url, not the real wire form. A replay written as /api/v10/channels/{id} against a census whose paths are unversioned matches nothing, every {param} falls back to a placeholder, and the run reads AMBIGUOUS at scale — ~110 claims in one 2026-09-03 build — which looks like a modelling problem and is not. Check one replay path against one census row before you run it.

    bun scripts/spec-census-probe.ts --vendor <v> (add --apply to write) seeds a root with your gate.ts replay writes, fills each {param} from the replay step with the longest shared prefix, sends the fixture's servers host and base path plus that step's headers, and issues every claim through your fetch adapter. Verdicts: UNSERVED (answered exactly like a route the twin has never heard of, on a resource whose own GET …/{id} answers an unknown id differently — demoted on --apply), UNPROVABLE (same answer, but that resource cannot tell an unknown id from an unknown route: add a replay step that reaches the route, or rule by hand), AMBIGUOUS (a parent lookup answered first — yours to rule), PLANNED (served, but claimed by a todo id — remapped to a done owner). A vendor that rejects a request BEFORE routing when a query parameter is missing (Azure's api-version) makes every answer identical to its baseline: declare requiredQuery: {"<name>": "<value>"} in the census and the probe sends it. Runs append to $probeRuns.

Then bun scripts/spec-census.ts --check. Later specs reconcile through scripts/spec-refresh.ts, which defaults to the census's own committed scope (one loader, YAML or JSON, ?beta=true variants folded). One vendor generation, pinned. Your fixture pins the generation you serve; a request naming another (a version header, a dated path) is refused the way the vendor refuses an unsupported version, never answered in the pinned shape — stripe's Stripe-Version handling is the precedent. Drift is DETECTED on the census campaign's cadence (spec-refresh against a freshly fetched copy; volter-world audit shows the pin's age from MAINTAINERS.md's last verified) and never absorbed: a re-pin is a pack revision — re-dossier, re-capture, §9 — not a version bump. Rules:

  • The file is yours alone — no shared-file splicing. Format: 2-space JSON, trailing newline, non-ASCII as \uXXXX escapes (the ratchet writer emits that style; keep it so machine rewrites stay one-line diffs).
  • The pullAudit slice has two teeth the others don't, both literal-grep: every gaps[] id you file must exist as a literal todo('<id>'…) call in a packages/twin/<vendor>/src/*-capabilities.ts file — the checker greps for the literal string (scripts/pull-audit.ts), so a todo minted through a .map(...)/helper loop is invisible to it and reads as an unfiled gap; if your manifest generates entries programmatically, spell the pull-gap todos out as literal calls anyway. And every pulled[].evidence claim must be a literal substring of the connector file(s) it cites (same checker, connectorText.includes) — quote a real identifier from the connector (the sync/map function name), never a prose description of what it does.
  • The baseline slice is the coverage ratchet: it holds the pack's DONE count, and a new pack must seed it — the number you actually measured (bun scripts/manifest-baseline-one.ts <vendor> prints it), never a guess. It only ratchets upward from there; a later drop is a deliberate hand-edit visible in diff review. Seeding at wiring time is NOT the last touch: ratchet once, only after BOTH §9 rounds and their revert matrix are complete. Review moves the done count more than once (Google OAuth moved 108 → 111 → 113 across its two rounds): a demotion drops it below your seeded floor (red until you hand-lower the number in the same commit), and a review-driven fix raises it, leaving a stale-low floor with no teeth over the delta. Re-measure after round two settles and commit that final post-review number (bun scripts/twin-capabilities.ts --write-baselines ratchets up; demotions are the hand-edit). Do not call the round-one count final.

7. The generated docs block — README.md and ./conformance.md carry coverage tables that bun scripts/twin-capabilities.ts --check-docs verifies are in sync. Regenerate them with --write-docs; never hand-edit inside the generated markers. This is the one sanctioned exception to "don't edit repo docs" — you are editing generated output, not prose.

Docs generation is HERMETIC (gate plane): every row is computed by manifest-baseline-one.ts --row-json in a process that evaluated exactly ONE manifest, and totals derive from the rows — so a regen structurally cannot move another pack's numbers (the old single-process generator once spuriously reported Stripe 171 → 169). For your pack use the SCOPED form, bun scripts/twin-capabilities.ts --write-docs <vendor> (what land.ts --docs runs): it recomputes your row, merges it with the committed rows, and re-derives totals. Inspect git diff -- README.md ./conformance.md ./architecture.md anyway: only your row and the derived totals may move — anything else is a parser/renderer bug to report, never something to commit.

8. Interception — hosts on your pack descriptor (§3). Skip it and zero-edit injection silently breaks: the world runtime can't route your vendor SDK's traffic to the twin. This used to be the one wiring point NO gate enforced; after two production incidents (the openai/posthog/supermemory/xai packs shipped full twins with unreachable hosts), scripts/vendor-hosts.test.ts fails any manifest pack with neither an injector entry nor a stated hostsNone ruling on its descriptor.

Declare hosts on the descriptor. Do NOT edit VENDOR_HOSTS.

hosts: [
  { host: 'api.<vendor>.com' },                                // exact host
  { suffix: '.cognitiveservices.azure.com' },                  // per-resource-host vendor
  { host: 'www.googleapis.com', pathPattern: '^/youtube/' },   // a host two vendors share
],

A per-resource-host vendor (Azure's https://<resource>.cognitiveservices.azure.com, virtual-hosted buckets, *.atlassian.net) needs the suffix form, not an exact host; pathPattern is a RegExp source string applied to the URL pathname, and exists to split a host two vendors share (the youtube/googleauth case).

bun scripts/pack-facts.ts (point 13) compiles these into the committed artifact, and inject.cjs require()s that JSON and builds the VENDOR_HOSTS predicate from it — it must stay dependency-free preloaded CJS, so it consumes compiled output rather than importing packs. That is why the descriptor route satisfies vendor-hosts.test.ts exactly as a hand entry did: the gate requires the real injector table, and the table is now assembled from your declaration.

The hand VENDOR_HOSTS table in inject.cjs is LEGACY-ONLY. It shrinks toward empty as packs migrate, and a vendor present in both homes throws at require time:

inject: vendor "<x>" declares hosts on its pack descriptor AND in the hand VENDOR_HOSTS table — one home per fact; delete the hand entry.

A broken checkout must refuse to inject rather than half-route, so this is a throw and not a warning. A new pack never touches VENDOR_HOSTS. If you are migrating an existing pack, the edit is a move: add hosts to its descriptor and delete its hand entry in the same commit.

A host no descriptor can list? A name a person makes answer to the vendor while the World runs (a custom domain connected to an R2 bucket) is declared as hostsClaimed: { door, note } beside hosts: the twin lists the names it answers now at its own door (GET, answering { "hosts": [...] }), and the injector, in apps and in the redirect proxy alike, reads that door every five seconds and routes each name to the twin as DNS routes it to the vendor. A name any descriptor's hosts covers always answers first, so a claim never takes another pack's host. cloudflare is the example (cloudflare-server.ts, GET /twin/hosts).

No injector entry at all? That is a ruling, not an omission — declare hostsNone: '<why>' instead (hosts XOR hostsNone; declaring both throws in scripts/pack-facts.ts). The descriptor IS the ruling: scripts/vendor-hosts.test.ts and scripts/pack-shape-check.ts read it from generated/pack-facts.json. Its meaning is precise — "no VENDOR_HOSTS entry under THIS pack's own vendor key, for the stated reason" (aws's consolidated area keys are other vendor keys, kept honest by their own gate assertion).

9. The rate-budget rosters — every pack lands in exactly ONE of two files. A pack declaring a budget is picked up by scripts/rate-budget-isolation.test.ts's WIRED roster, which DERIVES from the *-budget.ts glob and needs no edit (it was a hand-listed roster until it was made derived; a build following the old instruction goes looking for a list that is not there) — and mirrors its vendor into ./architecture.md D8's "Wired today" list (the roster is the ground truth; D8 mirrors it). A budget-less pack goes in scripts/rate-budget-coverage.test.ts's EXEMPT roster, which makes silence impossible: every unresolved outbound call site in the pack must be excused by name, with a reason, or the test is RED. And if your declared ceiling admits a bigger burst than the fallback's, the isolation test additionally demands a hand-written VENDOR_BURST_ANCHOR entry (same file): the vendor's own documented per-minute allowance plus its source, with the assertion arithmetic against that number — an out-bursting pack with no anchor is RED by name. The anchor is a hand-written number deliberately: the old declaration-derived check was passed by all three hostile declarations thrown at it, so nothing the declaration itself carries can be the bound.

10. The journey registry — DERIVED, nothing to edit. scripts/ui-journeys.ts's JOURNEY_TEST_FILES globs packages/twin/*/src for journey files, so a journey is registered by EXISTING at the conventional path. What still binds is the census side, when your census.json ui slice declares needsUi: true: the ui-scope census hard-fails a needsUi pack with no journeys, and (the other direction) a journey file no census owns is census drift too.

Know the harness contract before designing the server. runUiJourney navigates first to the exact server.url returned by serve, then waits for #root to contain a child before it invokes your journey callback. It also installs a request guard that allows only that origin (plus data: and about:) and aborts every request or navigation leaving it. A browser-facing protocol twin may have no app shell at /, and an OAuth callback on a second loopback port is therefore unreachable. Follow googleoauth-journey.uitest.ts: make a journey-only same-origin facade that supplies the / mount, serves the real twin handler on its vendor paths, and hosts the integrating app's callback on that same origin. Keep that facade in the journey file so it cannot be mistaken for pack surface, and use the SDK fidelity test to prove the real cross-origin configuration separately.

11. Adoption discovery — adoption on your pack descriptor (§3). This is how volter-world covers / init attribute a repo's signals to your twin. Skip it and covers reports your vendor's SDK as unknown-sdk — or, before the scope-sibling guard, produced no row at all: in the Dub blind-adoption run @upstash/redis and @upstash/ratelimit simply vanished while their scope-sibling @upstash/qstash was mapped, even though the twin existed and was fully wired everywhere else. That class is exactly what let @planetscale/database escape (twin#255).

Declare adoption on the descriptor. Do NOT hand-edit SDK_TWINS / SDK_SCOPE_VENDORS / ENV_STEM_VENDORS / VENDOR_WORLD_IDS. And check the OTHER direction of birth: if your vendor's client or env stem is in the packless-vendor registry (packages/world-runtime/known-external-services.json — "external signal the catalog knows but has no pack for"), delete that entry in the same commit — an entry there dies the moment the pack is born, and scripts/packless-claims.test.ts is RED on any name both packless-listed and pack-claimed (the registry encoding subtracted nine pack-claimed deps at birth, and the gate's first runs killed five dep and four stem stale entries the same day). The gate reads the committed pack-facts artifact and the overlaid maps, so it bites only after bun scripts/pack-facts.ts publishes your descriptor — one more reason point 13's regeneration is not optional.

adoption: {
  sdks: ['<npm-client>'],   // every official npm client of the API surface you model
  scopes: ['@<vendor>/'],   // npm scope prefixes whose members ALL belong to this vendor
  envStems: ['<VENDOR>'],   // credential-env stems — 'STRIPE' for STRIPE_SECRET_KEY et al.
  worldIds: ['<other>'],    // extra world service ids this vendor answers to
},

Each key maps to exactly one legacy table, and packages/world-runtime/src/pack-facts.ts overlays the generated artifact onto them at module init — mutating the same objects every use site already reads, so nothing downstream changes:

descriptor key legacy map file
adoption.sdks SDK_TWINS world-runtime/src/project-inspect.ts (overlaySdkTwins)
adoption.scopes SDK_SCOPE_VENDORS world-runtime/src/covers.ts (overlayCoversMaps)
adoption.envStems ENV_STEM_VENDORS world-runtime/src/covers.ts (overlayCoversMaps)
adoption.worldIds VENDOR_WORLD_IDS world-runtime/src/covers.ts (overlayCoversMaps)

(MAPPED_SCOPES still derives itself.) Dual declaration THROWS at module init, not in a test — loud everywhere, because this drift is what the migration exists to end:

pack-facts: sdk "<x>" is declared BOTH on the <vendor> pack descriptor AND in the central SDK_TWINS table — the migration rule is one home per fact. Delete the central entry (the descriptor wins).

So if the overlay throws while you are wiring, a central entry already claims your name: delete the central entry, keep the descriptor. Note the stem form — the overlay lowercases and the detector normalizes (covers.ts, normalizeId: lowercase, strip non-alphanumerics), so NEXT_PRIVATE_ prefixes and punctuation collapse; write the stem the way stripe does ('NEXTPRIVATESTRIPE').

"NO gate enforces this one" is no longer true — that claim, carried by this section for two revisions, was the reason a whole class escaped. scripts/pack-facts.test.ts now has three teeth: drift (the committed artifact must equal what the descriptors compile to), dual declaration (asserts the overlay's throw is real, not folklore), and construction (a declaring pack's facts must surface through the SAME overlay the runtime uses, proving "drop the directory in and every aggregated view sees it" with zero central edits). If you add a new aggregated view, extend that fixture assertion.

12. World wiring — endpointEnv / endpointEnvNone on your pack descriptor (§3). The other half of interception, and the one the eleven-point list omitted entirely. When the injector cannot rewrite your vendor's traffic — a WebSocket media plane, a raw-TCP protocol, a base-URL-configured SDK, a control plane with no fixed vendor host — the way a world points an unmodified client at the twin is the env var the vendor's own SDK documents. volter-world init injects it pointing at the twin's URL:

endpointEnv: {
  name: 'INNGEST_BASE_URL',
  templates: { INNGEST_EVENT_API_BASE_URL: '${url}' },   // optional extra vars; ${url}/${host}/${port}
  note: 'no injector entry: the inngest SDK is base-URL-configured through INNGEST_BASE_URL / INNGEST_EVENT_API_BASE_URL.',
},

note is required and is the grounding: name the SDK source or doc that documents the var. packages/world-runtime/src/pack-facts.ts's overlayEndpointEnv merges the compiled fact into APP_READ_ENDPOINT_ENV in world-runtime/src/init.ts — the legacy table where these reasons used to live as comments — and dual declaration throws there too, so a migrating pack deletes its APP_READ_ENDPOINT_ENV entry in the same commit (inngest is the exemplar; its old entry is now a one-line tombstone comment in init.ts).

endpointEnv XOR endpointEnvNone; declaring both throws in scripts/pack-facts.ts. And the rule that decides which: never invent a <VENDOR>_BASE_URL the app does not read. A pack that is base-URL-configurable but has no conventional env var (algolia, pinecone, replicate, fal, twilio, sendblue, svix, figma, notion) declares endpointEnvNone with that reason and the proof reports it UNINTERCEPTABLE — which is the truth. Inventing a var would make covers call the world covered (any app-read env counts) while the app, reading no such var, still talks to the real vendor: the exact class of lie the proof exists to catch. Ground-truth caveat: endpointEnvNone is declaration only today — no consumer reads it and no gate demands it, so unlike hostsNone there is no second allowlist to also fill in. Declare it anyway; the ruling is what makes the absence readable, and it is what the migration converges on.

A PULL OBSERVES; IT MAY NOT DESTROY A LOCAL COMMIT. A connector's pull path appends OBSERVATIONS. It must never append a local action that deletes, tombstones or overrides state a caller wrote — the vendor is authoritative about the vendor, not about the world's own intent. A 2026-09-03 lane wrote a collision guard that auto-resolved by tombstoning the local row: during a READ-ONLY sync it orphaned entitlement and price rows keyed by the plan's UUID, silently revoked a granted entitlement, and left an action no push could converge. It passed typecheck, the suites, the invariants and the mutation gate; only an adversarial reader saw the data loss. The right answer to a collision is a LOUD REFUSAL.

The one shape that is legitimate, so you can tell them apart: reviving a subject the world has tombstoned but the vendor still returns (tinybird, tremendous). That honours the vendor without destroying anything, and both packs scope it to tombstoned subjects and file the unhandled half — deletes the vendor no longer returns — as a named todo rather than papering it.

ADDING A RESOURCE TYPE OBLIGES YOUR REPLAY. R2 requires that after the gate's replay runs, the world holds at least one subject of EVERY type your descriptor's resources declares — the declaration is a checked claim, not a wish. So a new resources entry silently reddens the invariant matrix unless the same commit teaches gate.ts's replay to create one. Two wiring points, one edit; nothing else connects them for you.

13. The generated pack-facts artifact — packages/world-core/generated/pack-facts.json. Points 8, 11 and 12 are inert until compiled. AND THE ORDERING BITES: scripts/invariants.ts --check reads resources from the COMPILED artifact, not from your descriptor, so between editing the descriptor and regenerating you get an R2 red that describes the old declaration and reads exactly like a real failure. Regenerate first, then believe the matrix. The hosts case has its own disguise: new routes ARE served, but the injector refuses them because it reads the compiled table, so the failure surfaces as an unrelated-looking SDK error rather than as anything about hosts. After any descriptor edit:

bun scripts/pack-facts.ts        # rewrites the artifact
git add packages/world-core/generated/pack-facts.json

14. The support-status row — policy/MAINTAINERS.md. scripts/maintainers.test.ts holds it in bijection with the catalog, so a pack with no row is RED and a pack is born with a status (Supported / Maintained / Odd Fixes / Orphan / Obsolete, plus owner, upstream, spec source, last verified). AGENTS.md has always said so; this section did not, and the 2026-09-02 expo build found it the hard way.

15. The host worker map — gone with the v1 shell. The host mounts whatever packs a world's config names; there is no roster to regenerate.

16 and 17. The two generated reports — bun scripts/scorecard.ts and bun scripts/ecosystem.ts. Both are drift-gated (scripts/scorecard.test.ts, scripts/ecosystem.test.ts) and both go RED on a new pack. Re-render and commit them. These are the two most commonly missed, because nothing about building a pack suggests a catalog-wide report needs rewriting.

19. The source-text roster — SDL-EXECUTING PACKS ONLY — scripts/text-modules.ts's TEXT_MODULES, plus its hand-pinned roster assertion in scripts/text-modules.test.ts. A pack that executes a vendored schema (buildSchema) renders it into a committed *.gen.ts and imports that; reading the text at runtime instead is an R12b violation the invariants gate catches. Add both entries in the commit that adds the module. Conditional: it binds iff your serve path executes or serves a source text.

A GraphQL trap worth the paragraph, if you execute an SDL: graphql-js COERCES a field's declared argument defaults into args before your resolver sees them, so an argument you never sent arrives populated. A guard written as "refuse any non-empty sorting" therefore refuses every list query the schema gives a default sort — measured at 33 verifies on one build. Compare against the field's DECLARED DEFAULT, not against emptiness.

Declare the implemented protocol. PROTOCOL_VERSION is 2.0; current packs declare protocol: '2' and pass the protocol-2, branch round-trip and applicable shape-parity gates. An undeclared pack is treated as protocol 1 and deprecated, not silently upgraded. Consult protocolStanding in packages/world-core/src/packRegistry.ts and the generated index for standing. Changing a descriptor alone does not migrate an implementation.

Pin the package. init emits the installed version; up checks that the mounted package satisfies it and records resolved version and protocol. volter-world outdated and audit inspect those records.

WHICH PACK TO DEEPEN is its own question, and bun scripts/deepen.ts answers it. The ladder decides which twin should EXIST; the packs that already do and are shallow carry todo · core entries, the author's own judgement that an operation is central and unbuilt. Ranking those by pile size is measurably the wrong signal: the biggest piles have been packs no measured repo demands, while a pack with a third the pile had eight repos wanting it. deepen.ts crosses what a pack says is missing against what the roster says is wanted, and writes nothing.

An undemanded todo · core is parked, never deleted and never built ahead of demand. It is a true sentence about the vendor, and demand is not a fact about the vendor — deleting it to shrink the tail would make the census lie. When deepen.ts reports an EMPTY demand head, the depth half is done until the ladder is re-censused and demand moves; until then the campaign is breadth, the ladder's build queue of uncovered vendors.

18. The coverage ladder's derived fields — bun scripts/ladder.ts --rederive. generated/ladder.json's build queue and new-vendor queue are derived against the CATALOG, so a pack landing makes the committed report stale and scripts/ladder.test.ts red. --rederive recomputes those fields from the committed measurements and needs no repo corpus — the measurements did not change, the catalog did. Do NOT run a full bun scripts/ladder.ts to fix this: without the corpus it measures nothing, and the point of --rederive is that a merge should never need one.

This point was found on 2026-09-02 by the same merge that rewrote this section — which is the argument for the framing above. The list is not the definition of the set; verify-pack.ts and the drift gates are. When one of them goes red on something not written here, write it here.

Never hand-edit it (the file says so in its own "//" key). It is generated rather than scanned at run time because its consumers either must stay dependency-free preloaded CJS (inject.cjs) or are latency-sensitive CLIs (volter-world init p50 is ~450ms across a 100-repo census; importing 70+ pack modules per invocation would dwarf that). The scan happens once, at authoring time.

A stale artifact is RED in the gate sweep — bun scripts/pack-facts.ts --check fails, and scripts/pack-facts.test.ts's drift test fails with it. The generator also refuses two authoring mistakes outright, before any drift check: a descriptor whose vendor disagrees with its directory name ("the directory IS the identity; rename one"), and either XOR pair declared on both sides.

What genuinely does auto-discover: capability-manifests.test.ts and architecture.test.ts both scan packages/twin/* and enforce shape/dep-hygiene without registration. And twin-check.sh PACKAGES does NOT need editing — new packs are gated via the architecture guardrail and the per-process meta-test, not that array. (Add it only if you want your unit tests in the headline [1/4] loop too.)

7.5 Integrating parallel pack branches

When several builder branches land together (ten at once in PR #220), virtually every textual conflict sits in the §7 wiring points — each branch appended its own entry to the same registries, rosters and censuses. The resolution is almost always the UNION: keep BOTH sides' entries, in the order each file's own rules dictate (§7 point 2's PACKS ordering; the censuses' insert-additively rule). Four hazards recur:

  • The dropped-closer signature. The classic conflict has both sides ending INSIDE their own allowlist/roster block and sharing the ONE trailing closer that sits below the conflict: ours adds entries and stops mid-block, theirs does the same, and the ]);/}; appears once, after >>>>>>>. Stripping the markers yields one run-on block missing a closer. The fix is mechanical: replace the ======= marker line with the closer, so ours' block is closed and theirs' keeps the original trailing one.

  • Shared JSON registries need ENTRY-LEVEL splicing, never marker-stripping (your pack's own census.json never conflicts — it is yours alone; this rule survives for any remaining shared JSON): deleting conflict markers mangles the neighboring entries' commas and braces. Take one side wholesale, then splice the other side's entries in by hand — additively, matching neighbors, never a parse → re-stringify of the file.

  • The forked-invariant class — un-catchable by any parse check. A repo-wide invariant that landed on main AFTER your branch forked can be invalidated by facts your branch introduces (and vice versa) with NO textual conflict at all: the files merge cleanly; the CLAIM is stale. Both post-merge reds in the ten-pack integration were this (commit 7f3c567): a covers-test fixture pinning "a vendor-shaped env var with no pack stays a loud unknown" chose UPSTASH_VECTOR_REST_URL as its control, which stopped being pack-less the moment the upstashvector branch landed; and a budget test's raw-host scan flagged a fixture-builder file that landed on main after the youtube branch forked, so its allowlist had never seen it. After the union: re-run the census suites AND actually READ every new-on-main test that touches your files — git log --stat <fork-point>..main -- scripts/ packages/world-runtime/ names them. A red here usually means repointing the invariant's fixture at a control that still holds the pinned property — not weakening the invariant.

  • Parse-gate the resolution with Bun.Transpiler, not bun build. bun build reports module-resolution failures alongside — and often instead of — syntax errors, so a leftover conflict marker can hide behind an unrelated import complaint. A per-file transpile is an unambiguous parse verdict:

    git diff --name-only <fork-point>..HEAD -- '*.ts' '*.tsx' | bun -e '
      const t = new Bun.Transpiler({ loader: "tsx" });
      for await (const f of console) {
        if (!f.trim()) continue;
        try { t.transformSync(await Bun.file(f.trim()).text()); }
        catch (e) { console.error(f, String(e)); process.exitCode = 1; }
      }'
    

A merged tree is exactly the "finished whole" the full gate exists for — the union of N green branches is not green by construction, and the forked-invariant class above is precisely what it catches. So an integration of several pack branches is the standard case for asking for the full sweep (§8), rather than merging on the per-merge slices alone.

8. Verify

Run this, first and often:

bun scripts/verify-pack.ts <vendor>     # T0 — the whole per-merge set, in seconds

(One of the steps it runs, scripts/pack-shape-check.ts, takes its pack positionally — bun scripts/pack-shape-check.ts <pack>, not --vendor <pack> — which is worth knowing on the day you need to run it alone.)

That single command is what AGENTS.md means by per-merge verification: the pack's suites, tsc, the manifest baseline in its own process, the mutation phase, the census self-checks, sibling shape, and — since 2026-09-03 — the two generated catalog reports. That last pair was a real hole: a pack change that moved a capability count or ratcheted a census baseline left generated/scorecard.json stale, so scripts/scorecard.test.ts went RED while T0 read GREEN and the lane reported a clean merge. It is also the fastest way to find an unwired §7 point. Since 2026-09-04 it also runs the CATALOG step: the catalog-wide gates a pack change can break (the vendor-pack architecture guardrails, the derived tables, the ladder, packless claims, rate budgets, the journey set, the UI conformance baseline), so a locally green lane can no longer leave a catalog red that only T2 would have named. Two stay T2's for cost: the capability-manifests meta-test and generated-docs drift — a manifest change regenerates the docs with bun scripts/twin-capabilities.ts --write-docs and commits them. The individual commands below are what it runs, kept here for when you need one in isolation.

The behavioural journey lane (five packs, by design). T0 asks whether each operation is mapped and answers identically twice; it does not ask whether the twin is RIGHT over a long arc. That question is a JOURNEY — packages/twin/<vendor>/journey.ts, one RL-task-length story (30+ steps) the runner walks with its beats (ECHO, PERSISTS, ISOLATED/UNKNOWN, REPLAYABLE, LANDED); the set is capped at five packs, the ones whose state models the rest borrow, and every walk runs in the scripts sweep. A derived pack is not under the cap: its journeys are authored and judged per pack. If your pack is one of them:

bun scripts/behavior-journey.ts --vendor <vendor> --verbose     # the walk, step by step
bun scripts/behavior-journey.ts --vendor <vendor> --coverage    # what the story has never done

--coverage is line and function coverage of the pack's STATE-SIMULATION MODULE while the story runs — the sources under src/ that write world state, minus pull/push — with the never-run ranges grouped under the declaration that encloses them. Read it as "the story never took the reopen branch of applyIssueWrite", never as a score to raise; a line that ran was reached, not proven right (scripts/journey-kit.ts, COVERAGE).

What you run per merge, and what you do NOT. The full twin-check.sh gate is a whole-repo sweep, not per-change verification — running it off the bat for every pack is a massive slowdown and is not the cadence. Per-merge verification is: your pack's own suites + typecheck + the specific meta-gates your change affects + §9's adversarial review, which is mandatory and is the only thing that catches a false-green. The full gate runs only on the owner's explicit ask; until then a pack claim queues for that run and says so.

# fresh worktree/checkout? `bun install` at the REPO ROOT first — missing devDeps fail the
# SDK-integration tests AND corrupt seemingly unrelated suites (an unarmed budget declaration
# silently falls back to kernel defaults).
cd packages/twin/<vendor> && bun install && bunx tsc --noEmit && bun test src/*.test.ts
# `git add` the new pack BEFORE trusting its own guard tests — see below.
# then from the repo root — the fast inner loop:
bun test scripts/architecture.test.ts          # pack shape + dep hygiene
bun test scripts/capability-manifests.test.ts  # baseline + discovery + wiring (per-process)
bun scripts/manifest-baseline-one.ts <vendor>  # YOUR pack alone: regressions=0, done>0, total>=50
                                               # (answers "unknown vendor" until §7 point 1 —
                                               #  the manifest-registry import — has landed)
bun test ./scripts/mutation-test.ts            # your verifies must FAIL against a dead twin
bun test scripts/protocol-2.test.ts            # the pack is a plugin: descriptor, serve path, module truth, engine slot
bun test scripts/branch-round-trip.test.ts     # the branch round-trip, every protocol-2 pack (positions, never tree bytes)
bun test scripts/shape-parity.test.ts          # write handler ⇄ refresh adapter (advisory until shapeParity: 'held')
# the meta-gates a new pack's wiring actually touches (§7) — run the ones your change affects:
bun scripts/pack-facts.ts --check              # point 13: descriptor↔artifact drift
bun test scripts/pack-facts.test.ts            # drift + dual declaration + construction
bun test scripts/vendor-hosts.test.ts          # point 8: an injector entry or a stated reason
bun test scripts/rate-budget-isolation.test.ts scripts/rate-budget-coverage.test.ts   # point 9
bun scripts/spec-census.ts --check && bun scripts/ui-scope.ts --check                 # points 3, 4
bun scripts/pull-audit.ts --check                                                     # point 5
bun scripts/ui-journeys.ts                                                            # point 10
bun scripts/twin-capabilities.ts --check-only --check-docs                            # points 6, 7

Then §9. Then, when the owner asks for it, the full sweep — serialize it even on a quiet machine; the default fan-out has produced unrelated 60s timeout flakes under load. Each run prints its own log dir + provenance (worktree + HEAD sha); read THAT dir, never an older run's:

TWIN_CHECK_CONCURRENCY=1 bash scripts/twin-check.sh --check

The ./ in the mutation line is load-bearing: mutation-test.ts doesn't match bun's *.test.* discovery pattern, so a bare bun test scripts/mutation-test.ts is treated as a name filter that matches nothing — bun prints "filters did not match any test files" and runs zero tests, which a scrolling terminal reads as a pass. The explicit ./ path makes bun execute the file directly. (The two *.test.ts lines above work either way — they match discovery.)

A WORKTREE'S bun test scripts/ IS NOT MAIN'S. A lane runs in a git worktree with no node_modules of its own, so @volter/world-core resolves to a second module instance and a handful of tests that depend on cross-module identity fail there and only there — rate-budget-isolation's RateBudgetError constructor check is the usual one. THREE separate lanes on 2026-09-03 reported those as "pre-existing failures on main" while main was green at 1,600 tests. Before you attribute a red to the base, re-run it in the main checkout or bun install in your worktree; and when you report one, say WHERE you ran it. A wrong claim about the baseline is worse than the failure, because it teaches the next reader that the sweep is noise.

git add the new pack BEFORE trusting its own guard tests. This is stated below in terms of the budget tests, but it is a GENERAL rule about every gate that shells out to git: any test using git ls-files / git grep (trackedPackFiles(), filesMentioning(), the raw-host allowlist checks) sees only tracked files, so on an untracked pack it reports green while checking nothing. If a guard test passes on a brand-new pack, ask whether it could have failed. Stage the pack first; only then does its green mean anything.

twin-check.sh runs materially more than the inner loop: every pack's unit tests and tsc, the mutation gate (API/write, mirror-render, connector, plus named isolated seam groups), the committed censuses and rosters (§7), the capability-baseline ratchet, and a generated-docs drift check. That breadth is exactly why it is not a per-change tool: it re-measures 70+ packs to tell you about one. Run the affected slices above per merge; leave the sweep for the asked-for run.

When the full gate IS run, it is run ONCE, at the end, over everything — never twice per pack. The earlier framing here (a pre-review sign-off run, then a repeat after §9) is superseded: build the full slate, land the per-merge verification above, complete §9's rounds and the revert matrix, ratchet the baseline, and let the ONE comprehensive sweep cover the lot. A gate run is not how you verify an intermediate change; it is how a finished whole is accepted.

Freeze the tree for whatever gate run does happen. Do not edit while it runs. A result over a moving worktree cannot identify what passed and is void, even if its last line is green. Commit first, require a clean git status, record the exact HEAD printed in the provenance line, and make no edits until the process exits. If anything changes — even documentation — stop/restart the gate from zero on the new commit.

Run the gate ALONE. Each run writes its logs to its OWN per-invocation directory, printed at start next to a provenance line (worktree path + HEAD sha + mode — match it to your tree before trusting any log; TWIN_CHECK_LOG_DIR overrides the location), and the script prints a loud WARNING naming the PIDs when a sibling twin-check.sh is already running. So concurrent gates can no longer clobber each other's logs — which used to manufacture spurious timeout failures in packs nobody touched — but they still share the machine's sockets and cores, and the WARN is not a refusal. The gate also flakes under its own default concurrency (CHECK_CONCURRENCY, 4) on machine-local noise — measured during the tinybird build: 2 of 5 default runs went red, on a different unrelated pack each time, and every one was green both isolated and under TWIN_CHECK_CONCURRENCY=1 bash scripts/twin-check.sh. Either way a red in an unrelated pack is measurement noise, not a finding: reproduce the failing pack solo before believing it. Use TWIN_CHECK_CONCURRENCY=1 proactively for the build-sign-off run; default concurrency remains a faster development probe, not the trustworthy final measurement (the mechanism — listening-socket pressure handing a probe a stranger's response — is written up in ./gates.md, "The gate goes red on a different pack each run"). (capability-manifests.test.ts now names this explicitly — a killed baseline runner reports the signal and the standalone reproduce command rather than a bare "exited null" that reads like a regression.)

A timeout is a PERFORMANCE bug. Never raise it to go green.

Three separate red gates in one 2026-07-25 session were all the same thing: a per-test timeout tripping on a test that was slow, not hung. The reflex — raise the number — is wrong, and each time it would have buried the real defect:

  • baseline runners were being SIGKILLed by bun's 5s default (they take 1-4s);
  • twin-check's own 10s cap fought its CHECK_CONCURRENCY=4;
  • UI journeys tripped a 30s cap — and chasing that one found the actual bug: journeys launched two chromiums per test (one thrown away by the availability probe), so ten journeys meant ~20 launches. Sharing one browser with a fresh context per journey took the whole suite to under 2s per file. The 30s cap had been hiding a 15× waste for as long as it existed.

So when a test trips its budget, the question is "why is this slow?" — never "what number makes it pass?" A timeout you raise is a performance regression you agreed to stop measuring. Budgets in this repo are deliberately tight and say so in their own error messages (JOURNEY_TIMEOUT_MS is 15s against a ~1-2s real run, and warns at half).

In-process server harnesses must keep the event loop alive and contain handler errors.

When a fidelity test starts the pack's server (through serveHttp) in the same process as a real client CLI, spawn that client asynchronously (Bun.spawn + await proc.exited), never with spawnSync. A synchronous child blocks the same event loop the server needs to answer, so client and server wait on each other forever. github-git.integration.test.ts is the copyable pattern for an unmodified CLI driving an in-process HTTP server.

Also make the server adapter return a defensive vendor-shaped or generic HTTP 500 when its handler fails or yields no response; do not let an exception escape the seam's fetch callback. Under bun test, a thrown server-handler error can be reported as “unhandled between tests” and then prevent later test() registration in a top-level-await harness, obscuring the original defect. Webhook delivery is fire-and-forget for vendors whose own request succeeds independently of delivery: catch delivery failures at that boundary and ledger/report them separately, as github-server.ts does. A dead webhook endpoint must not turn the originating vendor write into an HTTP failure unless the real vendor's protocol says it does.

The UI rung is certified by a REVIEW AGENT, not by automation.

Every other rung can be decided by an assertion. The UI rung cannot, and pretending otherwise is what produced this repo's worst false-green.

An assertion can prove seeded data reached the screen. It cannot decide whether the screen is any good — whether it reads as the product, whether the layout survived, whether a "rendered" row is a legible card or a two-pixel sliver of unstyled text. A mirror can satisfy every locator in a journey and still be unusable. That judgement needs eyes, so it belongs to the reviewing agent:

Journeys record themselves. The step vocabulary lives in the harness — visible, gone, atPath, waitForMount, clickByName, imported from @volter/world-tooling — and every step writes a frame, because every step a journey takes is by definition a moment worth seeing. Use those helpers and your journey produces its own filmstrip; define your own local copies (all four packs used to) and it records nothing. Frames land in .ui-certification/ (gitignored) with no flag to remember.

bun scripts/ui-certify.ts          # runs every journey, groups each one's filmstrip
# then READ the frames and judge them. If you did not look, you did not certify.

A filmstrip, not one shot at the end: a journey that goes board → detail → deep-link asserts three different screens, and a single final frame hides the two the reviewer most needs to see.

ui-certify.ts is deliberately not wired into twin-check.sh. Automation's job here is to produce the evidence; grading it is the reviewer's. A checkmark asserting "the UI is fine" that no one looked behind is exactly the fake-success the twins bar forbids — and this rung has already been bitten by it: for months every journey silently skipped and reported PASS on any machine whose playwright revision didn't match its installed chromium, so the top conformance rung certified itself green while launching no browser at all (2026-07-25).

What that means in practice: UI navigability is signed off in §9's adversarial review, by a reviewer who opened the frames. There is no status check standing behind that — review here is local, so nothing but the review itself is between a green gate and an unusable mirror. A reviewer who records a verdict without having opened the screenshots has certified nothing — and the journeys passing is evidence, not a verdict. Ask of each shot: does it read as the real product; is the seeded data legible on screen rather than merely present in the DOM; is anything collapsed, overlapping, or invisible.

The journeys themselves are a liveness check — they catch a mirror that is broken, never one that is merely bad. Only a reviewer catches the second, and only by looking.

A gate must never grade itself green on work it skipped.

The same investigation found the UI-navigability rung — the repo's top conformance rung — had been certifying itself green while running nothing: browserAvailable() returned false because playwright's expected chromium revision wasn't installed, and both the per-test guard and scripts/ui-journeys.ts responded by returning success. The suite printed passes having never launched a browser.

Applies to any capability that can be environmentally unavailable: absent capability ⇒ FAIL with the fix command, never a silent pass. If an environment genuinely can't run it, require a deliberate, loud opt-out (TWIN_ALLOW_SKIP_UI_JOURNEYS=1) that states the rung was not verified — so a reader can never mistake that run for a verified one. This is the same never-a-fake-success rule the twins hold vendors to, turned on the gate itself.

9. Adversarial review (MANDATORY before merge — not optional)

And this section is NOT birth-only. It reads as part of building a pack from zero, and it is worth as much or more on DEPTH work against an existing one: the 2026-09-03 lane that took stigg and elevenlabs from 174 to 208 done had 23 confirmed breaks found by two skeptics, several of them data loss or false greens in code it had just written, including the pull-path tombstoning above and an id mint that re-issued a deleted id. New capabilities in an old pack get the same two rounds and the same revert matrix as a new pack does.

Tell every skeptic WHICH VENDOR VERSION is authoritative. A reviewer judging vendor fidelity reads whatever SDK happens to be installed, and that is not always the one the consumer pins: a 2026-09-02 round-one blocker called a route invented because it had been dropped in a later SDK major, while the major our own consumer pins still ships it. Deleting it, as that review advised, would have broken the twin for the very app it exists for. Name the authoritative version and the consumer's pin in the skeptic's brief, and treat "this does not exist" as a claim that needs the version stated.

The gate proves the twin isn't broken; it does NOT prove it isn't false-green. Before merging, you (the building agent) spawn an independent read-only skeptic as a sub-agent — a fresh Explore agent context with a refute mandate (this is NOT your own self-assessment; a fresh context catches what you're blind to). It is read-only and is told to default to "done" being false and try to REFUTE every done by reading the code. It must NOT edit files or run mutating git — and tell it explicitly NOT to run the gate: its mandate is refutation by READING, and a skeptic-launched gate run only adds the sibling machine load §8 warns about while proving nothing a read would not. Freeze the tree before spawning: commit, and point every skeptic at that commit — findings against files you are still editing describe code that no longer exists, and the verdict you record must name the sha it judged. Have it hunt:

  • verify() that passes on an empty/dirty workspace (not failable), or asserts only a status code;
  • success asserted on a failure path; the vendor's negative 4xx missing;
  • ?? [] / || {} masking; a verify that would still pass if the handler body were return {};
  • hardcoded UI / marker-only verify on a data screen (claims behavior it can't prove);
  • semantic infidelity (wrong status/state-machine/shape vs the real vendor);
  • subject-id collisions that make a create return the wrong resource (pack-internal id minting can still collide within a type — the kernel resolves by (type, id), §5 note). Hunt BOTH directions by name: "create after connector pull" (a local mint lands on a pulled vendor id) AND "pull after local create" (a pulled id lands on a minted one) — a namespaced local id base (datadog's EVENT_ID_BASE) is the clean fix;
  • fixtures shaped like nothing the real SDK ever emits — a verify or connector test that is green over an impossible input has proven fidelity against a client that doesn't exist, and hides the bug the SDK's actual shape would hit;
  • omitted interfaces — start from the motivating app's actual clients and vendor documentation, then compare them with the descriptor, manifest and census. Refute completeness by looking for surfaces absent from every list, including protocols other than the one already implemented;
  • denominator padding — newly-added todos that are mis-tiered or fabricated/non-existent surface.

Two preconditions the 2026-09-16 six-pack batch added, each from a skeptic that passed a defect it could not have seen. First, the skeptic's model is set by the orchestrator, outside the builder's environment — a builder spawns its skeptic with the model its own environment maps, so a builder on a weak model gets a weak skeptic: a fresh context on the same model is a self-assessment with new eyes, not an independent review. In that batch the builders' own two rounds passed a protocol-2 index.ts that never called registerPack, four fine-tune endpoints answering 200 with nothing stored, and a conformance module still at its scaffold seed; a stronger model reading the same frozen sha confirmed eleven defects the first day. Second, a skeptic reviews only a pack scripts/pack-hygiene.ts has passed (it runs inside T0, §8): a raw NUL byte in a 1,246-line handler made grep treat the file as binary, and every text audit of it — both skeptic rounds included — returned nothing and read as clean. Refutation by reading cannot see a file it cannot read.

Demote or fix anything it can't confirm. This is the same bar a completeness sweep over an existing pack clears — a new pack is not exempt from it. Only merge once both independent rounds find no unresolved false-greens. Then complete the revert matrix and ratchet the census.json baseline slice once (§7 point 6): demotions and review-driven fixes from either round moved your done count off the number you seeded.

Expect ~10-15 minutes on a 100+ capability pack — the skeptic has not stalled. Let it finish. And run TWO rounds, with DIFFERENT attack surfaces — round two is not a re-run of round one: round one refutes the original dones; round two explicitly attacks round one's fixes — hollow pins (a fixture that cannot actually fail against the bug it claims to pin), assertions satisfied by unrelated markup, a rewritten check whose teeth stop at the dispatch — their COMBINATIONS (two round-one fixes interacting on the same state), and re-hunts round one's finding CLASSES in the files round one never opened — a class found once is a class the whole pack plausibly has. tinybird's conformance check needed exactly this second pass: round one caught two constants asserting about each other; round two caught the replacement grading only the router's own miss, leaving seven handler branches deletable (§6, conformance teeth). Budget round two as a full review — it historically finds as much as round one. A fix authored under review pressure deserves the same skepticism as the original done.

Round two's sharpest mandate: attack each fix's NEW REACHABILITY. A fix does not merely repair — it changes what executes, and the worst round-two findings have been defects the fixes introduced: groq's round-one "skip unpushable actions instead of aborting" made the rest of the push sweep reachable, so a local delete fired DELETE .../files/file_twin_1 — the twin's own minted id — at the REAL account; mistral's round-one fix left conversation_entry.delete pushable to an endpoint that exists nowhere. (The defect need not be NEW code — groq's id-mapping bug and even a pin blessing it existed at build time; the round-one fix merely made them RUNTIME-REACHABLE, which is the point: reachability, not authorship, is what round two must trace.) For every round-one fix, trace what it made newly reachable BEFORE walking the matrix below. And build the matrix per fix, then per capability that NAMES the fix: the cohere build's round two left four green cells that were not hollow pins but UNPINNED FIXES — a fix no capability names walks the matrix invisible (2026-08-31) — when no existing capability claims a fix, ADD one; a cell is hollow BY CONSTRUCTION otherwise (deepseek round one landed a fix that way).

The skeptic's target is a NAMED, FROZEN SHA — and the matrix is re-walked at the final head. Hand each review round one commit hash and do not advance it while that skeptic reads; fixes land ON TOP and the next round gets its own frozen SHA (this resolves the apparent tension with committing the green candidate first: commit, freeze, review, fix forward). A review of a moving tree makes the skeptic cover for the process — mailgun's lanes said so themselves (2026-08-31). And cells walked incrementally against intermediate heads prove nothing about the final state: after the LAST fix lands, re-walk EVERY cell at that head — mailgun's two late hollow pins hid exactly in the gap between an early walk and the final tree. Two refinements from practice (perplexity, 2026-08-31): a fix found MID-review lands as an ADDITIVE commit on top of the frozen SHA and is DISCLOSED to the running skeptic (never rebased under it); and a RETRACTION (removing a claim) is structurally unpinnable — record its cell as hollow-by-design with the retraction named, the same convention as dead-code cells.

Run the revert matrix — MANDATORY, and the single highest-yield technique across every pack build that has used it. It catches what BOTH skeptics miss. It is mechanized: bun scripts/capability-mutation-sweep.ts <vendor> . rewrites one line of the pack at a time (every if (…) guard to if (false), every object-literal property to undefined), re-runs every done verify in a child per mutation, and prints the done capabilities NO localized mutation reddened — the hollow set — exit 1 if it is not empty; every file is restored byte-identical (SHA-256-checked) after each mutation, so run it only on a checkout nobody else is writing to. Its first run on a shipped Supported pack (openrouter, 2026-09-16) found three hollow dones T0 had been passing. Read each hollow by hand before demoting: a cell whose behavior is the ABSENCE of a route (an unmodeled 404 fallthrough) has no line to neuter and reads hollow by method, not by fact. The hand-walked matrix below is still the bar for the cells the sweep cannot reach. That is not a figure of speech: on the 2026-09-02 bitly and stigg builds the matrix found a hollow pin that two independent adversarial rounds had each read past — a fix that was real, and evidence for it that was not. A skeptic reads for a claim that is wrong; the matrix asks whether anything would notice if it were. Budget for it as its own pass, not as a formality after the reviews.** For EVERY fix the review produced: re-introduce that fix's bug in a scratch copy of the tree — the MINIMAL revert, not a blunt sabotage — and minimal means NARROW enough that its own cell is the signal: a one-constant flip that reddens 26 cells is technically red-by-name and tells you almost nothing about the fix under test (deepseek, 2026-08-31) — and run the pack's verifies; the capability that CLAIMS the fix must go red by name. A pin whose cell stays green is hollow; a cell that goes red must also be read for WHICH assertion fired, because the second hollow-pin shape is a pin with no sensitivity to the bug — the re-introduced defect (or a saboteur used in its place) is too blunt to reach the NEW assertion, so something upstream reddens the verify and the matrix cell reads red for the wrong reason. And the COMMONEST cause of a green cell is neither of those: it is a FIXTURE THAT CANNOT REACH THE DEFECT AT ALL — ids sitting below the mint base, an A→B→C sequence where the bug needs a revert, a single-batch fixture for a cross-batch bug. Before calling a pin hollow, ask "does this fixture make the defect reachable?" (two of hubspot's three green cells and both of cohere's were this, 2026-08-31). Record the matrix (fix → capability id → reddened by name / hollow) alongside the skeptic verdicts below.

After EVERY review round, re-read the pack's own README against the round's outcome. The generated docs gate (--check-docs) covers only the ROOT README/CONFORMANCE tables — a pack README is checked by nobody, and hubspot's round-two blocker was round one's retracted gap still shipping in the pack README. Coverage prose and honesty notes in packages/twin/<vendor>/README.md move in the same commit as the fix they narrate.

The mechanics are load-bearing:

  1. Commit the green candidate first and require a clean worktree. Every cell starts from that exact commit. git checkout -- <file> restores from HEAD and will silently wipe uncommitted sibling fixes, so never use it as an informal backup.

  2. Put the scratch clone under this checkout's node_modules/.<vendor>-revert/, which is gitignored and excluded from every gate scan. A copy under /tmp cannot resolve workspace packages such as React and @volter/world-core; all cells then report RUN FAILED and the matrix misleadingly reads 0/N hollow. The nested location can resolve the already-installed parent node_modules:

    VENDOR=<vendor>
    REVERT_SCRATCH="node_modules/.${VENDOR}-revert"
    REVERT_BASE="$(git rev-parse HEAD)"
    git clone --shared --no-hardlinks . "$REVERT_SCRATCH"
    git -C "$REVERT_SCRATCH" checkout --detach "$REVERT_BASE"
    bun --cwd "$REVERT_SCRATCH" scripts/manifest-baseline-one.ts "$VENDOR"  # green control
    
  3. In that scratch clone only, reintroduce one minimal defect and run the same one-pack command. Read the named regression and the assertion that fired. Restore the touched file with git -C "$REVERT_SCRATCH" checkout -- <path> before the next cell, then rerun the green control. Never edit or restore the primary worktree while walking the matrix.

Run isolated-seam green controls before any sabotage as described in §7 point 2. A control after mock/restore cycles measures a perturbed module graph and cannot establish that the kill-list was green before its seam died.

This step is NOT optional, and the gate cannot replace it. The gate (tsc + tests + architecture

  • manifest baseline) proves the twin isn't broken — it is structurally blind to false-greens: it cannot tell a data-coupled UI verify from a marker-only one, semantic infidelity from fidelity, or a thin manifest from the real surface. §9 is the only thing between a green gate and a false-green twin. So: record the skeptic's verdict — paste its per-done findings into your commit message / handoff report. An un-recorded review is treated as not done; a reviewer (or the next maintainer) must be able to see that an independent skeptic actually ran and what it found. (In practice this caught a real slip — an OpenRouter build whose gate was green shipped two marker-only UI verifies that only the §9 review flagged.)

Measure in isolation, or you'll cry false-green. Run a pack's verifies via bun scripts/manifest-baseline-one.ts <vendor> (its own process) — that's the canonical gate. Running many packs' verifies in ONE process exhausts the Bun.build per-process ceiling and makes the later pack's UI verifies fail spuriously — that's a measurement artifact, NOT a false-green (the mirrorBundle is memoized, so a pack on its own does exactly one build and passes). A reviewer who runs everything in one process will wrongly report the UI caps as broken.

10. Acceptance checklist (from ./conformance.md)

  • Manifest is the real vendor surface (total >= 50), tiered honestly, every done has a passing failable verify(), 0 regressions.
  • A fidelity test drives the twin unmodified — the real vendor SDK, or real-transport fetch for REST/XML vendors with no canonical SDK. It may live in a dedicated <vendor>-sdk.integration.test.ts OR inside <vendor>-twin.test.ts (location varies across packs) — what matters is it EXISTS and proves an unmodified client speaks to the twin. "Unmodified" means un-patched, not un-configured: when the SDK's default API version has moved past the one you model (e.g. @notionhq/client v5 defaults to Notion-Version: 2025-09-03), pinning the version through the SDK's own public option is legitimate — it is what a real integrator pinning a version does. So is pointing the SDK at a plain-HTTP endpoint through its own public config (allowInsecureConnection and friends on the Azure/Google SDKs) — configuration, not modification. Record the pinned version in the README ## Coverage. What's forbidden is monkey-patching the client, hand-rolling requests to dodge it, or silently serving a different version than you claim.
  • The human-surface question was settled with "Does this vendor get a mirror?" (NOT by "does a UI exist"). If it mirrors: real twin state (data-coupled), API↔UI parity via the shared applyXxxWrite. If the API is the product: no mirror and no UI capabilities, with the reason stated in the README ## Coverage + the manifest. If vendor UI is protocol: one state-coupled renderer at the vendor route, with UI capabilities/journeys but no fabricated second dashboard.
  • Connector pull over an injected client, idempotent — required when the vendor has readable state. For a write-only protocol, pull only a genuine observable such as its banner/ capability handshake; keep twin-only local readback outside the vendor manifest, and record a reasoned impossible gap if no readable signal exists. Push MAY be a filed todo() gap when the vendor exposes no write path — an untickable "push" box for a read-only vendor is not the bar.
  • readOnly forbids writes; unmodeled ops fail like the vendor.
  • The pack descriptor is COMPLETE (§3): vendor matches the directory, transport, resources, specSource, description; rateBudget/pullPosture where they apply; and both XOR rulings made, not skipped — exactly one of hosts/hostsNone (§7 point 8) and exactly one of endpointEnv/endpointEnvNone (§7 point 12), each grounded rather than invented. adoption names every official npm client, scope, credential env stem and extra world id — and claims nothing this twin cannot honor.
  • No hand-table duplicate of a declared fact: nothing added to VENDOR_HOSTS, SDK_TWINS, SDK_SCOPE_VENDORS, ENV_STEM_VENDORS, VENDOR_WORLD_IDS or APP_READ_ENDPOINT_ENV for this vendor (dual declaration throws — §7 points 8, 11, 12).
  • bun scripts/pack-facts.ts re-run and packages/world-core/generated/pack-facts.json committed after the last descriptor edit; bun scripts/pack-facts.ts --check and bun test scripts/pack-facts.test.ts green (§7 point 13).
  • tsc clean; pack tests green; architecture guardrail + capability meta-test green.
  • ## Coverage section in the README; every unbuilt surface listed as a todo.
  • The motivating app scenario ran with its unmodified clients inside a World. Verify a meaningful write, subsequent read and failure/deletion path. When multiple interfaces address the same vendor state, write through each and read through the other; include the app behavior that depends on that consistency. Keep the scenario with the project's evidence for that app, beside any earlier journey. covers establishes connection requirements, not this proof. If an interface or workflow remains unsupported, keep that gap explicit and limit the completion claim accordingly.
  • §9 independent skeptic was run and its verdict is recorded in the commit/handoff report; no unresolved false-greens (incl. every primary-data UI verify is data-coupled, not marker-only); both rounds and every revert-matrix cell completed before the final baseline ratchet.
  • §8's per-merge set ran green (pack suites + typecheck + the meta-gates this wiring touched), and the pack's claim is stated as queued for the next full gate — which runs on the owner's ask, with TWIN_CHECK_CONCURRENCY=1 over one clean, committed, unchanged HEAD whose provenance SHA is recorded and during which no edit happened.

Self-audit: is your manifest honest? (run this before calling it done)

The gate catches broken twins; it does NOT catch a thin or false-green manifest. Audit yourself:

  1. Enumeration. List your capability ids; open the vendor's API reference; are you missing whole resource types / methods / a webhook family? If you listed 50 and they have 150, say so (README ## Coverage: "partial; core modeled; see todos") — don't imply completeness. Re-running the MANIFEST-COMPLETENESS AUDIT on your own pack is exactly this step.
  2. Tiering. For each core: would >50% of integrations use it in week one? If not, demote. For each niche: would you be unsurprised if most users never touch it? If you'd be surprised, promote.
  3. False-green hunt (per done): does its verify() start from a fresh root? does it assert field values (not just a status)? does it assert a negative 4xx? would it still pass if you replaced the handler body with return {}? For UI: does it prove data-coupling, not just a marker? If any answer is wrong, fix the verify or demote to todo.
  4. Regression. bun scripts/manifest-baseline-one.ts <vendor> must print regressions=0 and done>0 total>=50. Never push with a failing done verify — fix it or demote it.
  5. Adversarial pass. Have an independent READ-ONLY reviewer (Explore agent) try to REFUTE each done and confirm the new todos are honestly tiered (not denominator padding). Demote anything unproven. This is mandatory before merge.

Troubleshooting

Symptom Cause → fix
architecture.test.ts: "vendor SDK imported at runtime" SDK imported outside a *.test.ts → move it into <vendor>-sdk.integration.test.ts only (SDK is a devDep).
architecture.test.ts: conformance re-exported index.ts/cli.ts imports the conformance module → remove from index.ts; in cli.ts use await import('./<vendor>-conformance.ts') lazily.
capability-manifests.test.ts: "no manifest wired" / unknown vendor there is NO central edit any more — the registry globs packages/twin/* and throws on a vendor whose manifest module or conventionally-named export is missing. Fix the EXPORT NAME (<vendor>Capabilities and <VENDOR>_CAPABILITIES), not a registry.
capability-manifests.test.ts: missing <vendor>-capabilities.test.ts the meta-test requires it → add the baseline test (copy from the exemplar).
baseline fails: total<50 / done==0 thin manifest → enumerate more real surface; prove a few done.
baseline fails: "N regressions" a done verify now fails → bun scripts/manifest-baseline-one.ts <vendor> to see which; fix or demote.
mirror bundle times out under the full suite Bun.build per-process ceiling → expected; the meta-test isolates each manifest in its own process (scripts/manifest-baseline-one.ts); test your pack alone.
a create returns the WRONG resource (e.g. a project instead of your flag) the handler is writing/reading the wrong subject — the kernel resolves the returned resource by (type, id), so check the subjectType/subjectId you pass to applyTwinWrite (and that your pack doesn't mint duplicate ids within one type). See §5 note on subject ids.
a UI verify passes but the screen is hardcoded marker-only verify on a data screen → make it data-coupled (§6 UI bar); reserve marker-only for chrome.

Note on the scaffold script

This section used to say a scaffold generator was deliberately not provided. One exists: scripts/scaffold-pack.ts (§7, "Start here"). The old reasoning still holds for the half it does not attempt — twins vary enough by archetype (REST/GraphQL/generative/XML-SigV4/ management-wrapper) that a one-size domain skeleton would be thin — so the scaffolder is scoped to the wiring, not the twin: pack shell, the v2 pack descriptor with your hosts/adoption compiled in, the registrations, the censuses, and the regenerated pack-facts artifact. Domain code still comes from copy-from-exemplar (§0).

The bar the old note set is the one it holds itself to, with one honest exception: what it emits is architecture.test.ts-clean and tsc-clean, but assertManifestBaseline is RED until you earn the first real done, because a scaffold that shipped a total>=50 manifest stub would be fabricating a denominator. That red is the line's honest state, not a defect — and every judgment slot it leaves is a loud SCAFFOLD TODO(A1|A2) marker rather than a plausible guess.