Instructions for coding agents working in this repository, per the agents.md convention. Agents read the nearest AGENTS.md in the directory tree, so a package in a monorepo can add its own.
This file is hand-written and is never generated. The public agent routing document — the one served at
/AGENTS.mdfor external developers and their agents — ispublic/AGENTS.md, generated byscripts/agents.mjs. Do not edit that one by hand; your changes will be overwritten on the next commit that mentionsagents.md.
- This app runs on Next.js 16 — its APIs and conventions may differ from older Next versions in your training data.
- Install deps:
npm install - Start dev server:
npm run dev - Build:
npm run build - Install git hooks (once per clone):
./githooks/install.sh
These are written by scripts/llms.mjs and scripts/agents.mjs:
public/llms.txtpublic/llms-full.txtpublic/AGENTS.md
To change their contents, edit the source instead:
- A page's title or description → that route's
export const metadatain itspage.tsx. This is the same metadata Next uses for SEO, so there is one source of truth rather than two. - Ordering, sections, network reference, routing rules, freshness →
llms.config.mjs. - Hand-written prose inside
llms-full.txt→ edit between the<!-- LLMS_EXTRAS_START -->and<!-- LLMS_EXTRAS_END -->markers directly inpublic/llms-full.txt. That region is preserved verbatim across regenerations; everything outside it is not.
Check whether they are current: npm run llms -- --check
Include llms.txt or agents.md in your commit message and the post-commit
hook regenerates and creates one follow-up commit:
git commit -m "feat: add snapshots page; updates llms.txt"
Adding, deleting, or renaming a page.tsx triggers it automatically. To opt out
of a single commit, use SKIP_DOCS_HOOK=1 or put [skip-docs] in the message.
- TypeScript strict mode
- Follow the existing formatter config; do not reformat unrelated files
- Icons: there is no icon library; icons are hand-authored inline SVGs,
with the commonly reused ones centralized in
app/components/ui/icons.tsx. When adding or touching an icon that toggles between two states based on props/state (e.g. copy → check, menu → close) — not just shown/hidden — consider animating the transition withmorphiconsinstead of an instant swap:<MorphIcon icon={condition ? A : B} />frommorphicons/react, fed path data on a shared 24x24 grid (fitIcon()first if the source art isn't already on that grid). SeeCLIPBOARD_MORPH_ICON/CHECK_MORPH_ICONinapp/components/ui/icons.tsxand their use inapp/demos/b20/components/CopyPromptButton.tsx,app/internal-explorer/components/CopyButton.tsx, andapp/vibenet/components/CopyableValue.tsx.
Two separate suites, for two separate purposes — don't mix their files.
-
Unit/component/contract tests — Vitest.
*.test.ts(x), colocated next to the code they cover (e.g.app/snapshots/networks.contract.test.ts,deploy.config.test.mjs). Run withnpm test. Fast, no browser, no build — this is where logic, data transforms, and contract/matrix assertions live. Blocking CI job (test). -
End-to-end journeys — Playwright.
e2e/*.spec.ts. Run withnpm run test:e2e, which builds the app and drives it in a real browser against that production build. Non-blocking CI job (e2e (non-blocking),continue-on-error: true) — failures are visible but don't block merges while this suite is still young.vitest.config.mtsexcludese2e/**from Vitest's default test glob so the two runners never pick up each other's files.When to add an e2e test: only for surfaces that are stable — established pages/journeys not expected to change shape week to week (e.g. the home page, core nav, Snapshots, Upgrades). Do not add e2e coverage for a new demo or anything still being iterated on (e.g. work under
app/vibenet/demos/) — the UI will keep shifting under the test faster than the test catches real regressions, making it pure maintenance overhead. Add a smoke test once a demo's surface has settled, not before. -
Run the llms-kit suite (generation scripts under
scripts/, unrelated to the app tests above):node --test tests/ -
Include liveness checks against the deployed site:
LLMS_LIVE=1 node --test tests/
This app uses Vercel Web Analytics. Two things must stay in place:
-
Page views — render
<Analytics />from@vercel/analytics/nextinapp/layout.tsx. Removing it stops all analytics (page views and events). -
Custom events — typed helpers in
app/analytics/events.tsemit only when a component calls them. When you refactor or rewrite a wired surface, carry thetrack()call over. Current wiring:Helper Call site trackNavClick(label)app/components/AppShell.tsx—NavRow<Link onClick>trackSnapshotNetworkSelect(network)app/snapshots/SnapshotsClient.tsx— network buttononClicktrackSnapshotPresetSelect(name)app/snapshots/SnapshotsClient.tsx—selectPreset()trackSnapshotCommandCopy(network, preset)app/snapshots/SnapshotsClient.tsx—InlineCommandonCopytrackFaucetRequest(token, status)app/vibenet/faucet/page.tsx—runDrip()trackAccountAction(name)app/vibenet/demos/account/AccountDemo.tsx— feature tiles (transact, sponsorship, batched calls, gas-in-token, modify owners)trackB20ModuleSelect(module)app/vibenet/demos/b20/B20Demo.tsx— module navigationtrackB20Action(module, action, status)app/vibenet/demos/b20/B20Demo.tsx— B20 broadcaststrackB20PromptCopy(module, prompt)app/vibenet/demos/b20/components/CopyPromptButton.tsx— copy AI prompttrackExplorerChainSelect(chain)app/internal-explorer/components/ChainToggle.tsx— chain toggletrackExplorerActiveBlockJump(chain, jump)app/internal-explorer/components/ActiveBlockButton.tsx— zeronet latest/previous active blockAdd a helper (and a row here) for a new key journey; remove the helper if you remove its surface. Confirm the wiring with
grep -rn "analytics/events" app. Custom events collect on Vercel deployments only (not localnpm run dev); Vercel auto-discovers event names.
This one repo builds two deployables from the same source:
- external — the public site on Vercel (
npm run build/npm run dev). The default target. - internal — a separate internal deployment
(
npm run build:internal/npm run dev:internal, which setNEXT_PUBLIC_DEPLOY_TARGET=internal).
Which sections ship to which target is declared in deploy.config.mjs — the
SURFACES map (section → { routePrefixes, apiPrefixes, targets }). A section
absent from the map ships everywhere — the map is an exception list, so an
internal-only page with no entry will fail open and publish. A disabled surface
is unreachable in that build (routes + API 404, dropped from nav/sitemap/llms);
its client chunks may still be emitted, so treat this as a reachability
guarantee, not secrecy. Internal Explorer (/internal-explorer) and
Benchmark are internal-only today, and the public-build-excludes-internal
CI job enforces their absence from the public build — extend that job's path
list when you add another internal-only surface.
When you add or change an environment-specific section:
- Add/edit its
deploy.config.mjsentry. That alone makes middleware 404 its routes (disabledRoutePrefixes()) and drops it from the generated llms/agents artifacts (llms.config.mjsusesdisabledRouteGlobs()). - Gate the per-section surfaces that aren't automatic: the nav entry
(
app/internal-explorer/flag.ts→app/navigation.ts), a layoutnotFound()backstop, and the API routes (app/api/internal-explorer/guard.tspattern). UsesurfaceEnabled(...). A section with no API routes of its own needs no guard and noapiPrefixesentry — Benchmark is the example: its browser code calls the report API directly viaNEXT_PUBLIC_BENCHMARK_API_BASE_URL. - Select the target only via the build/dev script, never a hand-set env var. The
internal image sets it in its Dockerfile (
npm run build:internal). - Regenerate the agent index with the external (default) target so the
committed public artifacts never leak an internal-only section:
npm run llms && npm run agents.
The matrix logic is covered by deploy.config.test.mjs.
- Keep the generated-file commit separate from your own work — the hook already does this for you. Do not squash it away; it is what makes the generated diff reviewable.