metagraphed — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited metagraphed (Agent Skill) and scored it 92/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 2 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
metagraphed is the Bittensor subnet integration registry — every subnet, metagraphed. The repo is a Cloudflare Worker API + Node build scripts; JSON Schema is the canonical contract (→ OpenAPI → typed clients), and everything under public/metagraph/ is a _generated projection_ of reviewed source, never hand-authored truth.
It merges through an automated, one-shot review: the Gittensory Gate (a GitHub App that posts Gittensory Gate + Gittensory Context checks and a single verdict) plus a strict CI suite (Validate). There is no human ping-pong and no "fix it in review" — the PR must be right before you push. This skill is the end-to-end procedure to make that happen with AI tools (Claude Code / Codex).
Work through the phases in order for your contribution type. If you cannot get the local gate green, do not push — an incomplete PR is auto-closed or held, not coached.
reference.md (next to this file) has the exhaustive tables — every CI check, the surface schema, the kind enum, the gate disposition, the validator list, the commit/PR rubric. Read it when a phase says to.
Zero-setup environment: if you're operating in a devcontainer-aware tool, open the repo there — .devcontainer/devcontainer.json pins Node 22 and preinstalls Playwright's Chromium (needed for Phase C2's screenshot contract), so npm install is the only remaining step. Otherwise .nvmrc at the repo root pins Node 22 for nvm use.
| You are… | Path | Files you touch |
|---|---|---|
| Adding or enriching a subnet's public surfaces (API, OpenAPI, docs, repo, dashboard, SDK, data artifact…) | Path A — Surface contribution (Phases A0–A5) | exactly one registry/subnets/<slug>.json |
Changing code, schemas, or build scripts (Worker API, schemas/, scripts/, workflows) | Path B — Code/schema PR (Phases B0–B5) | src/, workers/, schemas/, scripts/, .github/, + regenerated artifacts |
| Fixing a bug or shipping a feature in the web app (block explorer, docs pages, dev tools) | Path C — Frontend PR (below) | apps/ui/** only |
Most contributions are Path A. Do not mix any of the three in one PR.
The Gittensory Gate is not advisory. Once your checks settle, for a contributor PR (you are not the repo owner or an automation bot) it takes a one-shot disposition:
| Situation | Gate action |
|---|---|
| Content verified (owner-matched, fresh, grounded) + both AI reviewers confidently approve (≥0.9) + CI green + mergeable-clean | auto-approve → MERGE |
A deterministic fail — duplicate surface, placeholder, private/localhost URL, secret, dead source_url | CLOSE (one-shot) |
| Every reviewer returns a clear reject | CLOSE (one-shot) |
| Any CI check failed | CLOSE (cites the failing check) |
Legitimate but uncertain — a reviewer wanted merge but under 0.9, a reviewer said manual, reviewers split, owner-mismatch, stale repo, unfetchable evidence | MANUAL (held, not closed) |
| CI still pending / unverified fork run | no action — waits |
So a flawed contributor PR is closed, not coached — recovery means fixing the problem and opening a fresh PR. Verified + green ⇒ merged; a clear adverse signal ⇒ closed; genuine uncertainty ⇒ held for a human. (Owner / automation-bot PRs are exempt from auto-close — but assume you are a contributor.)
registry/subnets/<slug>.json and nothing else (no generated artifacts, no scripts, no other subnet). You may add several surfaces for that one subnet in the same diff — that is one merge, the way it should be. Never split a subnet's surfaces across multiple PRs and never re-title the same surface as a different kind to make it look new: the gate dedups within the file and closes redundant/near-duplicate PRs. (This is exactly the farming the single-file model exists to stop.)
url and a source_urls entry that_independently proves_ the subnet/operator actually publishes it (an official repo README, the provider's own site, on-chain identity). A source_url that 404s or doesn't back the claim → closed.
verification — the build's prober owns those. You set identity (url, kind, provider, source_urls) and review.state: community-submitted; the gate and build do the rest.
validator-local data anywhere — in files, commits, or PR text. auth fields are _placeholders_ (Bearer <token>), never real credentials.
Closes #<n> / Refs #<n>)in the PR body, and that issue must be open/unclosed at submission time — the gate verifies the PR against that issue's intent, clause by clause. No linked issue, or a linked issue that's already closed, is an automatic close on its own, before content is even scored. For surface work, the per-subnet enrichment issues under epic #427 are the natural home to link — pick one that's still open before you start.
schemas/ meansnpm run build then committing openapi.json + types/clients in the same PR, or validate:contract-drift fails CI.
no AI/Claude/agent mention anywhere in commits or PR text. Frontend/UI work lives in this repo at apps/ui/ — see Path C below; it is not Path A or Path B.
# External contributor? Fork JSONbored/metagraphed, then clone YOUR fork:
git clone https://github.com/<you>/metagraphed && cd metagraphed
git remote add upstream https://github.com/JSONbored/metagraphed
nvm use # Node 22 (engines: >=22.23.0)
npm install # required before any validator runsclose-worthy signal. Browse good first issue / help wanted; the per-subnet enrichment issues (#427) each name the exact gap.
npm run curation:brief lists profile-light subnets (directory-only, no website /source repo, public APIs with no OpenAPI yet). See docs/curation-playbook.md.
url you can fetch, plus a source_urlthat proves the subnet publishes it. Pick the right kind (full enum in reference.md): contributor kinds are docs, website, source-repo, openapi, subnet-api, dashboard, sse, data-artifact, sdk, example, repo-registry — all auto-reviewable; authed/paid APIs + unknown providers are higher-trust (airtight ownership proof). Base-layer chain endpoints (subtensor-rpc/wss, archive) are maintainer-curated infra (the endpoint lane), not contributor surfaces. Prefer high-value callable kinds (openapi, subnet-api, sse, data-artifact, sdk) — source-repo and website are auto-promoted from on-chain identity data, and validate:surface rejects them if the machine already has the URL (CI fails → gate closes). See reference.md §5.
A surface contribution adds entries to the surfaces[] array of registry/subnets/<slug>.json. Use the helper so the id/shape are correct:
# Find the provider slug for the team behind the surface.
npm run providers:list
# Append a community surface to the subnet file (writes into registry/subnets/<slug>.json):
npm run surface:add -- \
--netuid 43 --kind subnet-api \
--url https://api.example.com/v1 \
--source-url https://github.com/example/project/blob/main/README.md \
--provider <provider-slug> --submitted-by <github-login> --write
# Debut provider (slug not registered)? Add the team identity and surface:add scaffolds
# registry/providers/<slug>.json (flat — trust is the authority field) in the SAME PR.
# --provider-url is the provider's website_url and MUST be a public URL (validate
# rejects private/localhost), as must any logo/docs/github/team/contact/social URL:
# --provider-name "Example Team" --provider-url https://example.comEach added surface must carry authority: "community" and a review block — the helper sets these:
{
"id": "sn-43-example-subnet-api",
"name": "Example subnet API",
"kind": "subnet-api",
"url": "https://api.example.com/v1",
"provider": "example",
"authority": "community", // existing enum value — community-submitted, not official truth
"auth_required": false,
"public_safe": true,
"source_urls": ["https://github.com/example/project/blob/main/README.md"],
"review": {
"state": "community-submitted",
"submitted_by": "<github-login>",
},
"notes": "One line on what it is / why it's the right surface.",
}You set identity + proof + `review.state: community-submitted` only. For an existing subnet manifest, do not add verification, health, or curation changes, and do not touch other surfaces or top-level fields in the file — a community PR that edits anything beyond appending its own community surface(s) is out-of-shape and gets routed to full review or closed. A missing subnet manifest is the exception: subnet:new creates the required top-level scaffold fields, then surface:add appends the community surface in that same new file. review.state is the human-governance axis: a maintainer flips it → maintainer-reviewed (or rejected) in place; machine verification + freshness is the separate probe overlay (the build's prober fills verification/health).
New subnet not yet inregistry/subnets/? Scaffold it withnpm run subnet:new -- --netuid <n>first (one file), then add your surface to it in the same PR.
npm run validate:surface -- registry/subnets/<slug>.json # schema + provider-slug + review-shape
npm run scan:public-safety # no secrets / private URLsFix every finding. (CI runs the full validate suite; these two are the fast local pre-checks for the submission lane.)
git diff --stat should show a singleregistry/subnets/<slug>.json.
feat(registry): add SN43 Example subnet-api surface (#<issue>)..github/pull_request_template.md honestly — a real Summary, the url +source_url proof, the validation commands you ran, and `Closes #<issue>` — required, and the issue must still be open. No AI attribution.
Watch Validate and Gittensory Gate go green. Verified + green → merged. A deterministic fail (dup / dead source / private URL) or a clear reject → closed; fix and open a fresh PR. Genuine uncertainty → held for a human — don't open a duplicate.
npm install (Node 22). Open an issue first for anything risky (public behavior, schema/contract changes, new routes, workflows, deps). Keep the PR narrow — one coherent change. Anchor on existing code: find ≥2 analogues in the repo, cite them file:line, trace the closest end-to-end, and match its structure, naming, and comment density. Build for the class, not the one case.
workers/api.ts; serving/overlay/health logic lives in src/*.ts;the contract lives in `schemas-src/` (Zod, the single source) and src/contracts.ts. schemas/*.json is a different thing — the INPUT schemas that validate hand-written registry files. A published component declared anywhere but schemas-src/ fails validate:single-schema-source.
.mjs/.js. The TypeScript migration(metagraphed#7510) is complete, and the validate:no-hand-written-mjs CI gate fails any PR that adds a .mjs/.js file under src/, workers/, scripts/, or tests/ (metagraphed#7521).
schemas-src/ (and register it in schemas-src/openapi-registry.ts if it is new) → npm run build → commit openapi.json + generated types/clients in the same PR.
/api/v1 route or artifact trips hidden contract gates — see the new-route checklist inreference.md before adding one.
Tests are vitest under tests/. Add coverage for new branches and fallback paths, and a regression test for every bug fix. Codecov is the coverage gate — codecov/patch enforces 99% patch coverage, branch-counted, with zero threshold slack (target: 99%, threshold: 0% in codecov.yml), scoped to src/** + workers/** runtime code. Run it unsharded locally: npm run test:coverage. Reader tests serve R2-only artifacts that only exist after a build, so npm run build before the suite if a test reads served artifacts. Running a single reader test file in isolation (e.g. npx vitest run tests/some-route.test.ts) needs the same precondition — those artifacts live under gitignored dist/metagraph-r2/metagraph/, populated as a side effect of tests/artifacts.test.ts / tests/discovery-artifacts.test.ts when the full suite runs first (alphabetically), but absent on a fresh checkout or a filtered single-file run. For just the fixture tree, without the rest of npm run build's slower steps (type/client/GraphQL generation) and without ever touching public/metagraph/, run npm run artifacts:prepare-local first.
| You changed… | Run | Commit |
|---|---|---|
schemas-src/ (any published schema) | npm run build | openapi.json, generated types, contracts.json, api-index |
A new/edited /api/v1 route or artifact | npm run build | the derived public/metagraph/* it produces |
A canonical registry/providers/<slug>.json | npm run build | regenerated artifacts (commit only the provider file + its artifacts) |
MCP tools in src/mcp-server.ts | — | nothing — the server card is worker-computed, not a committed artifact |
| _(any of the above)_ | npm run build | never public/metagraph/r2-manifest.json / public/metagraph/schemas/index.json — see below |
Stale committed artifacts fail the derived-artifact freshness + contract-drift gates.
Never commit `public/metagraph/r2-manifest.json` or `public/metagraph/schemas/index.json`. npm run build always rewrites both to reflect a full local/CI build, but neither committed copy is yours to move: r2-manifest.json is the publish lockfile, read from its committed path at publish time; schemas/index.json is a live network-capture cache whose current copy lives in the generated/schemas-index.json R2 store, written daily by a Worker cron (src/schema-snapshots-sync.ts) — the committed file is only the fallback seed a credential-less build reads when it cannot reach that store. Both are excluded from the "Verify committed derived artifacts are fresh" step in .github/workflows/validate.yml for this reason. A contributor build will always show them as changed for reasons unrelated to your change.
public/metagraph/operational-surfaces.json is the same shape of thing one rung down: also a fallback seed (for the health prober's cold start), also written live by an hourly Worker cron (src/operational-surfaces-sync.ts → generated/operational-surfaces.json), also excluded from that gate. It is not on the never-commit list because a normal build reproduces it deterministically — but if it shows up dirty in a PR that did not intend to change the surface set, revert it the same way.
Both npm run build and a standalone node scripts/build-artifacts.ts / npm run build:artifacts already auto-revert whichever of these actually went dirty, back against your base remote (upstream/main if you forked per Phase A0, origin/main otherwise) — git status should already be clean after either. Treat that as a safety net, not a guarantee: it silently degrades to a printed warning (not a failure) if the revert itself can't reach your base remote (e.g. no network, or upstream/origin isn't fetched). So still check git status before staging, and if either file shows modified, revert it by hand:
git checkout "$(git remote | grep -qx upstream && echo upstream || echo origin)/main" -- \
public/metagraph/r2-manifest.json public/metagraph/schemas/index.jsonbefore staging/committing.
Client SDK version: do not bump packages/client/package.json in your PR. The sync-client-version workflow auto-opens a chore/sync-client-version PR after a contract-changing merge. validate:client-sdk-sync now emits a notice (not a failure) when the version wasn't bumped in the contributor PR.
git diff --check
npm run lint && npm run format:check # NOTE: main isn't fully prettier-clean — never reformat whole files you didn't change
npm run typecheck # tsc --noEmit, whole in-scope tree (src/workers/scripts/tests)
npm run validate # registry + API + OpenAPI checks
npm test # or: npm run test:coverage for the coverage gate
# Then the focused validators for what you touched (full list in reference.md), e.g.:
npm run validate:contract-drift npm run validate:schemas npm run validate:api npm run validate:openapiFor a faithful full local run, npm run pipeline:check — but only trust it in isolation after a clean npm run build (see the build-gotchas note in reference.md).
Conventional Commit (no AI attribution); Closes #<issue> — required, and the issue must still be open; fill the PR template with the validation commands you actually ran. Sync with main if it moved (git fetch upstream && git rebase upstream/main) — a base conflict closes a contributor PR.
apps/ui/)apps/ui/ is the TanStack Start + Vite + React web app at metagraph.sh — folded into this repo as an npm workspace via the monorepo consolidation. It has its own ui CI job (lint + typecheck + test + a responsive-overflow e2e check + build + bundle-budget, see reference.md §2) and its own review contract, distinct from Path A/B.
npm install # root install wires the apps/ui workspace too (Node 22)Pick a gittensor:bug / gittensor:feature issue scoped to apps/ui/ (Wave 3 milestone). Keep the PR narrow — aim for ≤10 files / ≤1000 LOC; if an issue looks bigger than that once you're in the code, ship the smallest coherent slice and leave a follow-up note rather than bundling everything into one PR.
packages/ui-kit/src/styles.css (the v2system — warm bone/paper background, deep ink text, mint accent used sparingly, flat surfaces with hairline borders, no shadows or gradients) instead of inventing new one-off styles.
shared primitives (table-controls, chart primitives, copy/share buttons, entity hover-cards, freshness badges) that most issues should compose rather than reimplement.
extra scrutiny, and call out explicitly in the PR body anything you added beyond the issue.
Non-negotiable for an external-contributor PR that changes rendered output. Without an explicit maintainer override, a PR without this evidence is auto-closed. A real PR (#3757) shipped 10 of its 12 screenshots at 115,000–142,000px tall (a full-page capture bug, not a display issue) and sat unreviewable until recaptured. Don't repeat that.
Maintainer-directed override. When a repository maintainer explicitly waives screenshots for the current task, skip this phase—including static and animated evidence—and do not request the waived evidence again. Never infer the waiver from branch ownership or repository access. Continue to run proportional responsive/interaction validation and every required code/CI gate.
Recommended: automated capture (#3769). apps/ui/tests/e2e/capture-pr-screenshots.ts automates everything below — the two-worktree orchestration, fixed-viewport-only capture, explicit theme toggling, the 12-image matrix, and (with --push) hosting + the ready-to-paste markdown table:
npm run screenshots --workspace=apps/ui -- --route /subnets/1 --section volume-24h --prefix 5483-volume --pushAdd --section <id> for a below-the-fold section anchor (omit to capture the page top), and --fallback-section <id> when before doesn't have that anchor yet (the common case for a new section — point it at the existing anchor the new one attaches after). Already have two dev servers running (e.g. mid-session in an AI coding tool)? Skip the orchestration and point at them directly: --before-url http://localhost:8081 --after-url http://localhost:8080. Run npm run screenshots --workspace=apps/ui -- --help for the full flag list.
If the tool doesn't fit your case (a capture step needs manual intervention, or you're debugging the tool itself), the equivalent manual steps are below — same contract, same output.
1. Two dev servers — one for `before`, one for `after`. Don't reuse a single server for both; run the before state from a separate worktree so nothing needs stashing/restoring mid-capture:
git worktree add ../metagraphed-before $(git merge-base main HEAD)
cd ../metagraphed-before && npm install && npm run dev --workspace=apps/ui # note the printed Local URL — this is "before"
cd - # back to your feature branch
npm run dev --workspace=apps/ui # note this Local URL — this is "after"2. Fixed viewport sizes only — never a full-page / `fullPage: true` capture. A full-scroll-height capture is exactly what produced #3757's broken screenshots. Use these three sizes — chosen to straddle this app's actual Tailwind breakpoints (md=768px, lg=1024px, the two most-used responsive prefixes in apps/ui/src):
| Viewport | Size (px) |
|---|---|
| Mobile | 375 × 812 |
| Tablet | 768 × 1024 |
| Desktop | 1280 × 800 |
Capture exactly that viewport, nothing more. If the changed content is below the fold, scroll to it first — don't reach for a full-page capture to get there.
Also never `locator.screenshot()` / `elementHandle.screenshot()` (e.g. page.locator(".hero").screenshot(...)) as a shortcut to "just the changed part" — confirmed on #6928/#6933, both shipped with a locator-scoped capture instead of the fixed viewport. It crops to that element's own rendered box, not the fixed viewport frame this contract requires, so a reviewer can't see the change in its real page context (surrounding layout, whether anything else shifted). The only call to make is page.screenshot({ path }) — no fullPage, no locator, after resizing the viewport and scrolling to the changed content if needed.
3. Force each theme explicitly — never rely on system/`prefers-color-scheme` (it varies by capture environment, so it isn't reproducible run to run). In the page, before capturing:
localStorage.setItem("mg-theme", "dark"); // or "light"
location.reload();mg-theme is THEME_STORAGE_KEY in apps/ui/src/lib/theme.ts — the only supported mechanism. Reload after setting it so the pre-hydration bootstrap script applies it with no flash-of-wrong-theme.
4. 3 viewports × 2 themes × {before, after} = 12 images, for a page/feature-level change. Skip a combo only if you state in one sentence why it's provably unaffected (e.g. a change gated behind a desktop-only code path).
5. Host the 12 files on a dedicated branch in your own fork — never drag-and-drop, never commit them to your feature branch. Drag-and-drop into the GitHub web editor requires a human browser session, which an AI coding tool cannot do end-to-end; a pushed branch is fully scriptable and keeps binary images out of your feature branch's diff entirely. Do this from a throwaway worktree, not your feature branch's working directory:
git worktree add ../metagraphed-screenshots main
cd ../metagraphed-screenshots
git checkout --orphan screenshots # first time; if you already have a `screenshots` branch from a
git rm -rf . 2>/dev/null # prior PR, just `git checkout screenshots` instead and skip these two lines
cp /path/to/your/12/*.png .
git add *.png && git commit -m "screenshots for PR"
git push origin screenshots
cd - # your feature branch's working directory was never touchedReference each file as https://raw.githubusercontent.com/<your-fork-owner>/metagraphed/screenshots/<file>.png.
6. Table format — one row per viewport+theme, thumbnail + caption in each cell, both before and after:
| Viewport · Theme | Before | After |
| ---------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Desktop · Light | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Desktop · Dark | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Tablet · Light | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Tablet · Dark | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Mobile · Light | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Mobile · Dark | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |Screenshots go inside the table only — never pasted loose in the PR body, never committed to the feature branch itself.
A PR confined to apps/ui/src/lib/** / apps/ui/src/hooks/** / test files, with no visual change, skips this entirely — it isn't rendering anything different.
The devcontainer (.devcontainer/devcontainer.json) preinstalls Node 22 + Playwright's Chromium, so setup for the steps above (manual or via the automated tool) is zero-config there.Animated evidence (#4825) — for effects no static screenshot can show. Required whenever the changed behavior is only visible in motion: a hover-triggered popover, a scroll-linked effect, a CSS transition/animation, a drag interaction, or anything else where "before" and "after" aren't just two different static layouts. This is _additional_ to the static table above, not a replacement for it — a real PR (#4814) shipped both: the static viewport × theme matrix for the at-rest layout, plus a before/after GIF table for the hover behavior itself, because a still image genuinely cannot show what happens on hover.
Cmd+Shift+5or screencapture -V; Linux wf-recorder/ffmpeg -f x11grab) or a Playwright video/trace, scoped tightly to the interactive element — not the full viewport, and not a long clip. A few seconds showing the cursor entering, the effect triggering, and the resulting state is enough.
.mov/.webm file won't render inline in a GitHub-hosted <img> tag theway a .gif does:
ffmpeg -i recording.mov -vf "fps=12,scale=480:-1:flags=lanczos" -loop 0 hover-before.gifKeep it small (a few seconds, ~12fps, ≤480px wide) — an oversized GIF is as unreviewable as #3757's full-page screenshot bug was.
Push the .gif files alongside your PNGs in the same orphan-branch commit; reference them the same way: https://raw.githubusercontent.com/<your-fork-owner>/metagraphed/screenshots/<file>.gif.
effect is rarely breakpoint- or theme-dependent, so don't multiply it out the way the static matrix does unless the interaction genuinely differs by breakpoint):
### Hover interaction (animated)
Static images can't show the pointer-driven [behavior] — here's the actual interaction.
| Target | Before | After |
| --------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------ |
| <describe the interactive element, e.g. "Blocks · author cell"> | [<img src="BEFORE_GIF_URL" width="380">](BEFORE_GIF_URL) | [<img src="AFTER_GIF_URL" width="380">](AFTER_GIF_URL) |One row per distinct interactive element the PR changes.
skip the static matrix for that specific view — state why in one sentence, the same "provably unaffected" exception already allowed for a static viewport/theme combo.
The ui CI job runs lint, typecheck, test, build, a responsive-overflow e2e check, and a bundle-size-budget check, in that order — run the same locally before pushing:
npm run lint --workspace=apps/ui && npm run format:check --workspace=apps/ui
npm run typecheck --workspace=apps/ui # auto-builds packages/client first (pretypecheck) -- no separate step needed
npm test --workspace=apps/ui
npm run build:worker --workspace=apps/ui # MUST precede test:e2e since #8928 — see below
npm run test:e2e --workspace=apps/ui # needs a Chromium browser: npx playwright install --with-deps chromium (once)Build now comes before `test:e2e` (#8928). The e2e check serves the PRODUCTION build via wrangler dev rather than npm run dev: the sweep loads 26 routes x 4 viewports, and Vite's dev server compiled each route on first hit, making that step ~48% of the whole ui job. It also means the check now exercises the bundle that actually ships, so prod-only breakage the dev server hid is in scope.
Use `build:worker`, not build. A plain npm run build emits .output/ with no Worker entry; the cloudflare-module preset (which CI and production use) emits `dist/`. test:e2e serves dist/server/wrangler.json and fails without it.
This does NOT change Phase C2's screenshot workflow below, which still uses npm run dev on purpose: those are contributor-facing before/after captures, not a CI gate.
The responsive-overflow e2e check replays recorded API traffic (tests/e2e/har/*.har) instead of live production data, so it's deterministic regardless of live chain state. If your PR adds a new API call on one of the checked routes (/, /subnets/1, /endpoints, /status, /settings, /explorer), re-record: npm run test:e2e:record-har --workspace=apps/ui against a running dev server.
CI also gzip-measures the initial client JS for a cold / visit against a budget (currently 400 KB, .github/workflows/validate.yml's "Bundle size budget" step) — keep new dependencies/imports lean; if a real feature legitimately grows it, raise the budget deliberately in the same PR. If your PR also touches packages/client or packages/ui-kit, CI rebuilds each fresh and diffs against its committed dist (packages/client/dist / packages/ui-kit/dist) — run npm run build --workspace=packages/client (or --workspace=packages/ui-kit) and commit the result if you changed packages/client/src (or packages/ui-kit/src). packages/ui-kit also gets its own npm run typecheck --workspace=packages/ui-kit step in the ui CI job.
Conventional Commit (e.g. feat(ui): add validator directory table), no AI attribution, Closes #<issue> — required, and the issue must still be open. External-contributor visual changes must fill the screenshot table unless a repository maintainer explicitly waived it for the current task.
An external-contributor visual PR touching apps/ui/ is held for manual review regardless of AI-review confidence. For maintainer-directed work, an explicit authorization to merge after required CI is green replaces that manual-review wait; merge when green without asking for the same approval again. A non-visual apps/ui/ PR (data/hooks/tests only) follows the normal auto-merge/auto-close gate like Path A/B.
Path A (surface):
registry/subnets/<slug>.json changed; existing manifests only append communitysurface(s), while missing manifests may include the required subnet:new scaffold plus the community surface(s); no other file.
url + a proving source_url; right kind; authority: community;review.state: community-submitted; public_safe: true; no health/verification/secrets set by hand.
kind.npm run validate:surface + npm run scan:public-safety clean.npm run build locally out of caution (not normally required for Path A), your diffstill touches only your one subnet file — see the Path B note below on public/metagraph/r2-manifest.json / public/metagraph/schemas/index.json; the Gittensory Gate's registry-review lane rejects a PR that bundles either in with your surface change.
Path B (code/schema):
npm run build artifacts (OpenAPI/types/contracts) as applicable. MCPtool additions do NOT require server-card regen (worker-computed). Client version bump NOT required (auto-sync workflow handles it post-merge).
public/metagraph/r2-manifest.json and public/metagraph/schemas/index.json are not part ofyour diff — both always change on a local/CI build for reasons unrelated to your PR (they're deploy/publish-pipeline-owned, not contract artifacts). npm run build (and a standalone scripts/build-artifacts.ts) already auto-revert either one if it went dirty; double-check git status and fall back to the Phase B3 command above if either still shows modified (e.g. the auto-revert couldn't reach your base remote).
git diff --check clean · lint + format:check + typecheck clean · npm run validate green ·npm run test:coverage green · the focused validate:* for what you touched green.
main; Conventional Commit (no AI attribution); PR template filled; Closes #<issue> — required, referencing an issue that's still open.Path C (frontend):
apps/ui/** only; ≤10 files / ≤1000 LOC where reasonably possible.apps/ui/src/styles.css) and shared components rather thanone-off styling.
table (mobile + dark-mode captures where relevant) — missing/malformed external-contributor evidence is an automatic close.
explicitly waived: a before/after GIF table alongside the static one, per the "Animated evidence" step in Phase C2.
lint + format:check + typecheck + test + test:e2e + build all green(--workspace=apps/ui); bundle size still under budget.
packages/client/src or packages/ui-kit/src changed: rebuilt and committed the respectivedist (packages/client/dist / packages/ui-kit/dist).
Closes #<issue> — required, referencing an issue that's still open.If every box is checked, the PR has the best chance of a one-shot approve-and-merge. If any box can't be checked, keep working — don't push.
When you need the exhaustive detail behind any phase, read `reference.md` in this skill directory.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.