| name | metagraphed |
| description | Use when writing, validating, or preparing ANY contribution or pull request to the JSONbored/metagraphed repo — adding/enriching a subnet's public surfaces (the most common contribution), a code/schema change to the Worker API or build scripts, picking an issue, running the local gates, and formatting the commit + PR. metagraphed reviews PRs ONE-SHOT via the Gittensory Gate (the GitHub App that auto-merges/auto-closes) plus a strict CI suite; there is no review back-and-forth, so a PR must be correct, in-scope, and green before it is pushed. Surfaces live in ONE file per subnet (registry/subnets/<slug>.json) — never per-surface candidate files, never split across multiple PRs. Also covers frontend PRs against apps/ui/ (the web app, folded into this repo via monorepo consolidation) — visual changes require a before/after screenshot table and are always held for manual review. Invoke for any "contribute to / open a PR against / enrich a subnet in / add a surface to / fix a bug in / add a frontend feature to metagraphed" task. |
Contributing to metagraphed — the one-shot PR playbook
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.
Three kinds of contribution — pick your path
| 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.
What the gate does to your PR — it merges and closes, automatically
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.)
The non-negotiables (read once, hold throughout)
- One subnet = one file = one PR. A surface contribution edits exactly one
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.)
- Prove the claim. Every surface needs a public
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.
- Don't invent surfaces. Only register what a subnet actually exposes. Schema-valid ≠ accepted.
- Health is probe-derived only. Never hand-set health, uptime, latency, incidents, or
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.
- Public-safe only. No secrets, PATs, wallet/hotkey/coldkey paths, private/localhost URLs, or
validator-local data anywhere — in files, commits, or PR text.
auth fields are placeholders
(Bearer <token>), never real credentials.
- Link an open issue — required. Every PR must reference an issue (
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.
- Schema is the contract — regenerate + commit (Path B). Editing
schemas/ means
npm run build then committing openapi.json + types/clients in the same PR, or
validate:contract-drift fails CI.
- Conventional Commits, no AI attribution. Lowercase scope, specific subject, no trailing period;
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.
Path A — Surface contribution (the common case)
Phase A0 — Bootstrap
git clone https://github.com/<you>/metagraphed && cd metagraphed
git remote add upstream https://github.com/JSONbored/metagraphed
nvm use
npm install
Phase A1 — Pick the subnet + find a real surface
- Search first. Check open issues AND open PRs for the same subnet/surface — a duplicate is a
close-worthy signal. Browse
good first issue
/ help wanted; the per-subnet
enrichment issues (#427) each name the exact gap.
- Find the 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.
- Confirm the surface is real and public. A safe public
url you can fetch, plus a source_url
that 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.
Phase A2 — Edit the ONE subnet file
A surface contribution adds entries to the surfaces[] array of registry/subnets/<slug>.json. Use
the helper so the id/shape are correct:
npm run providers:list
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
Each 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",
"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 in registry/subnets/? Scaffold it with npm run subnet:new -- --netuid <n>
first (one file), then add your surface to it in the same PR.
Phase A3 — Validate locally
npm run validate:surface -- registry/subnets/<slug>.json
npm run scan:public-safety
Fix every finding. (CI runs the full validate suite; these two are the fast local pre-checks for the
submission lane.)
Phase A4 — Commit + open the PR
- One subnet file changed, nothing else.
git diff --stat should show a single
registry/subnets/<slug>.json.
- Commit (Conventional):
feat(registry): add SN43 Example subnet-api surface (#<issue>).
- PR body: fill
.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.
Phase A5 — Let the gate adjudicate
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.
Path B — Code / schema PR
Phase B0 — Bootstrap + scope
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.
Phase B1 — Implement (match the house style)
- The Worker entry/router is
workers/api.mjs; serving/overlay/health logic lives in src/*.mjs;
the contract lives in schemas/ (+ schemas/components/) and src/contracts.mjs.
- Schema-first rule: never hand-edit the generated contract. Edit
schemas/ →
npm run build → commit openapi.json + generated types/clients in the same PR.
- A new
/api/v1 route or artifact trips hidden contract gates — see the new-route checklist in
reference.md before adding one.
Phase B2 — Test
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.
Phase B3 — Regenerate what you invalidated (then commit it)
| You changed… | Run | Commit |
|---|
schemas/ or schemas/components/ | 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.mjs | — | 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 their committed copies on
main are owned by the real deploy/publish pipeline (r2-manifest.json is the publish lockfile
read from its committed path at publish time; schemas/index.json is a network-capture cache the
build reconciles in place) — see the "Verify committed derived artifacts are fresh" step in
.github/workflows/validate.yml, which explicitly excludes both for this reason. A contributor
build will always show them as changed for reasons unrelated to your change. After
npm run build, always revert them against your base remote — upstream/main if you forked
per Phase A0, or origin/main if you cloned this repo directly (no upstream configured):
git checkout "$(git remote | grep -qx upstream && echo upstream || echo origin)/main" -- \
public/metagraph/r2-manifest.json public/metagraph/schemas/index.json
before staging/committing — even if they show as modified.
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.
Phase B4 — Run the gates locally (must be green)
git diff --check
npm run lint && npm run format:check
npm run validate
npm test
npm run validate:contract-drift npm run validate:schemas npm run validate:api npm run validate:openapi
For 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).
Phase B5 — Commit + PR
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.
Path C — Frontend 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.
Phase C0 — Bootstrap + pick an issue
npm install
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.
Phase C1 — Implement (match the house style)
- Reuse existing shared components and the design tokens in
apps/ui/src/styles.css (the "Bone & Ink"
system — 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.
- Anchor on an existing analogous page/component before writing a new one — this codebase already has
shared primitives (table-controls, chart primitives, copy/share buttons, entity hover-cards,
freshness badges) that most issues should compose rather than reimplement.
- Creative additions beyond an issue's stated scope are welcome but held to a higher bar — expect
extra scrutiny, and call out explicitly in the PR body anything you added beyond the issue.
Phase C2 — Screenshot contract (required for any visual change)
Non-negotiable for any PR that changes rendered output. PRs without it are auto-closed — no
exceptions. Follow the steps below exactly — 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.
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
cd -
npm run dev --workspace=apps/ui
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.
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");
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
git rm -rf . 2>/dev/null
cp /path/to/your/12/*.png .
git add *.png && git commit -m "screenshots for PR"
git push origin screenshots
cd -
Reference 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 is zero-config there. A scripted capture pipeline that automates the
screenshot-taking itself (tracked in #3769) doesn't exist yet — until it lands, follow the steps
manually.
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.
- Record the interaction, don't screenshot it. Use your OS's screen recording (macOS
Cmd+Shift+5
or 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.
- Convert to a GIF — a
.mov/.webm file won't render inline in a GitHub-hosted <img> tag the
way a .gif does:
ffmpeg -i recording.mov -vf "fps=12,scale=480:-1:flags=lanczos" -loop 0 hover-before.gif
Keep it small (a few seconds, ~12fps, ≤480px wide) — an oversized GIF is as unreviewable as #3757's
full-page screenshot bug was.
- Same hosting mechanism as step 5 above — the dedicated
screenshots branch on your own fork.
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.
- Table format — one row per interaction target (not per viewport/theme; a hover/scroll/transition
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.
- A PURELY interaction-only change (identical at-rest state, only the triggered behavior changed) can
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.
Phase C3 — Test + gates locally
The ui CI job runs lint, typecheck, test, a responsive-overflow e2e check, build, 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
npm test --workspace=apps/ui
npm run test:e2e --workspace=apps/ui
npm run build --workspace=apps/ui
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 ~300 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.
Phase C4 — Commit + PR
Conventional Commit (e.g. feat(ui): add validator directory table), no AI attribution, Closes #<issue> — required, and the issue must still be open. Fill the screenshot table if the change is visual.
Phase C5 — Review disposition
Any visual PR touching apps/ui/ is always held for manual review, regardless of AI-review
confidence — this is a deliberate exception to the normal one-shot autonomous gate. A non-visual
apps/ui/ PR (data/hooks/tests only) follows the normal auto-merge/auto-close gate like Path A/B.
Final pre-push checklist
Path A (surface):
Path B (code/schema):
Path C (frontend):
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.