diff --git a/AGENTS.md b/AGENTS.md index 93eb0b866..eed384dfa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,36 +1,90 @@ -# Agent Notes — JGengine +# JGengine Agent Guide -Operational facts discovered the hard way. Update this when you learn something that would have saved time. +Genre-agnostic pure-TypeScript game engine SDK plus its agent skills. Published packages: `packages/*` (npm, Apache-2.0); everything else private. Repo map, package table, website story: `README.md`. -## Cheap workers do the dumb work +## Rules that always apply -Read the **`fan-out`** skill. Cost first: a worker must save more than it costs — delegate substantial legs only (full gate, shoot, big-diff ships, bulk sweeps); small edits, small ships, and all waiting stay inline. A handful of workers per conversation, never one per action. Details: `.claude/skills/fan-out/SKILL.md` and root `CLAUDE.md`. +- **Editor-first for scene, placement, and asset work.** Building or changing what's in a world — placing spawns/objects/props, laying paths/roads/zones, painting terrain/materials, scattering foliage, authoring assets — is done **through the scene editor** (its RPC/CLI `bun packages/editor/src/mcp/cli.ts` or the GUI) and saved into the scene document (`editor.scene.json`), consumed at runtime by engine primitives (`` etc.). The builder should make this easy and look good with zero tuning. When the editor *can't* do something you need, **file a `[FEATURE]` issue for the missing editor/engine capability first**, then fall back to code and note the gap in the PR. Never hardcode level geometry, waypoint arrays, or bespoke per-game placement/render code when the editor could own it — that's the smell (see Design principles → "Author scenes in the editor"). +- **Every session is its own ephemeral cloud container.** The **main session** works directly on its assigned `claude/...` branch — no worktrees, no branch juggling. Commit and push early: `git push -u origin ` on its own line (never piped through a filter — a non-zero grep silently drops the push). `warn-unpushed` Stop hook catches strandings. Exception, and only here: parallel **shipping subagents** each run in their own isolated git worktree (`Agent({ isolation: "worktree" })`) so N tasks ship N PRs at once without stomping the shared tree — see `fan-out`. Main never juggles worktrees; the subagents do, and they auto-clean. +- **Ship = push → PR → subscribe → stop. Never merge.** When work is done and clean: push, open the PR (`create_pull_request`, ready for review), `subscribe_pr_activity`, report the link, **end the turn** — no waiting or polling. **One PR per branch, ever**: before creating, check none exists (`list_pull_requests` with `head`); if it does, the push already updated it — never open a second. +- **Silence is green.** The subscription delivers CI failures as chat events (PRs run only the ~30s `quick` job; the local gate proved the rest). Failure event → fix on the **same branch**, push, end turn. Never `merge_pull_request`/`enable_pr_auto_merge` unless the user asked this session. Never arm `send_later`/triggers/remote sessions to babysit CI. +- **The user owns merging.** PRs sit parked until they say so in chat. When asked to merge: squash-merge, report, done — no post-merge babysitting of `main` (a red `main` surfaces on the next PR; fix forward from `origin/main` on a fresh branch). One exit before green: a red run whose fix lives outside the repo — report it, hand off as a browser-agent prompt (see Communication), stop. +- **One task, one PR — never chunk.** A task ships as a single PR, however big. Never slice one request into separately-PR'd parts "to keep them small." Keep working the same branch until the whole task is done, then ship once. +- **New task in the same session = fresh branch off `origin/main`, new PR.** The previous branch stays parked under its PR — never add to it, never reset it. `git fetch origin main && git checkout -b claude/ origin/main`. Many small parked PRs are the steady state. +- **Git ceremony happens once per task, at the end.** No mid-task branch resets, restarts, or checkouts. Never stack new work on merged history — that's where conflicts came from; the session-start hook restarts a clean branch from `origin/main` automatically. The only mid-task restart is a fix-forward after a red merge, and even that is one restart. +- **Claim an issue before working it.** On starting real work on a tracked issue, comment `Working on this in claude/` (`add_issue_comment`), once, up front. Skip for throwaway spikes. +- **A fixed issue must be closed.** Put `Closes #N` in the PR body; if it won't auto-close (cross-repo, etc.), close it yourself (`issue_write`) with a one-line reason pointing at the PR. +- **"Done" is a claim, not a fact — verify before you repeat it.** A prior session's brief, handoff note, commit message, or your own memory saying something is complete/merged/fixed is unverified until you've opened the actual file/diff/issue checklist yourself. Never relay another session's completion claim to the user without checking it first — that's how a half-done item (seam built, thinning never done) gets reported as done twice. Any non-trivial `critique-combined.md` / audit item gets its own GitHub issue with acceptance criteria before it's called resolved; a merged PR with no issue behind it is not independently checkable later — file one retroactively rather than trust the commit message. +- **Issues: telegraph style, three headings, cold reader.** `## Problem` (first line = what's missing + why, never buried) / `## Context` (bullets a reader without your session needs — what exists, links) / `## Suggested scope` (numbered, primitive-level, no essays). +- **Docs ship in the same PR as the change.** Any change to public API, a workflow, a convention, or tooling updates everything that teaches it — affected `.claude/skills/*` (+ `bun run gen:skill-api` when exports changed), `CLAUDE.md` / `AGENTS.md` (keep them identical), script `--help`/README. Grep the skills for the changed command/API/flag before shipping; a stale skill is worse than none. +- **No freestanding docs — ever.** Never create decision records, ADRs, architecture notes, or any stray markdown under `docs/` or elsewhere. Those rot fast and freeze patterns nobody asked to keep; only user/maintainer efficiency matters. A decision worth keeping is a sentence in `CLAUDE.md`, a skill, or the nearest `README.md` (existing ones only — creating a new README needs the user to ask); a plan lives in the issue/PR that finishes it, and dies when it merges. +- **Engine gaps and improvement ideas are never papercuts.** Note and keep going; file as issues in one pass at session end (or before a big implementation). +- **Log papercuts the moment they happen.** Workflow friction only (retried call, dead-end command, misleading error, steering-wrong wording): `bun run papercut -m "doing X → Y got in the way"` right then — rides along in whatever ships next. Never engine gaps (those are issues), not `CHANGELOG.md`, not a bug issue. `/papercut` sweeps a session, user-triggered only. +- **The user owns release timing.** Merging to `main` can trigger npm publish (`publish.yml` ships any `@jgengine/*` version not yet on npm via `npm publish --access public` — not `bun publish`, which skips `~/.npmrc` auth). Never bump a version to force a release unless asked. +- **Layering is one-directional.** `core` imports nothing. `ws`/`sql` import only core. `react` adds React; `convex` adds Convex + React; `node` adds Node builtins + `ws`; `shell` adds React + three.js and is the only package that renders. `editor` sits on `shell`/`react`/`core`. `assets` and `github` sit outside the chain (data packages usable by games and `shell`, never by `core`). Never lower→higher; never core→React/Convex/three.js/browser/game. +- **Extracting SDK primitives must not change how a game plays.** Extract the reusable core *behind* a feature; confirm before cutting anything a player sees. +- **Ports copy behavior and data, never implementation.** The source is a spec: harvest numbers, tables, layouts, palettes, formulas, feel — then rebuild every system on this engine's primitives. Never transplant its functions, renderers, or canvas/DOM hacks; those import its debt. If our engine lacks the seam, build it here or file the gap. Assets under a permissive license may be copied as-is. +- **Visual work is judged by eye, harshly.** Any "make it look better" task on a world/terrain/environment starts with a screenshot — prefer `bun run drive --mode editor --shot name` over `--mode play` for reviewing world/spawn/layout content: layers and markers stay readable, and the camera snaps to a fixed frame (`--rpc '{"method":"camera_goto","id":""}'`, or select the object/asset and press `F`) so a before/after pair shares the exact same framing instead of two different free-camera angles. Reserve `--mode play`/`bun run shoot --mode play` for the final "does it feel shipped" pass. Look at it and call it honestly — flat untextured ground and default lighting "doesn't look like a game" and you say so. Then use the whole art stack — terrain texture/variation, materials, lighting/daylight, sky/fog, post-processing, vegetation density, props and landmarks — and re-shoot until it reads like a shipped game. `summarizeEnvironment` proves content exists; only your eyes prove it looks good. Milestone shots, not per-tweak loops; before/after in the PR body. Full loop: `jgengine-ui` → "Visual quality bar". +- **Upstream by default — game-local is the tagged exception.** Every edit inside `Games/*` passes one litmus first: *would game N+2 hit this?* Yes → the change lives upstream (engine primitive, default, catalog metadata, editor capability, or doc) and the game merely consumes it. This includes edits that don't feel like fixes: any compensation for a wrong default — rotating a backward-spawned model, nudging an offset, hand-tuning a magic number — IS a downstream patch (the backward bookcase's upstream fix is the asset catalog knowing canonical facing, not a rotation in game code). Before touching game code, name the upstream home in one line; if it exists, fix it there (or file the gap and note it in the PR). Game-local only when the user says so ("keep it in the game") or it's genuinely this-game content/feel. +- **Build reusable the first time.** New capability built for one consumer (a game, the editor, a script) lands as a shared primitive with that consumer as its first adopter — never as private code to "extract later." The editor is the standing example: its cameras are `shell` components games import; anything it grows (placement, gizmos, snapping) must be built the same way. If you're writing behavior inside `packages/editor` or `Games/*` that a second consumer could plausibly want, stop and put the core in `core`/`shell`/`react` first. +- **Credit borrowed work in the same PR.** Built from / inspired by / ported from someone's project, prompt, or design → record in [`CREDITS.md`](CREDITS.md) **and** surface to players: the game's HUD **and** its website page (`credit` field in `apps/web/src/content/games.ts`, rendered by `Credit.tsx`). Use their real GitHub/X avatar (`https://unavatar.io/x/`) with a fallback. +- **Never Kenney — repo-wide.** Kenney.nl assets (3D kits, 2D icon/UI packs, audio) are barred from JGengine: the author declined use. Never add, pull, re-index, alias, credit, or reference any `kenney-*` source anywhere — engine, games, docs, or scenes. For CC0 3D use **Quaternius** (broadest low-poly library) or **KayKit**; for icons use game-icons.net; for PBR use ambientCG. Applies to every future session. -## Publishing +## Design principles -- **Use `npm publish --access public`, not `bun publish`.** `bun publish` does not read `~/.npmrc` auth tokens; it fails with "missing authentication" even when `NPM_TOKEN` is written to `.npmrc`. -- **The only `workspace:*` dep is in `packages/node` devDependencies** (`@jgengine/sql`). npm strips devDependencies before packing, so `npm publish` never sees it. -- **The publish workflow must trigger on its own file changes.** Include `.github/workflows/publish.yml` in the `on.push.paths` filter; otherwise fixing the pipeline requires a dummy change to package source. -- **Already-published versions are skipped** by the `npm view` guard, so re-pushing the workflow is safe. +Judge every engine change against three axes — extensibility, modularity, scale — before writing it. -## Skills +- **Extend through seams, never edits.** New capability arrives through an existing plug-in point (transport pipe, structural interface, data catalog, auto-registering glob), not a new branch in engine code. If the seam is missing, build it first. An engine file edited for every new variant is the smell — turn the variant into data or a registration point. +- **Modules are deep, boundaries are narrow.** Each package owns one concern behind the smallest surface. Cross-layer deps are structural, not nominal — `sql` accepts any pool shape instead of importing `pg`, `ws` takes any transport pipe. If a feature needs another module's internals, move the seam, don't reach through it. +- **Public APIs express game intent, never engine mechanics.** Callers choose `candidates`, `avoid`, `distanceBias: "far"`, an injected `random` — never internal rolls, weighting exponents, store plumbing, or module paths. Keep deterministic/performance machinery behind the boundary. Replacing a leaky API: add the semantic form, migrate engine + skill examples immediately, keep a deprecated overload only if consumers would break. +- **Build for the next ten games, not this one.** A primitive earns `packages/*` by being genre-agnostic; per-game special cases never enter engine code. Content is data — game N+1 is data plus glue. When close, pick the option that leaves more seams open. +- **Author scenes in the editor, render them generically — never hardcode level geometry.** Paths, foliage, terrain, and gameplay spots are **editor-document data** (`editor.scene.json` / `editorLayers`), rendered at runtime by engine primitives (``/`` drape paths and instance foliage from the document). Both the render *and* the gameplay read that one document — enemy waypoints come from a `route` path, tower plots from markers — so coordinates live once, editable in the 3D editor like a scene in Unreal. Hand-rolling a per-segment path mesh or pasting waypoint arrays into game code is the smell; if a scene kind can't render from the document yet, add the engine seam, don't hardcode it in the game. **Enforced by `check-content-gate`** (in `check-types`): a game with hard-coded world geometry must render from an editor document, and dense hand-placed coordinate literals fail the lint. Two shrinking baselines (`scripts/content-gate-baseline.json` offender games, `scripts/coordinate-literal-baseline.json` offender files) pin today's offenders so `main` stays green and only lose entries as games migrate; after a migration reseed with `bun run check-content-gate --update`. Procedural/geometry-free games are declared in the script's `EXEMPT` map. +- **Scale is a default, not a feature.** Core primitives assume many entities, many players, a hosted multiplayer backend from day one: no unbounded per-frame scans or allocation storms on hot paths, state that serializes cleanly, nothing single-player-only. Works at 10 but not 10,000 → not done. +- **Prefer removing a dependency to managing one.** `core`'s zero deps is the pattern. Every new dep in a published package is a scaling liability needing a reason the language and existing layers can't give. +- **Engine chrome is composable, never imposed.** The SDK ships defaults, not a mandatory look. No feature may pin a fixed-position overlay onto every game (the forced settings gear was the smell) — surface it as a headless component the game places itself. Give a placement hook, ship a good default skin, let the game decide where and whether it appears. +- **A game has exactly one main menu, and the game builds it.** Each game authors its own title/attract screen and in-game menus (see `jgengine-ui`). `apps/web`'s `/games/` is a bare loader: mounts `/play`, shows only a spinner until ready — never its own title, tagline, or CTA (that was the two-main-menus bug, #690). -- **Human interface outside this monorepo: one sentence** — `Make a game that … with jgengine`. Not “run these CLI steps.” The CLI is for agents (and optional power users); create auto-installs skills when the agent scaffolds. From this source monorepo, build `packages/jgengine` first or force the published CLI with `npm exec --yes --package=jgengine@latest -- jgengine skills`; npm otherwise prefers the unbuilt local workspace bin on Windows. -- **Pushing to `main` does NOT auto-update existing skill installs.** Agents re-run `npx jgengine skills -p` (or `-g`) / `npx skills update` if needed. +## Stack -## Verification & screenshots +- **bun workspaces.** `packages/*` (`@jgengine/*` + the `jgengine` CLI, imported by path) · `Games/*` (private, source-consumed, one dir per game, built via `harvest-game`) · `apps/*` · `examples/*`. +- **apps.** `dev` = Vite runner + screenshot target; games auto-register from `Games/*` via a glob in `apps/dev/src/main.tsx` (no manual registry). Root `bun dev` = the website, every game playable at `/games/` (embeds the runner from its `/play` mount). `bun run dev:runner` = runner alone; `bun run games:` = one game standalone. `desktop` = Tauri; `web` = jgengine.com. +- **New game:** `bun run new:game --name "Title"` scaffolds the full `check-game-shape`-compliant harness (incl. the root `games:` script) as a booting game — never hand-copy another game's harness. Menu-gated visual checks: `bun run drive --click "TEXT" --shot name --key KeyW:2500` (`--key CODE:MS` holds a key; see `jgengine-verify`). +- **Compiler is `tsgo`** (`@typescript/native-preview`), not `tsc`. Strict TS everywhere; no `any` in engine code. +- **Skills in `.claude/skills/` are the spec — invoke them** (auto-surface every session; never work game code from memory or by copying games). Outside this monorepo the human interface is one sentence: *Make a game that … with jgengine* (CLI is for agents). Merging skills to `main` does **not** refresh already-installed skill copies — recovery is `npx jgengine skills -p` (or `-g`). From this source tree, build `packages/jgengine` first or force the published bin with `npm exec --yes --package=jgengine@latest -- jgengine skills` (npm otherwise prefers the unbuilt local workspace bin on Windows). A top-level `skills/` dir is invisible and fails the gate; descriptions stay ≤15 words. **`SKILL.md` bodies only ever shrink** — every session pays their intake cost, so a **skill-size ratchet** (`check-skills`, baseline `scripts/skill-size-baseline.json`) fails the build if a skill grows past its recorded line count, and caps new skills at 250 lines. To slim one, move encyclopedic prose out of `SKILL.md` into the domain's `reference.md` / generated `api.md` / `capabilities.md` (loaded on demand, not every session), then rebaseline with `bun run gen:skill-sizes`. `check-types` validates them against the real API surface: each domain carries a generated `api.md` (`bun run gen:skill-api`; new/changed exports need a JSDoc description, pre-existing debt in `scripts/api-doc-baseline.json`). An **orphan gate** fails the build when a new exported symbol has no game adopter and no skill example — surface it in the domain `SKILL.md`, use it, or mark `@internal` (baseline: `scripts/api-orphan-baseline.json`). That baseline is a **shrink-only ratchet** (`bun run check-orphan-ratchet`): the committed set must stay a subset of `origin/main`'s — you can only remove entries (by adopting/documenting/`@internal`-ing the primitive), never add. Baselining a fresh orphan to silence the gate is rejected; each new orphan must be offset by removing an existing one. Discovery side: tag a primitive `@capability `, `bun run gen:capabilities` writes it into the domain's `capabilities.md` (an intent→import table builds consult before hand-rolling). Start with the router, then read only the domains intake selects: -- **Run the verify ladder through `fan-out` workers**, not the frontier model (`check-types` · `bun test` · `shoot`). -- **A hung `bun run shoot` is never re-run in the foreground.** Chromium/Playwright on heavy WebGL scenes hangs, crashes the GPU, or emits corrupt output. Report it once, fall back to the `summarizeEnvironment` world test to prove the scene resolved, and retry the shot only if the user asks. Full ladder: the `jgengine-verify` skill. -- **Silently-unstyled game UI means a missing `@source` entry** in `apps/dev/src/index.css` (or the game's own `index.css`) — Tailwind never scanned the HUD's classes, so they compile to nothing. + | Skill | Role | + | --- | --- | + | `jgengine` | Main skill: intake, foundation (shape, runtime, catalogs), selective domain routing | + | `jgengine-world` | World runtime and generated environments | + | `jgengine-combat` / `jgengine-gameplay` | Combat and game systems | + | `jgengine-multiplayer` | Networking, authority, persistence seams | + | `jgengine-ui` / `jgengine-assets` | Interface and asset surfaces | + | `jgengine-verify` | Browserless scene gate; shoot last | +- **Harvest scope differs by skill:** `harvest-game` = minimal probe to surface gaps fast; `harvest-full-game` = the complete requested game. Don't ship a slice when a full build was asked, or gold-plate a probe. +- **This is the engine repo:** fix engine gaps and doc errors directly. The only issues filed from inside it are the `[FEATURE]` gap issues from `harvest-game`. -## Cloud sessions +## Delegation & verification -- **Every session is an isolated cloud container on its own `claude/...` branch.** No worktrees. Push early — a reclaimed container takes unpushed commits with it. -- **Never commit on top of a squash-merged branch.** That is where the recurring merge conflicts came from. The session-start hook restarts a clean, already-merged branch from `origin/main` automatically; by hand: `git checkout main && git pull`, then start the next change on a fresh `claude/...` branch. Don't leave HEAD on a dead session branch that only matches main by reset. -- **Ship = push → PR (ready for review) → PR checks green → stop. Never merge.** The user merges by asking in chat ("merge my PRs"); `merge_pull_request`/`enable_pr_auto_merge` only on that request. New task in the same session starts a fresh `claude/...` branch off `origin/main` and its own PR. -- **GitHub goes through the MCP tools** (`create_pull_request`, `merge_pull_request`, `enable_pr_auto_merge`, `add_issue_comment`) — there is no `gh` CLI in cloud containers. +Delegation policy lives in the **`fan-out`** skill (`.claude/skills/fan-out`) — read it whenever a turn carries multiple independent tasks or one large multi-leg task. In short: the trigger is **independence, not just size** — ≥2 units that don't need each other's output fan out and run in parallel by default. "Do all the issues" is N parallel ships (one subagent per issue, each in its own `isolation: "worktree"`, each branching off `origin/main` → N PRs), never a list ground through one at a time in the main loop. One small single thing stays inline — this session. Main only plans, decomposes, and judges; `model` is set explicitly on every subagent (Haiku = run-and-report verify ladder; Sonnet = the parallel ship motions and fixing failures; Opus = scouts and judgment legs; Fable never runs a leg). Screenshots (`shoot`/`drive`) and pushing them to `pr-shots` aren't a fan-out leg — run both directly, no subagent, never one subagent per file (a hung or dropped "upload worker" per screenshot is the exact smell this rule kills). Announce workers on a 🤖 line, judge their output, never dump raw worker text. Convert other agent setups: `convert-to-fanout`. -## Environment +`bun run gate` = the full local verdict (`agent:preflight` → `check-types` → `test:all` → `build`), run via workers. `agent:preflight` catches a half-finished `bun install` or a `bun.lock` out of sync with manifests (it replays CI's frozen install against a cold copy — a warm `node_modules` masks drift, so version bumps re-run `bun install` and commit `bun.lock`). `ship:preflight` (gate + dirty-tree/stale-`origin/main`/no-op check) runs immediately before commit/push/PR. Every gate is hard-bounded by `scripts/guard.ts` (kills the tree, exits 124); a hang is a fast loud 124 — diagnose it, never re-run and wait. Tests are always the guard-bounded scripts (`bun run test`, `bun run test:all`) or an explicit `bun test packages ...`; a bare `bun test` scans the whole tree unbounded and hangs. For scene/HUD work follow `jgengine-verify`: prove content with `summarizeEnvironment` assertions in `bun test`; `bun run shoot --mode ui|play` is a final human glance, never the inner loop. **A hung `shoot` is never re-run in the foreground** (WebGL/Chrome hang or corrupt PNG) — report once, trust the world test, retry only if the user asks. Unstyled game UI = a missing `@source` in `apps/dev/src/index.css`. Menu-gated games declare `capture: { play: [...] }` so `--mode play` lands on live gameplay. Visual PRs embed screenshots in the PR body (`git push` to the `pr-shots` branch, never GitHub MCP file-content tools — base64 in context is expensive; embed raw URLs). -- **Lefthook is missing in this shell** but git commits still succeed; the warning is harmless. +## Communication + +**Telegraph style everywhere** — chat, statuses, quips, briefs. Fragments beat sentences; cut courtesies, hedges, recaps, transitions, play-by-play. Target ~20% of polite prose. If a word gives the reader nothing, cut it. + +- **Hard cap: a normal reply fits one phone screen (~8 lines).** The user reads on mobile. One reply per turn — no interleaved narration between tool calls. If it doesn't change what the user does next, don't write it. +- Result + decision only. Reasoning stays internal unless asked. +- **Waits are silent.** No "Waiting on…" or per-leg pass/fail lines while workers run. One line when work starts (the 🤖 line covers it), then nothing until all legs resolve — report the combined verdict once, or the first failure. +- Status = one line, PR/issue numbers **always hyperlinked**: `PR [#297](https://github.com/Noisemaker111/jgengine/pull/297) merged, CI green 🚀`. +- Next move = last line, own line, owner explicit: `Next: you add VERCEL_TOKEN` vs `Next: shipping Batch 2`. +- **Out-of-repo fixes are handed off as a prompt, never instructions.** When the next move lives outside the repo (dashboard, secrets, DNS, third-party accounts), give a fenced code block: one standalone prompt the user pastes into a browser agent that knows nothing about this repo — goal, every non-discoverable fact (repo/org name, exact setting names, what the value is for, how to verify done), no click-by-click steps. +- One naming scheme: parallel rounds = **Batch N**, workers job-named (**shell-input worker**), never lettered. +- Deliverables are exempt: a plan, spec, or harvest report is self-contained and runs as long as it needs. + +## Style + +- **Repetitive edits are one command, never N manual edits.** The same small change across ≥3 files (or ≥5 spots in one file) means build one regex find-and-replace and run it everywhere: `rg -l '' | xargs sed -i -E 's///g'` (or `perl -pi -e` for multiline/lookarounds). Dry-run with `rg ''` first to see every hit, run the replace, then `git diff --stat` to confirm scope. Hand-editing the same character in five files is the smell. +- No code comments. Rename, extract, or encode in types instead. +- Dense files: catalogs and content tables stay one file per domain, not scattered micro-modules. +- Agent-invokable skill descriptions: ~10 words (cap 15), lead with *why*. Triggers and mechanics live in the skill body. diff --git a/CLAUDE.md b/CLAUDE.md index a5c71628b..eed384dfa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,11 +16,11 @@ Genre-agnostic pure-TypeScript game engine SDK plus its agent skills. Published - **A fixed issue must be closed.** Put `Closes #N` in the PR body; if it won't auto-close (cross-repo, etc.), close it yourself (`issue_write`) with a one-line reason pointing at the PR. - **"Done" is a claim, not a fact — verify before you repeat it.** A prior session's brief, handoff note, commit message, or your own memory saying something is complete/merged/fixed is unverified until you've opened the actual file/diff/issue checklist yourself. Never relay another session's completion claim to the user without checking it first — that's how a half-done item (seam built, thinning never done) gets reported as done twice. Any non-trivial `critique-combined.md` / audit item gets its own GitHub issue with acceptance criteria before it's called resolved; a merged PR with no issue behind it is not independently checkable later — file one retroactively rather than trust the commit message. - **Issues: telegraph style, three headings, cold reader.** `## Problem` (first line = what's missing + why, never buried) / `## Context` (bullets a reader without your session needs — what exists, links) / `## Suggested scope` (numbered, primitive-level, no essays). -- **Docs ship in the same PR as the change.** Any change to public API, a workflow, a convention, or tooling updates everything that teaches it — affected `.claude/skills/*` (+ `bun run gen:skill-api` when exports changed), `CLAUDE.md`, script `--help`/README. Grep the skills for the changed command/API/flag before shipping; a stale skill is worse than none. +- **Docs ship in the same PR as the change.** Any change to public API, a workflow, a convention, or tooling updates everything that teaches it — affected `.claude/skills/*` (+ `bun run gen:skill-api` when exports changed), `CLAUDE.md` / `AGENTS.md` (keep them identical), script `--help`/README. Grep the skills for the changed command/API/flag before shipping; a stale skill is worse than none. - **No freestanding docs — ever.** Never create decision records, ADRs, architecture notes, or any stray markdown under `docs/` or elsewhere. Those rot fast and freeze patterns nobody asked to keep; only user/maintainer efficiency matters. A decision worth keeping is a sentence in `CLAUDE.md`, a skill, or the nearest `README.md` (existing ones only — creating a new README needs the user to ask); a plan lives in the issue/PR that finishes it, and dies when it merges. - **Engine gaps and improvement ideas are never papercuts.** Note and keep going; file as issues in one pass at session end (or before a big implementation). - **Log papercuts the moment they happen.** Workflow friction only (retried call, dead-end command, misleading error, steering-wrong wording): `bun run papercut -m "doing X → Y got in the way"` right then — rides along in whatever ships next. Never engine gaps (those are issues), not `CHANGELOG.md`, not a bug issue. `/papercut` sweeps a session, user-triggered only. -- **The user owns release timing.** Merging to `main` can trigger npm publish (`publish.yml` ships any `@jgengine/*` version not yet on npm). Never bump a version to force a release unless asked. +- **The user owns release timing.** Merging to `main` can trigger npm publish (`publish.yml` ships any `@jgengine/*` version not yet on npm via `npm publish --access public` — not `bun publish`, which skips `~/.npmrc` auth). Never bump a version to force a release unless asked. - **Layering is one-directional.** `core` imports nothing. `ws`/`sql` import only core. `react` adds React; `convex` adds Convex + React; `node` adds Node builtins + `ws`; `shell` adds React + three.js and is the only package that renders. `editor` sits on `shell`/`react`/`core`. `assets` and `github` sit outside the chain (data packages usable by games and `shell`, never by `core`). Never lower→higher; never core→React/Convex/three.js/browser/game. - **Extracting SDK primitives must not change how a game plays.** Extract the reusable core *behind* a feature; confirm before cutting anything a player sees. - **Ports copy behavior and data, never implementation.** The source is a spec: harvest numbers, tables, layouts, palettes, formulas, feel — then rebuild every system on this engine's primitives. Never transplant its functions, renderers, or canvas/DOM hacks; those import its debt. If our engine lacks the seam, build it here or file the gap. Assets under a permissive license may be copied as-is. @@ -50,7 +50,7 @@ Judge every engine change against three axes — extensibility, modularity, scal - **apps.** `dev` = Vite runner + screenshot target; games auto-register from `Games/*` via a glob in `apps/dev/src/main.tsx` (no manual registry). Root `bun dev` = the website, every game playable at `/games/` (embeds the runner from its `/play` mount). `bun run dev:runner` = runner alone; `bun run games:` = one game standalone. `desktop` = Tauri; `web` = jgengine.com. - **New game:** `bun run new:game --name "Title"` scaffolds the full `check-game-shape`-compliant harness (incl. the root `games:` script) as a booting game — never hand-copy another game's harness. Menu-gated visual checks: `bun run drive --click "TEXT" --shot name --key KeyW:2500` (`--key CODE:MS` holds a key; see `jgengine-verify`). - **Compiler is `tsgo`** (`@typescript/native-preview`), not `tsc`. Strict TS everywhere; no `any` in engine code. -- **Skills in `.claude/skills/` are the spec — invoke them** (auto-surface every session; never work game code from memory or by copying games). A top-level `skills/` dir is invisible and fails the gate; descriptions stay ≤15 words. **`SKILL.md` bodies only ever shrink** — every session pays their intake cost, so a **skill-size ratchet** (`check-skills`, baseline `scripts/skill-size-baseline.json`) fails the build if a skill grows past its recorded line count, and caps new skills at 250 lines. To slim one, move encyclopedic prose out of `SKILL.md` into the domain's `reference.md` / generated `api.md` / `capabilities.md` (loaded on demand, not every session), then rebaseline with `bun run gen:skill-sizes`. `check-types` validates them against the real API surface: each domain carries a generated `api.md` (`bun run gen:skill-api`; new/changed exports need a JSDoc description, pre-existing debt in `scripts/api-doc-baseline.json`). An **orphan gate** fails the build when a new exported symbol has no game adopter and no skill example — surface it in the domain `SKILL.md`, use it, or mark `@internal` (baseline: `scripts/api-orphan-baseline.json`). That baseline is a **shrink-only ratchet** (`bun run check-orphan-ratchet`): the committed set must stay a subset of `origin/main`'s — you can only remove entries (by adopting/documenting/`@internal`-ing the primitive), never add. Baselining a fresh orphan to silence the gate is rejected; each new orphan must be offset by removing an existing one. Discovery side: tag a primitive `@capability `, `bun run gen:capabilities` writes it into the domain's `capabilities.md` (an intent→import table builds consult before hand-rolling). Start with the router, then read only the domains intake selects: +- **Skills in `.claude/skills/` are the spec — invoke them** (auto-surface every session; never work game code from memory or by copying games). Outside this monorepo the human interface is one sentence: *Make a game that … with jgengine* (CLI is for agents). Merging skills to `main` does **not** refresh already-installed skill copies — recovery is `npx jgengine skills -p` (or `-g`). From this source tree, build `packages/jgengine` first or force the published bin with `npm exec --yes --package=jgengine@latest -- jgengine skills` (npm otherwise prefers the unbuilt local workspace bin on Windows). A top-level `skills/` dir is invisible and fails the gate; descriptions stay ≤15 words. **`SKILL.md` bodies only ever shrink** — every session pays their intake cost, so a **skill-size ratchet** (`check-skills`, baseline `scripts/skill-size-baseline.json`) fails the build if a skill grows past its recorded line count, and caps new skills at 250 lines. To slim one, move encyclopedic prose out of `SKILL.md` into the domain's `reference.md` / generated `api.md` / `capabilities.md` (loaded on demand, not every session), then rebaseline with `bun run gen:skill-sizes`. `check-types` validates them against the real API surface: each domain carries a generated `api.md` (`bun run gen:skill-api`; new/changed exports need a JSDoc description, pre-existing debt in `scripts/api-doc-baseline.json`). An **orphan gate** fails the build when a new exported symbol has no game adopter and no skill example — surface it in the domain `SKILL.md`, use it, or mark `@internal` (baseline: `scripts/api-orphan-baseline.json`). That baseline is a **shrink-only ratchet** (`bun run check-orphan-ratchet`): the committed set must stay a subset of `origin/main`'s — you can only remove entries (by adopting/documenting/`@internal`-ing the primitive), never add. Baselining a fresh orphan to silence the gate is rejected; each new orphan must be offset by removing an existing one. Discovery side: tag a primitive `@capability `, `bun run gen:capabilities` writes it into the domain's `capabilities.md` (an intent→import table builds consult before hand-rolling). Start with the router, then read only the domains intake selects: | Skill | Role | | --- | --- | @@ -67,7 +67,7 @@ Judge every engine change against three axes — extensibility, modularity, scal Delegation policy lives in the **`fan-out`** skill (`.claude/skills/fan-out`) — read it whenever a turn carries multiple independent tasks or one large multi-leg task. In short: the trigger is **independence, not just size** — ≥2 units that don't need each other's output fan out and run in parallel by default. "Do all the issues" is N parallel ships (one subagent per issue, each in its own `isolation: "worktree"`, each branching off `origin/main` → N PRs), never a list ground through one at a time in the main loop. One small single thing stays inline — this session. Main only plans, decomposes, and judges; `model` is set explicitly on every subagent (Haiku = run-and-report verify ladder; Sonnet = the parallel ship motions and fixing failures; Opus = scouts and judgment legs; Fable never runs a leg). Screenshots (`shoot`/`drive`) and pushing them to `pr-shots` aren't a fan-out leg — run both directly, no subagent, never one subagent per file (a hung or dropped "upload worker" per screenshot is the exact smell this rule kills). Announce workers on a 🤖 line, judge their output, never dump raw worker text. Convert other agent setups: `convert-to-fanout`. -`bun run gate` = the full local verdict (`agent:preflight` → `check-types` → `test:all` → `build`), run via workers. `agent:preflight` catches a half-finished `bun install` or a `bun.lock` out of sync with manifests (it replays CI's frozen install against a cold copy — a warm `node_modules` masks drift, so version bumps re-run `bun install` and commit `bun.lock`). `ship:preflight` (gate + dirty-tree/stale-`origin/main`/no-op check) runs immediately before commit/push/PR. Every gate is hard-bounded by `scripts/guard.ts` (kills the tree, exits 124); a hang is a fast loud 124 — diagnose it, never re-run and wait. Tests are always the guard-bounded scripts (`bun run test`, `bun run test:all`) or an explicit `bun test packages ...`; a bare `bun test` scans the whole tree unbounded and hangs. For scene/HUD work follow `jgengine-verify`: prove content with `summarizeEnvironment` assertions in `bun test`; `bun run shoot --mode ui|play` is a final human glance, never the inner loop. Unstyled game UI = a missing `@source` in `apps/dev/src/index.css`. Menu-gated games declare `capture: { play: [...] }` so `--mode play` lands on live gameplay. Visual PRs embed screenshots in the PR body (`git push` to the `pr-shots` branch, never GitHub MCP file-content tools — base64 in context is expensive; embed raw URLs). +`bun run gate` = the full local verdict (`agent:preflight` → `check-types` → `test:all` → `build`), run via workers. `agent:preflight` catches a half-finished `bun install` or a `bun.lock` out of sync with manifests (it replays CI's frozen install against a cold copy — a warm `node_modules` masks drift, so version bumps re-run `bun install` and commit `bun.lock`). `ship:preflight` (gate + dirty-tree/stale-`origin/main`/no-op check) runs immediately before commit/push/PR. Every gate is hard-bounded by `scripts/guard.ts` (kills the tree, exits 124); a hang is a fast loud 124 — diagnose it, never re-run and wait. Tests are always the guard-bounded scripts (`bun run test`, `bun run test:all`) or an explicit `bun test packages ...`; a bare `bun test` scans the whole tree unbounded and hangs. For scene/HUD work follow `jgengine-verify`: prove content with `summarizeEnvironment` assertions in `bun test`; `bun run shoot --mode ui|play` is a final human glance, never the inner loop. **A hung `shoot` is never re-run in the foreground** (WebGL/Chrome hang or corrupt PNG) — report once, trust the world test, retry only if the user asks. Unstyled game UI = a missing `@source` in `apps/dev/src/index.css`. Menu-gated games declare `capture: { play: [...] }` so `--mode play` lands on live gameplay. Visual PRs embed screenshots in the PR body (`git push` to the `pr-shots` branch, never GitHub MCP file-content tools — base64 in context is expensive; embed raw URLs). ## Communication