diff --git a/.agents/skills/native-performance-loop/SKILL.md b/.agents/skills/native-performance-loop/SKILL.md index 5c3d1d041e..0097f81c4b 100644 --- a/.agents/skills/native-performance-loop/SKILL.md +++ b/.agents/skills/native-performance-loop/SKILL.md @@ -1,6 +1,6 @@ --- name: native-performance-loop -description: Run an autonomous ThreeNative native-performance optimization loop using existing harnesses, quick synthetic load tests, and representative real games. Profile bottlenecks, make focused changes, accept or reject measured experiments, and maintain an HTML benchmark dashboard. Use for sustained native runtime optimization, frame pacing, startup, memory, or thermal efficiency campaigns, including the Tempest (rain) campaign where the web Wasm engine must beat stock three.js with the same image. +description: Run an autonomous ThreeNative native-performance optimization loop using existing harnesses, quick synthetic load tests, and representative real games. Profile bottlenecks, make focused changes, accept or reject measured experiments, and keep a campaign tab in the one `pnpm dashboard` hub. Use for sustained native runtime optimization, frame pacing, startup, memory, or thermal efficiency campaigns, including the Tempest (rain) campaign where the web Wasm engine must beat stock three.js with the same image. --- # Native performance loop @@ -28,8 +28,8 @@ the developer to supervise each iteration. Creating or editing this skill does n Browser results are comparison or diagnosis, never substitute native evidence. Read the local `measure-steady-state-fps` and `probe-android-startup-and-heat` skills when their lanes apply. 4. Create or resume `/artifacts/native-performance-loop//`. Record the - contract and results described in [dashboard.md](references/dashboard.md), and create - `index.html` immediately, including an honest empty state. Choose workloads and targets + contract and results described in [dashboard.md](references/dashboard.md), and show them in a + `pnpm dashboard` tab immediately, including an honest empty state. Choose workloads and targets autonomously and state them. Default to a two-hour invocation budget, including setup and verification, unless the user supplied another duration or experiment limit. Reserve the last 20 minutes for confirmation, restoration, and delivery; do not launch work that cannot finish @@ -139,7 +139,7 @@ hypothesis; reaching a target does not establish a hardware limit. Do not instal scheduler or claim work continues after the agent stops. Before delivery, reproduce the best accepted build, rerun affected repository checks and native -scenarios, refresh `index.html`, and record unrun platforms honestly. Preserve the dashboard and +scenarios, update the campaign record, and record unrun platforms honestly. Preserve the record and raw artifacts outside a disposable task checkout. Follow the repository's PR and worktree cleanup rules within the session's existing authorization; no implicit push, merge or publication. Report any retained worktree's exact path, size and cleanup blocker. Finish with the dashboard link, diff --git a/.agents/skills/native-performance-loop/references/dashboard.md b/.agents/skills/native-performance-loop/references/dashboard.md index 2f76fe5a97..fdef73f062 100644 --- a/.agents/skills/native-performance-loop/references/dashboard.md +++ b/.agents/skills/native-performance-loop/references/dashboard.md @@ -1,7 +1,8 @@ -# Campaign state and HTML dashboard +# Campaign state and the dashboard tab -Generate this on invocation, not when installing the skill. The HTML is the developer's view of -one machine-readable campaign record, not a second manually maintained evidence report. +Create this on invocation, not when installing the skill. The developer's view is one tab of the +single KPI hub, `pnpm dashboard` (`scripts/perf-live.ts`, PRD-751), reading one machine-readable +campaign record. Do not generate a per-campaign HTML page or a second evidence report. ## Files and updates @@ -9,22 +10,21 @@ Use `/artifacts/native-performance-loop//` with: - `results.json`: versioned contract, environment identities, workloads, baselines, incumbent, experiment history, status, heartbeat, limits and next hypothesis. -- `index.html`: an offline, self-contained dashboard generated from `results.json`. - `raw//`: existing harness output, exit codes, sample data, profiles, screenshots, candidate patch/commit reference, build identities and exact reproduction commands. -Reuse a suitable existing report renderer if present. Otherwise generate a small HTML file with -inline CSS, embedded data and minimal browser JavaScript; save the small renderer alongside the -campaign only if needed to regenerate it reliably. No web framework, backend, CDN or new dependency. -It must open with `file://` without fetching JSON. Write JSON and HTML via temporary siblings and -atomic replacement, preserving the last readable snapshot if generation fails. +Reuse an existing hub tab when its reader fits the record (the Shadows vs Unreal tab reads a +campaign `results.json`). Otherwise add one: a small reader beside `scripts/shadow-perf/dashboard.ts` +that reads `results.json`, writes nothing and fails closed (missing = `pending`, malformed = +`invalid`), plus one `TABS` row and one route in `scripts/perf-live.ts`, with a test in +`scripts/__tests__/perf-live.spec.ts`. No web framework, CDN or new dependency. Write +`results.json` via a temporary sibling and atomic replacement, preserving the last readable record. -Create the dashboard before the first benchmark, showing “baseline pending,” never sample wins. -Refresh after each state transition and decision. During long commands, publish a timestamp and -heartbeat at least every 60 seconds using the existing gate status where available. The HTML must -show its snapshot time; when an old snapshot is open, say “reload for current progress” instead of -pretending it is live. If serving locally is already supported, optional refresh must preserve -filters. A stale heartbeat is “stale / check process,” never proof the worker is still active. +Add the tab before the first benchmark, showing “baseline pending,” never sample wins. Update the +record after each state transition and decision. During long commands, publish a timestamp and +heartbeat at least every 60 seconds using the existing gate status where available. The tab shows +the record's update time. A stale heartbeat is “stale / check process,” never proof the worker is +still active. Escape benchmark names, descriptions, commands and log content as text. If embedding JSON in an HTML script element, escape `<` as `\u003c` so data cannot terminate the script. Use textContent for @@ -73,8 +73,8 @@ FPS derived from percentiles or average several games into an unqualified perfor ## Verify and resume -Before handing off an HTML snapshot, open it in a browser when a browser tool is available. Check -that it opens offline, shows actual recorded values, distinguishes missing/provisional data, and +Before handing off, open the tab in `pnpm dashboard` in a browser when a browser tool is available. +Check that it shows actual recorded values, distinguishes missing/provisional data, and that filters, keyboard controls and evidence links work. A renderer with non-trivial logic needs one small runnable check covering metric direction, missing values and unsafe text. If no browser is available, check generated structure and links and mark visual inspection unverified. @@ -82,5 +82,5 @@ is available, check generated structure and links and mark visual inspection unv On resume, load the contract and history, verify ownership and process state, and reconcile the worktree with the recorded incumbent before editing. Check executable/workload/environment hashes; if any comparison dimension changed, create a new series and baseline instead of connecting its -points to the old curve. Preserve all previous decisions. Link to this dashboard in the PRD/PR and +points to the old curve. Preserve all previous decisions. Link to this tab in the PRD/PR and final response rather than copying its experiment log into another report. diff --git a/.claude/skills/ci-speed-loop/SKILL.md b/.claude/skills/ci-speed-loop/SKILL.md index bd3cc1a139..56379302c0 100644 --- a/.claude/skills/ci-speed-loop/SKILL.md +++ b/.claude/skills/ci-speed-loop/SKILL.md @@ -1,6 +1,6 @@ --- name: ci-speed-loop -description: Run ThreeNative's CI speed loop — measure how long a change takes to merge, name the biggest cost from the first failing jobs and queue wait, change one thing, re-measure the same window, record the iteration and refresh the HTML dashboard. Use when asked to speed up CI or the merge queue, work PRD-550, check progress toward the 30-minute merge goal, or refresh docs/ci-speed/index.html. Not for runtime FPS (perf-loop). +description: Run ThreeNative's CI speed loop — measure how long a change takes to merge, name the biggest cost from the first failing jobs and queue wait, change one thing, re-measure the same window, record the iteration. Use when asked to speed up CI or the merge queue, work PRD-550, check progress toward the 30-minute merge goal, or read the CI speed tab of `pnpm dashboard`. Not for runtime FPS (perf-loop). --- # CI speed loop @@ -20,11 +20,11 @@ node scripts/ci-merge-latency.mjs --since 2026-10-09 # measure only, pr node scripts/ci-speed-loop.mjs record --since 2026-10-09 --label "gating board" --notes "..." [--decision keep] node scripts/ci-speed-loop.mjs show # the ledger node scripts/ci-speed-loop.mjs discard --iteration 3 --reason "queue outage, 2 PRs only" -node scripts/ci-speed-loop.mjs report # regenerate the HTML only +pnpm dashboard # the CI speed tab renders the ledger ``` -`docs/ci-speed/ledger.json` is the record. `docs/ci-speed/index.html` is generated from it and opens with -`file://`. Never edit either by hand. Commit both with the change they measure. +`docs/ci-speed/ledger.json` is the record; the CI speed tab of `pnpm dashboard` renders it on each load +(PRD-751). Do not edit it by hand. Commit it with the change it measures. ## The turn diff --git a/.gitleaksignore b/.gitleaksignore index 834108da0f..7e21f9b70d 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -28,3 +28,8 @@ e2e871f1feac6fa0f7dc4d372f30d3cb586a8f72:packages/core/__tests__/warmup-default. # Verified 2026-10-11: PRD-595's slash-separated CellPrewarm counter names are prose, not a credential. 0c2f9e4b57382d980ef98424f6ac03b86e1d785b:docs/PRDs/strata-terrain-hardening/PRD-595-world-cells-splits-by-seam-and-survives-a-race-fuzz.md:generic-api-key:63 + +# Verified 2026-10-27: PRD-750 scripts/kit-loop.mjs line 45 is a KPI field identifier (k2ScenarioPct), not a credential. +f799f51aba375ce12df5eb43db2a7c69b65d96ae:scripts/kit-loop.mjs:generic-api-key:45 +# Verified 2026-10-27: line 31 of this file in commit 0fd2f4d57 quotes that same identifier in a comment; the full-history scan reads every fetched ref. +0fd2f4d574d82305373434bc8cf1c00600b3a36e:.gitleaksignore:generic-api-key:31 diff --git a/AGENTS.md b/AGENTS.md index 86e35c1c3a..f2f80f04e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,7 +81,7 @@ pnpm native:build # opt-in; downloads deps, compiles th pnpm native:verify:desktop # 300 native frames + a non-blank screenshot ``` -**CI.** The pre-push hook runs `pnpm ci:fast` drift checks only; it proves no runtime behaviour. `pnpm ci:local --full` runs the full board; `pnpm ci:local --affected --base origin/develop --target develop` runs what CI would select (dirty or unknown inputs fall back to full). A change touching only Markdown that no fixture consumes (not `AGENTS.md`/`CLAUDE.md`), or only the generated speed-loop record (`docs/ci-speed/ledger.json`, `docs/ci-speed/index.html`, `docs/ci-speed/runs/.jobs.json`), runs no product CI job. Ordinary PRs into develop use the complete candidate diff: CI-only changes run CI contracts, exact template changes run scaffolder contracts and affected templates, and shared executable inputs earn the thin review lane (PRD-731): scope, typecheck, lint, budgets, secrets scan, the unit tests that import a changed file, `test` and `build-artifacts` for a package change, and one template smoke, about 10 minutes; draft PRs run nothing. Template instruction and Markdown changes retain their contract lanes without adding runtime kits. Merge groups run the full selected board once, name the first red job in a PR comment and cancel the rest of the run, so a change pays for one board (PRD-550, PRD-731); `ci-required` rejects a review plan anywhere but a PR into develop, and `pnpm ci:local --affected` runs the queue's board. Develop pushes, explicit full runs, unknown inputs, main qualification and scheduled audits use fresh exhaustive qualification: all shipped templates, both golden journeys and the full supported native matrix. `ci-required` independently rejects a review plan for these events; Integration is a required reusable join on the same candidate, with each selected canonical job proven by a receipt from its latest attempt in the current run and an independently checked completed-success API job; missing, skipped, cancelled, stale or unmapped receipts fail closed. So a red job on an unchanged head needs only `gh run rerun --failed`, not a full run; a fix still needs a push. Native per-leg authoritative completion and cold-path queue feasibility remain follow-ups before deferring any Android PR coverage. Template scenarios run once per selected kit; develop changes run one representative golden journey and full qualification retains both starter and platformer journeys. `ci-required` rejects missing or failed selected checks; retained contexts include `supply-chain`, `template-nonvisual`, `golden-path`, and native `desktop-parity` when applicable. Native host/platform tests run for native or shared runtime consumers, main qualification and unresolved scopes. Unsafe matrix/reusable verdict reuse stays disabled until authoritative complete coverage and per-leg routing exist. One exception (PRD-730): a merge group whose tree equals the tree of its pull request's head, which has a green full `ci.yml` run an author dispatched (a review-lane PR run never counts), reuses that run; `ci-required` re-decides it and writes a receipt, and any doubt runs the board. A new candidate runs fresh tests; only exact-run immutable build artifacts are shared. Details: `docs/PRDs/done/PRD-483-impact-driven-ci-without-repeated-template-setup.md` and `docs/PRDs/done/PRD-373-selective-ci-and-develop-promotion.md`. +**CI.** The pre-push hook runs `pnpm ci:fast` drift checks only; it proves no runtime behaviour. `pnpm ci:local --full` runs the full board; `pnpm ci:local --affected --base origin/develop --target develop` runs what CI would select (dirty or unknown inputs fall back to full). A change touching only Markdown that no fixture consumes (not `AGENTS.md`/`CLAUDE.md`), or only the generated speed-loop record (`docs/ci-speed/ledger.json`, `docs/ci-speed/runs/.jobs.json`), runs no product CI job. Ordinary PRs into develop use the complete candidate diff: CI-only changes run CI contracts, exact template changes run scaffolder contracts and affected templates, and shared executable inputs earn the thin review lane (PRD-731): scope, typecheck, lint, budgets, secrets scan, the unit tests that import a changed file, `test` and `build-artifacts` for a package change, and one template smoke, about 10 minutes; draft PRs run nothing. Template instruction and Markdown changes retain their contract lanes without adding runtime kits. Merge groups run the full selected board once, name the first red job in a PR comment and cancel the rest of the run, so a change pays for one board (PRD-550, PRD-731); `ci-required` rejects a review plan anywhere but a PR into develop, and `pnpm ci:local --affected` runs the queue's board. Develop pushes, explicit full runs, unknown inputs, main qualification and scheduled audits use fresh exhaustive qualification: all shipped templates, both golden journeys and the full supported native matrix. `ci-required` independently rejects a review plan for these events; Integration is a required reusable join on the same candidate, with each selected canonical job proven by a receipt from its latest attempt in the current run and an independently checked completed-success API job; missing, skipped, cancelled, stale or unmapped receipts fail closed. So a red job on an unchanged head needs only `gh run rerun --failed`, not a full run; a fix still needs a push. Native per-leg authoritative completion and cold-path queue feasibility remain follow-ups before deferring any Android PR coverage. Template scenarios run once per selected kit; develop changes run one representative golden journey and full qualification retains both starter and platformer journeys. `ci-required` rejects missing or failed selected checks; retained contexts include `supply-chain`, `template-nonvisual`, `golden-path`, and native `desktop-parity` when applicable. Native host/platform tests run for native or shared runtime consumers, main qualification and unresolved scopes. Unsafe matrix/reusable verdict reuse stays disabled until authoritative complete coverage and per-leg routing exist. One exception (PRD-730): a merge group whose tree equals the tree of its pull request's head, which has a green full `ci.yml` run an author dispatched (a review-lane PR run never counts), reuses that run; `ci-required` re-decides it and writes a receipt, and any doubt runs the board. A new candidate runs fresh tests; only exact-run immutable build artifacts are shared. Details: `docs/PRDs/done/PRD-483-impact-driven-ci-without-repeated-template-setup.md` and `docs/PRDs/done/PRD-373-selective-ci-and-develop-promotion.md`. Registry commands read auth from `~/.npmrc`; pass a checkout-local untracked `.npmrc` with `npm --userconfig .npmrc `. Never print either file. TypeScript 7 `strict` (the native `tsc`); `typescript` imports use the official TypeScript 6 compatibility API for AST scripts and tsup declarations, **ESM only**; relative imports end in `.js`. Versions come from the `catalog:` in `pnpm-workspace.yaml`, templates excepted. Biome formats — never hand-format. Interfaces are `I`-prefixed; classes and type aliases are not. Unit tests: `/__tests__/*.spec.ts`, vitest, node environment, DOM and GPU stubbed. diff --git a/CLAUDE.md b/CLAUDE.md index 2df6036c12..e6ba10401c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,7 @@ pnpm native:build # opt-in; downloads deps, compiles th pnpm native:verify:desktop # 300 native frames + a non-blank screenshot ``` -**CI.** The pre-push hook runs `pnpm ci:fast` drift checks only; it proves no runtime behaviour. `pnpm ci:local --full` runs the full board; `pnpm ci:local --affected --base origin/develop --target develop` runs what CI would select (dirty or unknown inputs fall back to full). A change touching only Markdown that no fixture consumes (not `AGENTS.md`/`CLAUDE.md`), or only the generated speed-loop record (`docs/ci-speed/ledger.json`, `docs/ci-speed/index.html`, `docs/ci-speed/runs/.jobs.json`), runs no product CI job. Ordinary PRs into develop use the complete candidate diff: CI-only changes run CI contracts, exact template changes run scaffolder contracts and affected templates, and shared executable inputs earn the thin review lane (PRD-731): scope, typecheck, lint, budgets, secrets scan, the unit tests that import a changed file, `test` and `build-artifacts` for a package change, and one template smoke, about 10 minutes; draft PRs run nothing. Template instruction and Markdown changes retain their contract lanes without adding runtime kits. Merge groups run the full selected board once, name the first red job in a PR comment and cancel the rest of the run, so a change pays for one board (PRD-550, PRD-731); `ci-required` rejects a review plan anywhere but a PR into develop, and `pnpm ci:local --affected` runs the queue's board. Develop pushes, explicit full runs, unknown inputs, main qualification and scheduled audits use fresh exhaustive qualification: all shipped templates, both golden journeys and the full supported native matrix. `ci-required` independently rejects a review plan for these events; Integration is a required reusable join on the same candidate, with each selected canonical job proven by a receipt from its latest attempt in the current run and an independently checked completed-success API job; missing, skipped, cancelled, stale or unmapped receipts fail closed. So a red job on an unchanged head needs only `gh run rerun --failed`, not a full run; a fix still needs a push. Native per-leg authoritative completion and cold-path queue feasibility remain follow-ups before deferring any Android PR coverage. Template scenarios run once per selected kit; develop changes run one representative golden journey and full qualification retains both starter and platformer journeys. `ci-required` rejects missing or failed selected checks; retained contexts include `supply-chain`, `template-nonvisual`, `golden-path`, and native `desktop-parity` when applicable. Native host/platform tests run for native or shared runtime consumers, main qualification and unresolved scopes. Unsafe matrix/reusable verdict reuse stays disabled until authoritative complete coverage and per-leg routing exist. One exception (PRD-730): a merge group whose tree equals the tree of its pull request's head, which has a green full `ci.yml` run an author dispatched (a review-lane PR run never counts), reuses that run; `ci-required` re-decides it and writes a receipt, and any doubt runs the board. A new candidate runs fresh tests; only exact-run immutable build artifacts are shared. Details: `docs/PRDs/done/PRD-483-impact-driven-ci-without-repeated-template-setup.md` and `docs/PRDs/done/PRD-373-selective-ci-and-develop-promotion.md`. +**CI.** The pre-push hook runs `pnpm ci:fast` drift checks only; it proves no runtime behaviour. `pnpm ci:local --full` runs the full board; `pnpm ci:local --affected --base origin/develop --target develop` runs what CI would select (dirty or unknown inputs fall back to full). A change touching only Markdown that no fixture consumes (not `AGENTS.md`/`CLAUDE.md`), or only the generated speed-loop record (`docs/ci-speed/ledger.json`, `docs/ci-speed/runs/.jobs.json`), runs no product CI job. Ordinary PRs into develop use the complete candidate diff: CI-only changes run CI contracts, exact template changes run scaffolder contracts and affected templates, and shared executable inputs earn the thin review lane (PRD-731): scope, typecheck, lint, budgets, secrets scan, the unit tests that import a changed file, `test` and `build-artifacts` for a package change, and one template smoke, about 10 minutes; draft PRs run nothing. Template instruction and Markdown changes retain their contract lanes without adding runtime kits. Merge groups run the full selected board once, name the first red job in a PR comment and cancel the rest of the run, so a change pays for one board (PRD-550, PRD-731); `ci-required` rejects a review plan anywhere but a PR into develop, and `pnpm ci:local --affected` runs the queue's board. Develop pushes, explicit full runs, unknown inputs, main qualification and scheduled audits use fresh exhaustive qualification: all shipped templates, both golden journeys and the full supported native matrix. `ci-required` independently rejects a review plan for these events; Integration is a required reusable join on the same candidate, with each selected canonical job proven by a receipt from its latest attempt in the current run and an independently checked completed-success API job; missing, skipped, cancelled, stale or unmapped receipts fail closed. So a red job on an unchanged head needs only `gh run rerun --failed`, not a full run; a fix still needs a push. Native per-leg authoritative completion and cold-path queue feasibility remain follow-ups before deferring any Android PR coverage. Template scenarios run once per selected kit; develop changes run one representative golden journey and full qualification retains both starter and platformer journeys. `ci-required` rejects missing or failed selected checks; retained contexts include `supply-chain`, `template-nonvisual`, `golden-path`, and native `desktop-parity` when applicable. Native host/platform tests run for native or shared runtime consumers, main qualification and unresolved scopes. Unsafe matrix/reusable verdict reuse stays disabled until authoritative complete coverage and per-leg routing exist. One exception (PRD-730): a merge group whose tree equals the tree of its pull request's head, which has a green full `ci.yml` run an author dispatched (a review-lane PR run never counts), reuses that run; `ci-required` re-decides it and writes a receipt, and any doubt runs the board. A new candidate runs fresh tests; only exact-run immutable build artifacts are shared. Details: `docs/PRDs/done/PRD-483-impact-driven-ci-without-repeated-template-setup.md` and `docs/PRDs/done/PRD-373-selective-ci-and-develop-promotion.md`. Registry commands read auth from `~/.npmrc`; pass a checkout-local untracked `.npmrc` with `npm --userconfig .npmrc `. Never print either file. TypeScript 7 `strict` (the native `tsc`); `typescript` imports use the official TypeScript 6 compatibility API for AST scripts and tsup declarations, **ESM only**; relative imports end in `.js`. Versions come from the `catalog:` in `pnpm-workspace.yaml`, templates excepted. Biome formats — never hand-format. Interfaces are `I`-prefixed; classes and type aliases are not. Unit tests: `/__tests__/*.spec.ts`, vitest, node environment, DOM and GPU stubbed. diff --git a/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md b/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md index 3b949ac503..d47fd096f5 100644 --- a/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md +++ b/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md @@ -145,7 +145,7 @@ qualifying run. A PR can still opt in to qualifying on the merge path with a lab #### Phase 3: Capacity and the latency audit **Status:** IN PROGRESS **Files:** `scripts/ci-merge-latency.mjs` (new), `scripts/ci-speed-loop.mjs` (new), `docs/ci-speed/` (ledger and HTML dashboard), `.claude/skills/ci-speed-loop/`, `.github/workflows/ci.yml` and `.github/workflows/integration.yml` `runs-on` expressions, `scripts/__tests__/ci-structure.spec.ts`, `docs/PRDs/CI/EXECUTION-ORDER.md`. -**Implementation:** Route gating jobs to `vars.TN_RUNNER` with hosted fallback; route qualifying jobs to hosted. Write the audit script with `gh api` and no new dependency. `scripts/ci-speed-loop.mjs` records each audit window in `docs/ci-speed/ledger.json` and renders `docs/ci-speed/index.html`, a dashboard of every metric against its goal (skill: `ci-speed-loop`). +**Implementation:** Route gating jobs to `vars.TN_RUNNER` with hosted fallback; route qualifying jobs to hosted. Write the audit script with `gh api` and no new dependency. `scripts/ci-speed-loop.mjs` records each audit window in `docs/ci-speed/ledger.json` and feeds the CI speed tab of `pnpm dashboard` (PRD-751), a dashboard of every metric against its goal (skill: `ci-speed-loop`). - [x] The audit and the dashboard exist, reproduce this PRD's own table, and hold the baseline. proof: `pnpm exec vitest run scripts/__tests__/ci-speed-loop.spec.ts` 14 of 14; `node scripts/ci-merge-latency.mjs --since 2026-10-08` gives #461 104.2, #462 242.9, #457 432.7, #449 593.9, #455 626.9, #456 892.1, #465 131.9 min enqueue → merge (this PRD's table) and 8 `auto-exposure` first failures; ledger iteration #1 = baseline. **Investigation (2026-10-10):** The current hosted placement of the `template-nonvisual` template legs is intentional — the workflow comment pins them to hosted on purpose, to keep one routing variable from sending the whole burst to a single pool. No routing change is accepted pending queue and capacity proof. A rejected attempt to move the job onto `vars.TN_RUNNER` was reverted in full (`.github/workflows/ci.yml`, `scripts/__tests__/ci-structure.spec.ts`); nothing about this job's routing is changed here. **Repair (2026-10-10, balancer state):** `scripts/ci-runners.sh` `balance` caches `have_heavy`/`have_light` from its own last write, so a routing variable deleted outside the loop (author unknown; `up` documents the balancer as owner and `down` stops the balancer before it clears them) stays unset while the pool is idle and capacity routes to hosted. In scope: `scripts/ci-runners.sh` (`balance` only) and `scripts/__tests__/ci-runners.spec.ts`. Failure hypothesis: a cached `have_*` never re-reads the actual variable, so an external delete is invisible. Fix: one bounded `gh api repos/$repo/actions/variables` read per poll filtered to the two known routing names, exact expected-label compare, re-advertise while capacity stays debounced-on, skip advertising when the read fails. Live effect observed 2026-10-10T13:34:48Z: both routing variables are restored to their expected labels (`tn-local`, `tn-local-light`) with one `balance` controller running. On candidate `d6c1af145` (run 38056537726) the local build, test, integration and native jobs did run in CI, so variable restoration and job uptake are now observed, while latency acceptance stays open; the `exposure` lane measured 19.5 min in queue. That board failed cold boot: `verifySnap` timed out with `state.cutSampleFrames` expected 180 but last 89 in 60 sec, while the 20-launch `verifyColdBoot` check passed. Whether the cause is speed or the game is unproved and no assertion is loosened. The Windows failure triage is pending, so no full-green board is claimed. The local fake-`gh` harness stays the only behavioural proof of the balancer itself. diff --git a/docs/PRDs/authoring/PRD-735-game-reference-content-at-scale.md b/docs/PRDs/authoring/PRD-735-game-reference-content-at-scale.md new file mode 100644 index 0000000000..2940355e07 --- /dev/null +++ b/docs/PRDs/authoring/PRD-735-game-reference-content-at-scale.md @@ -0,0 +1,85 @@ +# PRD-735 — Reverse-engineer references at scale: the repo rename, the full list, the classics and the scout pipeline + +**Status:** PARTIAL — AC-0, AC-1 and AC-3 done +**Priority:** P1 — AC-2, AC-4 and AC-5 are open, and the PRD-750 kit loop (P1) needs their references past its first row; today the release holds one Tier-1 game (SM64). +**Complexity:** 4 (MEDIUM) — separate data release +2, external API +1, 1–5 files +1; risk override: none. +**Owner:** João Paulo Furtado +**Depends on:** PRD-734 (done: the reference tools, the schema and the `v0.1` release) +**PR:** the PRD-750 epic PR (owner, 2026-10-26) + +## Context + +PRD-734 ships the reference tools and one Tier-1 game, Super Mario 64. This PRD fills the data repo +`threenative-game-refs` so the references cover what users actually ask for: + +- **Tier 2:** an index row for each project in + (2,359 on 2026-10-20). +- **Tier 1:** about 20 classic repos with a full map and full cards, in the order of the PRD-750 + genre list (its candidate reference per row), then Ocarina of Time for Lantern Vale and the other + classics. +- **Promotion:** saved Tier-2 cards that users contribute through `engine_save_reference` are + reviewed and merged. + +The schema, the borrow-not-copy guardrail and the pinned-SHA rule come from PRD-734 and do not +change here. + +## Solution + +- **Rename** (owner, 2026-10-26: "we should rename ... threenative-game-refs to + threenative-reverse-engineer-mcp"). The data repo moves to + `ThreeNativeHQ/threenative-reverse-engineer-mcp`. `REFERENCES_URL` and `CONTRIBUTION_REPO` in + `packages/engine-mcp/src/references.ts` follow it. The tools stay in `threenative-engine-mcp`, + and their descriptions carry "reverse engineer" so a search for those words finds them. GitHub + redirects the old release URL, so installed games keep working. +- **Import script** in the data repo. It parses the list README into Tier-2 rows (title, platform, + repo URL), gives each row a stable kebab-case id and keeps the ids of rows that already exist. +- **Scout pipeline.** A scout is a cheap agent (the `ds` harness, V4.1-Flash max) that clones one + repo at a pinned SHA, writes the map, then writes the cards for each mechanic node. It gets a + fixed brief: the schema, the guardrail and the unit rule. Each constant states its unit and its + timestep, for example "per frame at 30 Hz". The PRD-734 `validate` job checks every path, + function and line against the pinned clone. A full review then reads each card before merge, + because a wrong constant misleads every user. +- **Tier-1 order:** OoT first, then a ranked list of about 20 classics, kept in the data repo + README. Each choice must have a mature decomp and a mechanic that users ask for. +- **Contributions:** an issue made by `engine_save_reference` is validated by the same job, reviewed + and promoted to Tier 1 when its game is complete enough. + +## Acceptance Criteria + +See the phase boxes below. Each box names its proof. + +## Integration Ledger + +Integration: the release URL and contribution repo in `packages/engine-mcp/src/references.ts` change with the rename (AC-0). The PRD-734 tools read the new releases with no other code change. + +## Decisions + +- 2026-10-26 (owner): rename the data repo to `threenative-reverse-engineer-mcp`; the tools stay in engine-mcp with "reverse engineer" as search words, not a separate server. +- 2026-10-26 (owner): this PRD lands before the PRD-750 loop runs past its first row. +- 2026-10-24 (agent): split from PRD-734 to keep both PRDs within the 3-phase cap. Content at scale does not change the tool contract. + +## Execution Phases + +#### Phase 1: The rename and Tier 2 for the full list + +**Status:** PARTIAL — AC-0 and AC-1 done; AC-2 waits for a published release +**Files:** `packages/engine-mcp/src/references.ts`; the data repo: import script, release workflow. + +- [x] AC-0 [local]: an installed project's MCP fetches the release from `ThreeNativeHQ/threenative-reverse-engineer-mcp`, and `engine_search_capabilities "reverse engineer"` returns the reference tools. proof: `pnpm exec vitest run packages/engine-mcp/__tests__/references.spec.ts packages/engine-mcp/__tests__/search.spec.ts` — red 2 failed, then 90/90 green (1 live skipped); `-t live` against the new URL 1/1 green; repo transferred and renamed with `gh api .../transfer` on the owner's admin login; the old `jonit-dev/threenative-game-refs` release URL redirects (HTTP 200), so installed games keep working (2026-10-26) +- [x] AC-1 [shared]: the release index holds one Tier-2 row per list project, and a second import keeps every existing id. proof: data repo `import.mjs` (red, then `import.spec.mjs` 4/4) run on the list README at `9051afcd` ("3287 projects"): 3287 catalog rows = 3286 distinct list repositories (one repository is listed twice) + Quake; a second import is byte-identical (`cmp`); the `validate` job builds `index.json.gz` from it, green on `806e728` (run 40441794931) (2026-10-26) +- [ ] AC-2 [local]: engine-mcp searches a Tier-2 title from the new release and returns its repo with the explore guidance. proof: `THREENATIVE_REFERENCES_URL= pnpm exec vitest run packages/engine-mcp/__tests__/references.spec.ts -t live` + +#### Phase 2: Scout pipeline and Ocarina of Time + +**Status:** DONE +**Files:** the data repo: scout brief, scout runner script. + +- [x] AC-3 [shared]: OoT has a reviewed map and at least 10 reviewed cards at a pinned SHA, each constant with its unit and timestep, and the `validate` job passes. proof: `game-oot.json` at `zeldaret/oot@14759abf`, 8 nodes and 17 cards, scouted on the cheap arm (`scout/run.sh`), then a full per-card review against the pinned tree fixed 14 cards (the 1.5x `R_UPDATE_RATE` position scaling at `src/code/z_actor.c:987`, adult/child values, ledge thresholds); `validate` green on `806e728` (run 40441794931) (2026-10-26) + +#### Phase 3: The classics and the contribution path + +**Status:** NOT STARTED +**Files:** the data repo: the ranked Tier-1 list, the contribution issue template. + +- [ ] AC-4 [shared]: about 20 Tier-1 games each have a reviewed map and at least 10 reviewed cards, and the `validate` job passes. proof: data repo `validate` job, Tier-1 count in the release index +- [ ] AC-5 [shared]: one issue made by `engine_save_reference` passes `validate`, is reviewed and appears in the next release. proof: data repo issue, `validate` run and release diff diff --git a/docs/PRDs/done/assets/PRD-729-assets-ship-smallest-and-preload-fastest.md b/docs/PRDs/done/assets/PRD-729-assets-ship-smallest-and-preload-fastest.md index fe4acc5ce3..0acf748288 100644 --- a/docs/PRDs/done/assets/PRD-729-assets-ship-smallest-and-preload-fastest.md +++ b/docs/PRDs/done/assets/PRD-729-assets-ship-smallest-and-preload-fastest.md @@ -18,8 +18,8 @@ Phase 3 lever changes `packages/assets` defaults that every game inherits (+2). **Depends on:** [PRD-351](../../assets/PRD-351-compression-never-looks-worse-than-a-floor.md) (the quality floor that every lever must hold), [PRD-496](../../performance/PRD-496-the-loading-screen-is-bytes-not-files.md) (the loader fan-out and progress work; its probe method is the preload method here). -**Dashboard:** [`docs/asset-perf/index.html`](../../../asset-perf/index.html), generated from -[`docs/asset-perf/ledger.json`](../../../asset-perf/ledger.json) by `node scripts/asset-perf-loop.mjs report`. +**Dashboard:** the Asset perf tab of `pnpm dashboard` (PRD-751) renders +[`docs/asset-perf/ledger.json`](../../../asset-perf/ledger.json) on each load. ## Context diff --git a/docs/PRDs/done/starter-kits/PRD-751-one-dashboard.md b/docs/PRDs/done/starter-kits/PRD-751-one-dashboard.md new file mode 100644 index 0000000000..3a3e586c51 --- /dev/null +++ b/docs/PRDs/done/starter-kits/PRD-751-one-dashboard.md @@ -0,0 +1,69 @@ +# PRD-751 — One dashboard: `pnpm dashboard` holds every KPI tab + +**Status:** DONE (2026-10-26) +**Priority:** P2 — the owner could not see what the loops do from one place (done) +**Complexity:** 2 (LOW) — 6–10 files +2; risk override: none +**Owner:** João Paulo Furtado +**Depends on:** None +**PR:** the PRD-750 epic PR (owner, 2026-10-26) + +## Context + +Owner ask (2026-10-26): "i also need a kpi dashboard to keep track of wtf is being done", then "there +are existing performance dashboards somewhere on codebase. maybe merge them all? kill redundant. But +just leave only one to rule them all". + +Dashboards found at `origin/pr563-followup` 369e57e5a: + +| Dashboard | Data | Disposition | +| --- | --- | --- | +| `pnpm perf:live` (`scripts/perf-live.ts`, `scripts/perf-live.html`) | live tabs: bench:engines reports, Midway (`scripts/midway-bench/dashboard.ts`), Shadows vs Unreal (`scripts/shadow-perf/dashboard.ts`) | **keep** — renamed `pnpm dashboard`, the hub | +| `docs/ci-speed/index.html`, written by `scripts/ci-speed-loop.mjs` | `docs/ci-speed/ledger.json` | fold in as a tab; stop writing the page | +| `docs/asset-perf/index.html`, written by `scripts/asset-perf-loop.mjs` | its ledger | fold in as a tab; stop writing the page | +| `.agents/skills/native-performance-loop/references/dashboard.md` | tells a campaign to generate its own HTML page | point it at a hub tab | +| `docs/PRDs/Unreal/research/unreal-visuals-and-performance-feature-map.html` | the Unreal port order | keep — a plan, not a KPI view | +| `prd-dashboard` (port 4319, its own repo) | live agent sessions across repos | out of scope — cross-repo telemetry | + +## Solution + +- Rename `perf:live` to `dashboard` in `package.json` and every doc that names it. +- Add three tabs, each a small reader like `shadow-perf/dashboard.ts` (reads one ledger, writes + nothing, fails closed: missing = `pending`, malformed = `invalid`): **CI speed**, **Asset perf**, + **Kit loop** (PRD-750 `docs/kit-loop/ledger.json`). +- `ci-speed-loop.mjs` and `asset-perf-loop.mjs` keep writing their ledgers and stop writing HTML; + delete the two `index.html` files and update their consumers (root `AGENTS.md` CI paragraph, + `scripts/__tests__/ci-efficiency.spec.ts`, the CI-speed skill, PRD-550 and PRD-729 links). +- The native-performance-loop skill reference names the hub tab instead of a per-campaign page. + +## Acceptance Criteria + +See the phase boxes. Each box names its proof. + +## Integration Ledger + +| Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | +| --- | --- | --- | --- | +| One KPI hub | `pnpm dashboard` → `scripts/perf-live.ts` server → tabs | replaces `docs/ci-speed/index.html`, `docs/asset-perf/index.html` | AC-1..AC-4 | + +## Decisions + +- 2026-10-26 (owner): `perf:live` becomes the single dashboard; the static pages and the skill's per-campaign HTML go. + +## Execution Phases + +#### Phase 1: The hub and its two folded tabs + +**Status:** DONE +**Files:** `package.json`, `scripts/perf-live.ts`, `scripts/perf-live.html`, `scripts/ci-speed-loop.mjs`, `scripts/asset-perf-loop.mjs`, the two deleted `index.html` files and their consumers. + +- [x] AC-1 [local]: `pnpm dashboard` serves the existing tabs plus CI speed and Asset perf, read from their ledgers; a missing or malformed ledger shows `pending` or `invalid`. proof: `pnpm exec vitest run scripts/__tests__/perf-live.spec.ts` — red 3 failed before `ledgerTab`/`withNav`, then 29/29 green; with the ci-speed and asset-perf specs 49/49 (2026-10-26) +- [x] AC-2 [local]: no tracked file writes or links a removed page. proof: `rg -n "ci-speed/index.html|asset-perf/index.html|perf:live" --glob '!docs/PRDs/done/**'` empty, `pnpm check:docs`, `pnpm exec vitest run scripts/__tests__/ci-efficiency.spec.ts` — rg empty apart from this PRD's own history, check:docs exit 0 (3558 links), ci-efficiency 55/55; the `report` and `serve` commands are gone (2026-10-26) +- [x] AC-3 [local]: the hub renders all tabs in a real browser with no console error. proof: headed screenshot per tab on the PR — headed Chromium under Xvfb, 5 tabs, 0 console errors, nav identical on each; images on the `prd-751-screenshots` branch (2026-10-26) + +#### Phase 2: The kit-loop tab and the skill pointer + +**Status:** DONE +**Files:** `scripts/perf-live.ts` (kit-loop reader), `.agents/skills/native-performance-loop/references/dashboard.md`. + +- [x] AC-4 [local]: the kit-loop tab plots K1–K4 per iteration against the PRD-750 baseline and targets. proof: `pnpm exec vitest run scripts/__tests__/perf-live.spec.ts -t kit-loop` — red (no `kit-loop` tab), then green; with `kit-loop.spec.ts` 34/34; headed capture of the live tab (empty ledger, 0 console errors) and of a 2-row fixture render on the PR (2026-10-26) +- [x] AC-5 [local]: the native-performance-loop skill reference sends a campaign to a hub tab, not a new page. proof: `pnpm sync:agents --check`, `pnpm check:docs` — both exit 0; `index.html` no longer appears in the skill or its reference (2026-10-26) diff --git a/docs/PRDs/performance/PRD-573-unreal-class-performance-in-release-cycles.md b/docs/PRDs/performance/PRD-573-unreal-class-performance-in-release-cycles.md index 234d6d9ac3..822194c741 100644 --- a/docs/PRDs/performance/PRD-573-unreal-class-performance-in-release-cycles.md +++ b/docs/PRDs/performance/PRD-573-unreal-class-performance-in-release-cycles.md @@ -161,7 +161,7 @@ Cherry-picks of F `c1ef353ca 2007efe12 3d3695197 3b29482a6 504709d17 726df7f5c d #### Phase v1.9: The scorecard **Status:** NOT STARTED Today: `perf` has p95, p99 and hitches but no memory or host load; `bench:scoreboard` has medians only; the R-ladder runs only on legacy and Godot arms; `run-godot.ts` has no workloads, so Godot ratios exist only on the ladders; `profile:wasm-page --load` means load phases, not host load; no soak command exists. -- [x] `pnpm perf:live` serves a page that re-reads every bench report under `artifacts/engine-load-test` every 2 s and plots p50 per workload and arm, with a red ring for a >30% regression against the series best. proof: `pnpm exec vitest run scripts/__tests__/perf-live.spec.ts` (2 passed) and a browser check against the 505 recorded pilot points, 2026-10-11. Revamped 2026-10-12: cycle tabs v1 to v5, native-vs-legacy KPI tiles against the 2x target, epic progress and gate pills read live from this PRD (`--prd`), `epicOf` spec; the server watches `artifacts/engine-load-test/prd-573` only. Checked in a browser against the 18 first A/B reports. +- [x] `pnpm dashboard` serves a page that re-reads every bench report under `artifacts/engine-load-test` every 2 s and plots p50 per workload and arm, with a red ring for a >30% regression against the series best. proof: `pnpm exec vitest run scripts/__tests__/perf-live.spec.ts` (2 passed) and a browser check against the 505 recorded pilot points, 2026-10-11. Revamped 2026-10-12: cycle tabs v1 to v5, native-vs-legacy KPI tiles against the 2x target, epic progress and gate pills read live from this PRD (`--prd`), `epicOf` spec; the server watches `artifacts/engine-load-test/prd-573` only. Checked in a browser against the 18 first A/B reports. - [x] `bench:engines` and `bench:scoreboard` rows carry p95, p99, the hitch count, draw calls and pipeline compiles, and fail closed on a missing counter. proof: a red-green case in the engine-load-test report spec Result 2026-10-11: 147 engine-load-test specs green, red first (14 failures); Godot rows are exempt from `pipelineCompiles` only (Godot load test has no counter; `ponytail:` note in `report.ts`), a Godot rung with no draw call still fails. Unverified: the counter has not run in a real three/WebGPU renderer. - [ ] `perf`, `bench:engines` and `profile:wasm-page` carry `hostLoad` and the core count, and mark absolutes `invalid-absolute` above half the core count. proof: a red-green case per spec with a stubbed load Partial: `bench:engines` and `perf` stamp `hostLoad` and `invalid-absolute` (stubbed-load specs green, 4 red first for `perf`); `profile:wasm-page` is slice 7 and does not exist yet. - [ ] Memory (partial 2026-10-12, arm run reviewed by the lead: `parseProcStatusMemory` and `parseTotalPssMb` pure parsers fail closed with named errors, `--executable` reads `VmHWM`, 4 specs red then green; the `--logcat` PSS wiring and `profile:wasm-page --memory` are not exercised, no desktop or emulator run yet): `perf --executable` peak RSS (`VmHWM`), `perf --logcat` PSS (`dumpsys meminfo `), `profile:wasm-page --memory` (Wasm memory, JS heap). proof: red-green cases in `perf.spec.ts` and the profile-wasm-page spec, then one desktop run and one emulator run diff --git a/docs/PRDs/performance/PRD-610-native-engine-wins-on-real-games.md b/docs/PRDs/performance/PRD-610-native-engine-wins-on-real-games.md index 5fa1a44e08..aa4f9854b5 100644 --- a/docs/PRDs/performance/PRD-610-native-engine-wins-on-real-games.md +++ b/docs/PRDs/performance/PRD-610-native-engine-wins-on-real-games.md @@ -172,7 +172,7 @@ The boxes are under the execution phases. Phase 1 holds AC-1 and AC-2 (done). Ph | Animation off the critical path | `AnimationPlayer.update` → C++ mixer step and jobs | per-rig `mixer.update` calls and JS stride on native | AC-17 | | Engine JS in C++ | `three-native/src/audio.ts`, `core/src/ui-state.ts`, `core/src/ui-bridge.ts`, player shim `core-three.mjs`, `three-native/src/object-surface.ts` | JS pose sync, per-publish validation, per-read crossings | AC-18 | | HUD under one frame | WebKit overlay snapshot mailbox → frame blend | — | AC-5 | -| Final-tier result | `pnpm perf:live` hero | R5 hero becomes the cheap-tier row | AC-19 | +| Final-tier result | `pnpm dashboard` hero | R5 hero becomes the cheap-tier row | AC-19 | ## Decisions diff --git a/docs/PRDs/starter-kits/PRD-750-kit-authoring-loop.md b/docs/PRDs/starter-kits/PRD-750-kit-authoring-loop.md new file mode 100644 index 0000000000..7e76d63296 --- /dev/null +++ b/docs/PRDs/starter-kits/PRD-750-kit-authoring-loop.md @@ -0,0 +1,154 @@ +# PRD-750 — Kit authoring loop: build a genre kit, cold-build a game from it, learn, repeat + +**Status:** PARTIAL — Phase 1 done +**Priority:** P1 — "make me a game like X" is the primary workflow before the release; the measure step exists, but no iteration has run (Phases 2-3 open) +**Complexity:** 3 (LOW) — 1–5 files +1, new module (the loop script) +2; risk override: none +**Owner:** João Paulo Furtado +**Depends on:** PRD-735 (reverse-engineer references at scale), PRD-749 (one kit base and a kit contract), PRD-751 (one dashboard) +**PR:** one epic PR for the loop and every kit it builds (owner, 2026-10-26), merged at the release + +## Context + +The owner wants the release to invest in the authoring layer: a user asks "make me a game like X", +an agent starts from the matching starter kit, and the first build is good on few tokens +(root `AGENTS.md`: "A request that names a game or genre a kit covers starts from that kit"). + +Today: + +- 16 kits ship in `packages/create-threenative/templates/`. Only three (collectathon, adventure, kart; + PRD-732/733/734) were built to the current bar, with `kit.json` `aliases` and pinned reference + citations. +- The reverse-engineer tools (`engine_search_references`, `engine_reference_detail`, + `engine_save_reference` in `packages/engine-mcp/src/references.ts`) shipped in #563, but the data + release holds one Tier-1 game (SM64). PRD-735 fills it. +- Nothing measures what a cold user agent pays to build from a kit, or what friction it hits. Each + kit lane learned alone, and the next lane did not read it. +- Harnesses exist and are reused here, not rebuilt: `pnpm sandbox` (user-like install), + `pnpm sweep:measure` (`scripts/measure-sandbox.ts`), the `build-on-sandbox` and + `score-build-experience` skills, the playtest runner and the visual judge rule. + +## Solution + +One **iteration** builds one row of the genre list below: + +```mermaid +flowchart LR + R[Reference: engine_search_references
-> pinned decomp cards] --> A[Author: in-repo agent
writes or revamps kit N on the kit base] + A --> C[Cold build: fresh agent in pnpm sandbox
prompt 'make me a game like X'] + C --> M[Measure: 4 KPIs
from transcripts + gates + judge] + M --> L[Retro row in docs/kit-loop/ledger.json
worked / failed / top 3 frictions] + L --> F[Fix: top frictions in the authoring layer
before iteration N+1] + F --> R +``` + +- **Author step.** An in-repo agent takes the genre row, reads the reference cards through the MCP, + and writes (or revamps) the kit on the PRD-749 kit base with its own short PRD in + `docs/PRDs/starter-kits/`. Mechanics borrow constants with `file:line` citations; art, names and + layouts stay original. +- **Cold build.** A fresh agent in a `pnpm sandbox` folder gets only the user prompt + ("make me a game like "). It must find the kit by alias, build, and pass the kit's + gates. It gets no repo `AGENTS.md` chain. +- **Measure.** `scripts/kit-loop.mjs` (the only new module) reads the cold-build transcript and the + gate outputs and appends one ledger row. It measures; it does not drive agents. +- **Retro.** The coordinator reads both transcripts (author and cold), writes three lists into the + ledger row (worked, failed, top 3 frictions with the transcript line), and the next iteration fixes + the top frictions first. **Fix scope** (owner, 2026-10-26): the whole authoring layer — MCP tools, + `capabilities.json`, template `AGENTS.md`, skills, scaffolder, and small shared helpers that several + kits need. New engine systems are out of scope. +- **Dashboard.** The ledger is a tab of the single `pnpm dashboard` (PRD-751). + +### KPIs (owner, 2026-10-26) + +Each one is a field of every ledger row. Iteration 1 sets the baseline; targets are relative to it. + +| KPI | Definition | Source | Target by the last iteration | +| --- | --- | --- | --- | +| K1 cold-build cost | tokens (input + output + cache writes), wall minutes and turns, from the prompt to the first build whose gates pass | cold transcript (`claude -p --output-format json` usage) | -40% tokens vs baseline | +| K2 first-build quality | share of kit playtest scenarios green on the first cold build with no human edit; the performance gate's cost p95 within budget; visual judge verdict against the reference image | playtest exit codes, `performance.maxFrameMsP95` cost p95, judge | 100% scenarios, budget met, judge CLOSE or better | +| K3 friction count | capability-search misses + hand-written duplicates of an installed system + wrong-API errors + retries of the same failing command | `score-build-experience` rows and the cold transcript | -50% vs baseline | +| K4 AI play grade | a fresh Opus judge plays the cold build (free play with real input, then the scenario captures) and grades feel and mechanics 1–5 against the reference cards | judge report | 4 or higher | + +**Stop rule** (owner, 2026-10-26): the loop runs without the owner and stops when the genre list is +done, or when two iterations in a row improve none of K1–K4 by 5% or more. After each kit, the +owner gets one ping with the dashboard tab and the ledger row. + +### Genre list (ranked; owner reviews it in the PR) + +Ranked by user demand, then by reference availability. "Revamp" rebuilds an existing kit on the kit +base to the PRD-732/733/734 bar; "new" adds a kit. PRD-746 already owns the shooter revamp, so it is +not a row here. The reference for each row is a candidate until PRD-735 lands its cards. + +| # | Genre (aliases) | Kind | Candidate reference | +| --- | --- | --- | --- | +| 1 | 3D platformer — calibration row (mario, sm64) | revamp collectathon | `sm64` (Tier 1 today) | +| 2 | Boomer shooter / arena FPS (doom, quake) | new | id Software GPL sources | +| 3 | Open-world third-person action (gta) | new | `re3` | +| 4 | RTS (command and conquer, red alert) | revamp rts | OpenRA | +| 5 | Metroidvania (metroid, castlevania) | new | Super Metroid disassembly | +| 6 | Arena fighter (smash) | new | SSB64 decomp | +| 7 | Voxel survival / crafting (minecraft) | new | Luanti (Minetest) | +| 8 | Arcade racer (need for speed, ridge racer) | revamp racing | to be scouted (PRD-735 Phase 3) | +| 9 | Tower defense (bloons, kingdom rush) | revamp tower-defense | to be scouted | +| 10 | Endless runner (subway surfers, temple run) | revamp runner | to be scouted | + +Row 1 runs first on purpose: its reference already exists, so it calibrates the loop and sets the +KPI baseline while PRD-735 scouts the next references. + +## Blocked on + +- Rows 4..N (the author step) need the PRD-749 kit base on `pr563-followup` (PRD-749 Phase 3, the #574 lane). + +## Acceptance Criteria + +See the phase boxes. Each box names its proof. + +## Integration Ledger + +| Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | +| --- | --- | --- | --- | +| Kit-loop ledger row | `pnpm kit:loop record ` → `scripts/kit-loop.mjs` → `docs/kit-loop/ledger.json` | new | AC-1 | +| Kit KPI tab | `pnpm dashboard` → kit-loop tab reads the ledger | new tab in the PRD-751 hub | AC-2 | +| Kit found by genre words | cold agent → `engine_search_capabilities` / `--genre ` → kit | unchanged path, measured | AC-4 | + +## Decisions + +- 2026-10-26 (owner): an iteration is an in-repo author step plus a cold sandbox build; both transcripts are inspected. +- 2026-10-26 (owner): fix scope is the whole authoring layer; new engine systems stay out. +- 2026-10-26 (owner delegated: "do whatever you want"): K2's frame budget is the performance gate's own cost p95 (`performance.maxFrameMsP95` `cost.p95`), not `TN_FRAME_BUDGET` presented p95. On the runner's private Xvfb, presented p95 is display cadence (17.5 ms with a game that costs 10.03 ms), and the gate itself reports it as not gated. Rows keep presented p95 for information; rows 1–3 were recomputed from their saved inputs (row 1 stays over at 17.27 ms). +- 2026-10-26 (owner): KPIs are K1–K4 above; the play grade comes from an AI judge, not the owner. +- 2026-10-26 (owner): a ranked mix of revamps and new kits; PRD-735 (references at scale) lands before the loop runs past row 1. +- 2026-10-26 (owner): one epic PR for the loop and every kit; it merges at the release. +- 2026-10-26 (owner): target-plus-plateau stop rule, autonomous, one ping per kit. +- 2026-10-26 (agent): the retro lives in the ledger row, not in a file per iteration, so the dashboard shows it and nothing drifts. +- 2026-10-27 (owner asked: "check if its being triggered properly"): the reference search did not fire on the authoring path. A cold session starts outside the game, so `.mcp.json` never loads (`mcp_servers: []`). Sandbox-4 (after #579) made 0 engine calls in 36. Sandbox-5 saw a ready `engine_search_references` command in the next-steps line and skipped it ("the kit is already a full SM64-style game"). So `create-threenative --genre ` now runs the search through the installed shim and prints the top 5 mechanics with a ready `engine_reference_detail` command (a50cb8a7b). Sandbox-6: the search fires and the agent reads the mechanics; it opened 0 of them, because the kit already covered the request. Probes, not ledger rows: rows 4..N wait for the kit base. + +## Execution Phases + +#### Phase 1: The ledger and the measure step + +**Status:** DONE +**Files:** `scripts/kit-loop.mjs` (new), `scripts/__tests__/kit-loop.spec.ts` (new), `docs/kit-loop/ledger.json` (new), `package.json` (`kit:loop`). + +- [x] AC-1 [local]: `pnpm kit:loop record ` turns a cold-build transcript and its gate outputs into one ledger row with K1–K4, and a missing input fails closed (no zero, no pass). proof: `pnpm exec vitest run scripts/__tests__/kit-loop.spec.ts` — red (module missing), then 4/4 green; `pnpm kit:loop record /nonexistent ...` exits 1 with `transcript.json is missing`; the dir contract (five files) is in the script header (2026-10-26) +- [x] AC-2 [local]: the PRD-751 dashboard shows the kit-loop tab with each KPI against its baseline and target. proof: `pnpm exec vitest run scripts/__tests__/perf-live.spec.ts -t kit-loop` — green, see PRD-751 AC-4 (2026-10-26) + +#### Phase 2: Iteration 1 — calibration on row 1 (3D platformer) + +**Status:** PARTIAL — AC-4 done (ledger rows 1–3); AC-3 waits for the PRD-749 kit base +**Files:** the row-1 kit PRD, the collectathon kit, the ledger row. + +- [ ] AC-3 [local]: the author step revamps the collectathon kit on the kit base, with SM64 card citations, and its own kit PRD is green. proof: the kit PRD's gates + - Open (2026-10-26): the kit base is not on `pr563-followup`; only PRD-749 Phase 1 (the kit contract, `9d1a848a0`) landed. On collectathon the contract reads `explore-clean` ok, `camera-sees-subject` FAIL (`TN_PLAYTEST_SIGHT_LINE_BLOCKED`), `hud-clean` FAIL (`TN_PLAYTEST_HUD_CONTRAST`), `no-hitch` ok, `foot-slip` ok, three rows opted out. Both reds belong to PRD-749's per-kit red list (its rule: fix in the base, an engine node or a contract row), so this step waits for PRD-749 Phases 2–3. +- [x] AC-4 [local]: a cold sandbox agent prompted "make me a game like Super Mario 64" picks the collectathon kit and reaches a gate-green build; the ledger holds the baseline row with K1–K4 and the retro lists. proof: `pnpm kit:loop record `, ledger row 1 + - Done (2026-10-26): ledger row 1 holds the baseline with K1–K4 and the retro lists (K1 97,840 tokens, K2 21/22 + CLOSE, K3 7, K4 4; its performance gate was red on a loaded host). Rows 2 and 3 are cold builds with the same prompt and isolation that picked the collectathon kit on their first call and reached gate-green builds, rerun by the recorder: 22/22 scenarios each, performance gate cost p95 10.03 ms (row 2) and 12.59 ms (row 3) against 16.7 ms. The predeclared quiet-host rerun of row 1 is withdrawn: the host never got quiet in 2 h, and rows 2–3 settle the gate-green claim on the same kit at the high tier. + +#### Phase 3: Iterations 2..N to the stop rule + +**Status:** IN PROGRESS — rows 1 and 2 recorded; row 2's retro fixes (bac933cdc, 59d741907) landed for row 3 +**Files:** one kit PRD per row, the authoring-layer fixes each retro names, the ledger. + +- [ ] AC-5 [local]: each later iteration lands its top retro fixes before its author step, and its ledger row names them. proof: ledger rows 2..N, the fix commits + - Open (2026-10-26): row 2 is recorded and names row 1's fixes (a4d5345ef: a failed cost bound names the foreign GPU processes and says rerun; 42cbf971c + c5bc052e7: start the agent in the project, or call `engine.mjs ''`). Row 2 against row 1: K1 44,483 vs 97,840 tokens (−55%), 15 vs 37 turns, 7 vs 19 min; K2 22/22 vs 21/22, judge CLOSE both (fresh Opus judge: NEUTRAL row 2 vs row 1); K3 2 vs 7; K4 4 both (Hero.ts unchanged). The row 2 agent changed no code and did not chase performance; the gate rerun by the recorder passed at cost p95 10.03 ms. Row 2's retro fixes landed for row 3: `--scenario` takes a brace list (bac933cdc, red-green `bridgeClient.spec.ts`) and the scaffold line says CLAUDE.md copies AGENTS.md (59d741907, red-green `scaffold.spec.ts`). Row 3 names row 2's fixes and confirms them (AGENTS.md read without its CLAUDE.md copy, one glob run for 22 playtests): K1 47,107 (+6% vs row 2, AGENTS.md read twice because its 22 KB overflowed one tool result with other output), K2 22/22 + CLOSE (NEUTRAL vs row 2), K3 0, K4 4. The remaining frictions are kit-layer (the kit `AGENTS.md` size, no win state at three Sunseeds, the wall-kick camera that hides the hero, the K4 movement gaps), so rows 4..N wait for the PRD-749 kit base and the author step. The box stays open while rows 4..N run. + - K2 budget: decided 2026-10-26 (see Decisions); rows 2 and 3 meet it at cost p95 10.03 and 12.59 ms, row 1 is over at 17.27 ms. +- [ ] AC-6 [local]: the loop stops by the rule: the list is done, or two rows in a row gain under 5% on all of K1–K4. proof: the ledger, the stop line in this PRD +- [ ] AC-7 [local]: K1–K4 meet their targets at the last row, or the gap per KPI is recorded with its cause. proof: dashboard kit-loop tab, the last ledger row diff --git a/docs/asset-perf/index.html b/docs/asset-perf/index.html deleted file mode 100644 index 304dcd1e09..0000000000 --- a/docs/asset-perf/index.html +++ /dev/null @@ -1,79 +0,0 @@ - - -Asset pipeline performance -
-

Asset pipeline performance

-

Latest iteration #14 of 14: quiet measurement of the kept build (lever 6). Snapshot 2026-10-25T04:39:30.714Z. Regenerate with node scripts/asset-perf-loop.mjs report; the page is a static render of docs/asset-perf/ledger.json. Goal: the template corpus ships as few bytes as it can and preloads to GPU-ready as fast as it can, with no texture below its quality floor (PRD-729).

-

Progress toward the goal

-
- - - -
MetricBaselineNowGoalProgress
Bytes shipped for the template corpus5.37 MiB3.68 MiB≤ 3 MiB 71%open
Shipped bytes as a share of source bytes45.1%30.9%≤ 25 % 71%open
Preload to GPU-ready, localhost629 ms372 ms≤ 450 ms 100%met
Preload to GPU-ready, 40 Mbps / 20 ms1705 ms1162 ms≤ 1200 ms 100%met
-

Where the bytes go

-
KindFilesSourceShippedShipped/source
model67.04 MiB3.19 MiB45.3%
texture30.27 MiB0.27 MiB100.0%
audio64.59 MiB0.21 MiB4.7%
-
Largest shipped files, latest iteration (solid = shipped, pale = source)
shared/player-viewmodel.glb1.43 MiB
shared/mannequin-combat.glb0.81 MiB
sailing/ship.glb0.70 MiB
shared/sky.jpg0.27 MiB
shared/mannequin.glb0.25 MiB
rain/thunder.wav0.10 MiB
rain/rain-loop.wav0.05 MiB
rain/wind-loop.wav0.04 MiB
snow/wind.wav0.02 MiB
snow/crunch.wav0.01 MiB
starter/pickup.wav0.00 MiB
puzzle/native-proof.glb0.00 MiB
-

Trend per iteration

-
Bytes shipped for the template corpus (lower is better)
- - - -3 -goal ≤ 3 MiB -5 -#3: 5.37 MiB#3#5: 4.94 MiB#5#6: 4.42 MiB#6#7: 4.33 MiB#7#8: 4.33 MiB#8#9: 3.78 MiB#9#10: 3.68 MiB#10#11: 3.55 MiB#11#14: 3.68 MiB#14
Shipped bytes as a share of source bytes (lower is better)
- - - -25 -goal ≤ 25 % -45 -#3: 45.1%#3#5: 41.5%#5#6: 37.2%#6#7: 36.4%#7#8: 36.4%#8#9: 31.7%#9#10: 30.9%#10#11: 29.8%#11#14: 30.9%#14
Preload to GPU-ready, localhost (lower is better)
- - - -450 -goal ≤ 450 ms -1109 -#3: 629 ms#3#5: 830 ms#5#6: 621 ms#6#7: 684 ms#7#8: 1036 ms#8#9: 1109 ms#9#10: 569 ms#10#11: 416 ms#11#14: 372 ms#14
Preload to GPU-ready, 40 Mbps / 20 ms (lower is better)
- - - -1200 -goal ≤ 1200 ms -1892 -#3: 1705 ms#3#5: 1892 ms#5#6: 1545 ms#6#7: 1611 ms#7#8: 1838 ms#8#9: 1823 ms#9#10: 1322 ms#10#11: 1122 ms#11#14: 1162 ms#14
-

Iterations

-
#DateCommitChangeDecisionShippedShipped/sourcePreload localPreload 40 MbpsCook
#12026-10-246da4a6606default cook, what a game ships today
superseded: re-recorded as #3 with preload
arm default {}
baseline5.37 MiB45.1%pendingpending55.1 s
#22026-10-246da4a6606uncooked control, every byte as authored
arm none {"textures":"none","models":"none","audio":"none"}
control11.91 MiB100%pendingpending0.7 s
#32026-10-24ed1764ca1default cook with preload, what a game ships today
arm default {}
baseline5.37 MiB45.1%629 ms1705 ms39.5 s
#42026-10-244a7848209 dirtylever 1: meshopt FILTER (animation quaternion/exponential filters)
superseded: control used the same rebuilt code, so the pair was A/A; bytes 4.94 MiB are valid
arm default {} keeps keyframe times; packages/assets/src/passes/model.ts
provisional4.94 MiB41.5%799 ms2104 ms40.5 s
#52026-10-244a7848209 dirtylever 1: meshopt FILTER (animation quaternion/exponential filters)
arm default {} keeps keyframe times; packages/assets/src/passes/model.ts; control = baseline build 6da4a6606 | worst rotation error 0.712 deg vs 0.041 baseline, translation 0.97 mm vs 0; paired preload within noise (830 vs 798 ms, 1892 vs 1970 ms); -8.0% bytes
reject4.94 MiB41.5%830 ms1892 ms51.2 s
#62026-10-24a3ceb360d dirtylever 2: drop animation tracks that hold the rest pose
arm default {} packages/assets/src/passes/prune-rest-tracks.ts; control = baseline build 6da4a6606 | visual judge NEUTRAL (114 poses x2 runs, 35 isolated pixels over 8 levels, no connected region); mixer worst node diff <1e-5; templates gate 7/8: snow production-performance red 3 of 3 runs, snow ships no model and its cook is byte-identical to the control, not yet run on a clean develop build | visual judge NEUTRAL (114 poses x2 runs, 35 isolated pixels over 8 levels, no connected region); mixer worst node diff <1e-5; templates gate 7/8: snow production-performance red 3 of 3 runs, snow ships no model and its cook is byte-identical to the control, not yet run on a clean develop build | visual judge NEUTRAL (114 poses x2 runs, 35 isolated pixels over 8 levels, no connected region); mixer worst node diff <1e-5; templates gate 7/8: snow production-performance red 3 of 3 runs, snow ships no model and its cook is byte-identical to the control, not yet run on a clean develop build | visual judge NEUTRAL (114 poses x2 runs, 35 isolated pixels over 8 levels, no connected region); mixer worst node diff <1e-5; templates gate 7/8: snow production-performance red 3 of 3 runs, snow ships no model and its cook is byte-identical to the control, not yet run on a clean develop build
keep4.42 MiB37.2%621 ms1545 ms47.4 s
#72026-10-24a1d5b60ef dirtylever 3: drop spec-default JSON fields from the GLB (normalized false, byteOffset 0, interpolation LINEAR)
arm default {} 4.42 to 4.33 MiB, preload not worse vs paired control (local 684 vs 699 ms, 40 Mbps 1611 vs 1646 ms); 114 animated poses pixel-identical (0 differing pixels), so no judge subagent; 7 model-bearing templates pass the gate (95 scenarios, exit 0); snow has no model
keep4.33 MiB36.4%684 ms1611 ms35.6 s
#82026-10-24a97ccfe04lever 4: start Basis transcoder when the manifest lists Basis output
arm default {} Measured under host load 30-138 from other agents. The absolute 1036/1838 ms is not comparable to the quiet baseline; this lever was rejected because the transcoder loaded eagerly.
reject4.33 MiB36.4%1036 ms1838 ms40.3 s
#92026-10-24290f8f3f5 dirtylever 5: KTX2 only when it repays the Basis transcoder
arm default {} Measured under host load; the paired control on the same host was 1483/2086 ms and the candidate 1109/1823 ms. The quiet measurement of the kept build is iteration #14 (372/1162 ms). | Owner 2026-10-24 accepts +3.67 MB GPU texture memory on player-viewmodel for -0.55 MiB and faster preload; quiet rerun #14 meets both preload goals; the transcoder charge is now decided once per build (compileAssets), so a build that ships the transcoder anyway keeps KTX2 model maps.
keep3.78 MiB31.7%1109 ms1823 ms45.4 s
#102026-10-242999fbc35 dirtylever 6: JPEG rung for opaque colour maps
arm default {} Measured in a quiet window; paired control (lever 5) 619/1448 ms, candidate 569/1322 ms. Judge NEUTRAL; shooter and action-rpg gates pass on the quiet RTX.
keep3.68 MiB30.9%569 ms1322 ms30.5 s
#112026-10-24a847bd2fe dirtylever 7: standalone textures take the smallest passing JPEG (sky.jpg)
arm default {} 3.68 -> 3.55 MiB, but local preload rose in three paired runs (+16, +33, +23 ms, 4-8%) for a ~5% throttled gain; the loop rule rejects a lever that worsens the other metric.
reject3.55 MiB29.8%416 ms1122 ms34.3 s
#122026-10-24a847bd2fe dirtylever 7 rerun: standalone JPEG rung
superseded: rerun of the same rejected lever
arm default {}
provisional3.55 MiB29.8%459 ms1136 ms26.2 s
#132026-10-24a847bd2fe dirtylever 7 rerun 2: standalone JPEG rung
superseded: rerun of the same rejected lever
arm default {}
provisional3.55 MiB29.8%402 ms1115 ms25.2 s
#142026-10-24a847bd2fequiet measurement of the kept build (lever 6)
arm default {}
keep3.68 MiB30.9%372 ms1162 ms26.6 s
-
diff --git a/docs/ci-speed/index.html b/docs/ci-speed/index.html deleted file mode 100644 index 7949f06174..0000000000 --- a/docs/ci-speed/index.html +++ /dev/null @@ -1,83 +0,0 @@ - - -CI speed loop -
-

CI speed loop

-

Latest live iteration #36 of 37: current window refresh (develop since 2026-10-14). Snapshot 2026-10-17T21:56:44.620Z. Regenerate with node scripts/ci-speed-loop.mjs report, then reload; the page is a static render of docs/ci-speed/ledger.json. Goal: a change merges within 30 minutes of its last push (PRD-550).

-

Progress toward the goal

-
- - - - -
MetricBaselineNowGoalProgress
Last push to merge, median280.4 min147.8 min≤ 30 min 53%open
Last push to merge, p90838.3 min443 min≤ 45 min 50%open
Merge queue passes on the first attempt33.3%50%≥ 90 % 29%open
Merge-group board wall time, median77.8 min46 min≤ 15 min 51%open
Runner queue wait, p9041.4 min18.2 min≤ 3 min 60%open
-

Trend per iteration

-
Last push to merge, median (lower is better)
- - - -30 -goal ≤ 30 min -745 -#1: 280.4 min#1#5: 474.3 min#5#6: 475.9 min#6#7: 475.9 min#7#8: 475.9 min#8#9: 475.9 min#9#10: 475.9 min#10#11: 475.9 min#11#12: 475.9 min#12#13: 475.9 min#13#14: 475.9 min#14#15: 475.9 min#15#16: 477.4 min#16#17: 477.4 min#17#18: 744.5 min#18#19: 475.9 min#19#20: 474.3 min#20#36: 147.8 min#36
Last push to merge, p90 (lower is better)
- - - -45 -goal ≤ 45 min -1130 -#1: 838.3 min#1#5: 474.3 min#5#6: 477.1 min#6#7: 477.1 min#7#8: 477.1 min#8#9: 477.1 min#9#10: 477.1 min#10#11: 477.1 min#11#12: 477.1 min#12#13: 477.1 min#13#14: 477.1 min#14#15: 477.1 min#15#16: 1023.9 min#16#17: 1023.9 min#17#18: 1130.1 min#18#19: 1090.9 min#19#20: 1071.3 min#20#36: 443 min#36
Merge queue passes on the first attempt (higher is better)
- - - -90 -goal ≥ 90 % - -#1: 33.3%#1#5: 0%#5#6: 50%#6#7: 50%#7#8: 50%#8#9: 50%#9#10: 50%#10#11: 50%#11#12: 50%#12#13: 50%#13#14: 50%#14#15: 50%#15#16: 40%#16#17: 40%#17#18: 33.3%#18#19: 25%#19#20: 22.2%#20#36: 50%#36
Merge-group board wall time, median (lower is better)
- - - -15 -goal ≤ 15 min -119 -#1: 77.8 min#1#2: 119.1 min#2#3: 113.3 min#3#4: 113.3 min#4#5: 111.6 min#5#6: 109.9 min#6#7: 109.9 min#7#8: 109.9 min#8#9: 109.9 min#9#10: 109.9 min#10#11: 102.5 min#11#12: 95.1 min#12#13: 95.1 min#13#14: 95.1 min#14#15: 95.1 min#15#16: 95.1 min#16#17: 95.1 min#17#18: 107.9 min#18#19: 107.9 min#19#20: 95.1 min#20#36: 46 min#36
Runner queue wait, p90 (lower is better)
- - - -3 -goal ≤ 3 min -51 -#1: 41.4 min#1#2: 50.9 min#2#3: 49.9 min#3#4: 46.9 min#4#5: 47.2 min#5#6: 45 min#6#7: 42.9 min#7#8: 41.5 min#8#9: 40.7 min#9#10: 39 min#10#11: 38.9 min#11#12: 38.7 min#12#13: 38.5 min#13#14: 38.7 min#14#15: 38.6 min#15#16: 36.9 min#16#17: 41.5 min#17#18: 42.3 min#18#19: 42 min#19#20: 39.6 min#20#36: 18.2 min#36
First failing job per red merge group, latest iteration
native-platforms / Desktop web/native parity2
native-platforms / Windows desktop core1
-

Iterations

-
#DateCommitChangeDecisionWindow fromPush→merge p50Push→merge p90First attemptBoard wallQueue wait p90
#12026-10-0911cafc003baseline: exhaustive board on every merge group
Before PRD-550. 2026-10-08 to 2026-10-09 window, same period as the PRD Context.
baseline2026-10-08280.4838.333.3%77.841.4
#22026-10-0990dee0c83no change landed: the queue still runs the exhaustive board
Window since 2026-10-09. 0 PRs merged, so push->merge, p90 and first-attempt are n/a; board wall and queue wait measure the exhaustive merge-group board this change removes. Iteration #1 stays the baseline. Re-measure once 5 or more PRs merged.
provisional2026-10-09n/an/an/a119.150.9
#32026-10-09a47851026phase 1 in review
Same window as iteration 2: 0 PRs merged, 9 merge-group runs, board median 113.3 min, queue wait p90 49.9 min. Named cost: integration / velocity was the first failing job in 2 of 4 red merge groups, because its cost arm priced shared-runner load as render cost. The change is PRD-550 phase 1 (481824f3c, a47851026) on PR 476. It moves that arm to a scheduled lane and widens two compile budgets. The compile specs pass locally, 40 of 40 in 220 s. Re-measure after it merges. The PR head also shows an intermittent iOS simulator red, seen in 2 of 10 recent runs with different errors, on code this branch does not change.
provisional2026-10-09n/an/an/a113.349.9
#42026-10-096ec34f52cPR 476 green on phase 1 head
Same window: 0 PRs merged, 12 merge-group runs. PR 476 head 6ec34f52c is green: 69 checks pass and 12 skip, and merge state is CLEAN. integration / velocity passes on that head, and the iOS simulator job passes there too, so the iOS red from iteration 3 did not recur. Merge-group metrics do not move until phase 1 reaches develop. Named costs still on develop: integration / velocity and test-unit (3/4), each first in 2 red merge groups. The compile-spec budget change (a47851026) targets the test-unit timeouts. Re-measure after the merge.
provisional2026-10-09n/an/an/a113.346.9
#52026-10-093ca761875hourly refresh
One PR merged since the last tick: 458, last push to merge 474.3 min, first attempt failed at test-unit (2/4). The window now has 13 merge-group runs, board median 111.6 min, and queue wait p90 47.2 min over 885 jobs. First failing job in the red groups: integration / velocity 2, test-unit (3/4) 2, test-unit (2/4) 1, integration / capture 1, test-playtest 1. PR 476 is still open, so phase 1 is not on develop yet. Provisional: fewer than 5 PRs merged.
provisional2026-10-09474.3474.30%111.647.2
#62026-10-097789bf328hourly refresh
One new PR merged since the last tick, 470, after 4 merge-group attempts. Its first failing job was integration / velocity. The window now has 2 merged PRs, 470 and 458. Last push to merge median 475.9 min, p90 477.1 min. First attempt merge 50 percent. 16 merge-group runs, board median 109.9 min, queue wait p90 45 min over 1086 jobs. First failing job in the red groups: integration / velocity 2, test-unit (3/4) 2, integration / capture 1, template-nonvisual platformer 1. PR 476 is still open, so phase 1 is not on develop yet. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%109.945
#72026-10-09aa13e02c1hourly refresh
No new PR merged since the last tick. The window still has 2 merged PRs, 470 and 458. It now has 18 merge-group runs, up from 16. Board median 109.9 min. Queue wait p90 42.9 min over 1227 jobs. Run success fell to 31.3 percent. First failing job in the 8 red groups: integration / velocity 4, test-unit (3/4) 2, integration / capture 1, template-nonvisual platformer 1. integration / velocity is the largest named cost, and phase 1 on PR 476 targets it. PR 476 is still open, so phase 1 is not on develop yet. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%109.942.9
#82026-10-099f36ac3c8hourly refresh
No new PR merged since the last tick. The window still has 2 merged PRs, 470 and 458. It now has 20 merge-group runs, up from 18. Run success fell to 27.8 percent. Board median 109.9 min. Queue wait p90 41.6 min over 1376 jobs. First failing job in the red groups: integration / velocity 4, test-unit (3/4) 3, test-browser 2, integration / capture 1. test-browser is new in this list. integration / velocity is still the largest named cost, and phase 1 on PR 476 targets it. PR 476 is still open. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%109.941.5
#92026-10-09cbd2e662ehourly refresh
No new PR merged since the last tick. The window still has 2 merged PRs, 470 and 458. It now has 24 merge-group runs, up from 20. Run success fell to 23.8 percent. Board median 109.9 min. Queue wait p90 41.2 min over 1579 jobs. First failing job in the red groups: integration / velocity 4, test-unit (3/4) 4, test-browser 2, integration / auto-exposure cold boot 1. integration / velocity and test-unit (3/4) are tied as the largest named costs. Phase 1 on PR 476 targets integration / velocity, and its compile-spec budget change targets test-unit timeouts. PR 476 is still open. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%109.940.7
#102026-10-092e100adc4hourly refresh
No new PR merged since the last tick. The window still has 2 merged PRs, 470 and 458. It now has 26 merge-group runs, up from 24. Run success fell to 20.8 percent, the fourth straight drop. Board median 109.9 min. Queue wait p90 39 min over 1785 jobs, down from 41.2. First failing job in the red groups: test-unit (3/4) 5, integration / velocity 4, test-browser 2, integration / auto-exposure cold boot 1. test-unit (3/4) is now the largest named cost. Phase 1 on PR 476 includes a compile-spec budget change for test-unit timeouts, and it is not on develop yet. PR 476 is still open. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%109.939
#112026-10-09f9a01bbe4hourly refresh
No new PR merged since the last tick. The window still has 2 merged PRs, 470 and 458. It now has 29 merge-group runs, up from 26. Run success is 22.2 percent, up from 20.8. Board median is 102.5 min, down from 109.9. Queue wait p90 is 38.4 min over 1993 jobs. First failing job in the 12 red groups: test-unit (3/4) 5, integration / velocity 4, test-browser 2, golden-path template platformer 1. The named costs are unchanged. A separate develop push run at b12b257f1 failed in test-native, and it is not counted in these groups. PR 476 is still open. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%102.538.9
#122026-10-09f1e868d47hourly refresh
Catch-up tick after the 09:00 stop. No new PR merged. The window still has 2 merged PRs, 470 and 458. It now has 31 merge-group runs, up from 29. Run success is 24.1 percent, up from 22.2. Board median is 95.1 min, down from 102.5. Queue wait p90 is 38.7 min over 2118 jobs. First failing job in the 13 red groups: test-unit (3/4) 5, integration / velocity 4, test-browser 2, test-unit (1/4) 2. test-unit (1/4) is new in this list. The named costs are unchanged. PR 476 is still open. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%95.138.7
#132026-10-091310367b6hourly refresh
No new PR merged since the last tick. The window still has 2 merged PRs, 470 and 458. It now has 33 merge-group runs, up from 31. Run success is 22.6 percent, down from 24.1. Board median is 95.1 min, unchanged. Queue wait p90 is 38.5 min over 2258 jobs. First failing job in the 14 red groups: integration / velocity 5, test-unit (3/4) 5, test-browser 2, test-unit (1/4) 2. integration / velocity rose by one. PR 476 is still open. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%95.138.5
#142026-10-096639a5bafhourly refresh
No new PR merged since the last tick. The window still has 2 merged PRs, 470 and 458. It now has 35 merge-group runs, up from 33. Run success is 21.2 percent, down from 22.6. Board median is 95.1 min, unchanged. Queue wait p90 is 38.7 min over 2396 jobs. First failing job in the 15 red groups: integration / velocity 5, test-unit (3/4) 5, test-unit (2/4) 3, integration / auto-exposure cold boot 2. The list changed: test-unit (2/4) and auto-exposure cold boot appear, and test-browser and test-unit (1/4) drop out. The first attempt hit a transient GitHub 502, and the rerun succeeded. Develop still sits at b12b257f1, so the queue has no new merges. PR 476 is still open. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%95.138.7
#152026-10-094c1bbd890hourly refresh
No new PR merged since the last tick. The window still has 2 merged PRs, 470 and 458. Develop still sits at b12b257f1, so the queue has no new merges. It now has 36 merge-group runs, up from 35. Run success is 20 percent, down from 21.2. Board median is 95.1 min, unchanged. Queue wait p90 is 38.6 min over 2476 jobs. First failing job in the 16 red groups: integration / velocity 5, test-unit (3/4) 5, test-browser 3, test-unit (2/4) 3. test-browser is back in the list. PR 476 is still open. Provisional: fewer than 5 PRs merged.
provisional2026-10-09475.9477.150%95.138.6
#162026-10-094d81de73chourly refresh
The window merged five PRs, which are 458, 470, 471, 473, and 468, and this matches the merged PR line that shows 5 PRs. The window held 42 merge group runs, with a run success of 19.4 percent and a first attempt merge of 40 percent. The merge group board median wall was 95.1 minutes, and the last push to merge median was 477.4 minutes with a p90 of 1023.9 minutes. The runner queue wait p90 was 36.9 minutes over 2845 jobs. The first failing job counts, largest first, were 5 for integration / velocity, 5 for test-unit (3/4), 4 for test-unit (2/4), 3 for test-browser, 2 for integration / auto-exposure, 2 for integration / auto-exposure cold boot, 2 for test-unit (1/4), and 1 for each of golden-path-template (platformer), integration / capture, native-platforms / iOS simulator runtime and no-Xcode consumer handoff, native-platforms / macOS desktop core, template-nonvisual (platformer), and test-playtest. Develop moved from b12b257f1 to 6b18d913e, and its CI run on that head was still queued when this row was written. None of these PRs carries the phase 1 change on PR 476, which is still open, so this window cannot judge that change, and the decision is inconclusive.
inconclusive2026-10-09477.41023.940%95.136.9
#172026-10-090a4ce848chourly refresh
Merged into develop since 2026-10-09 are 5 PRs, 473, 471, 470, 468 and 458. The merge-group run count is 42 and the run success percent is 18.4 with the first-attempt merge percent at 40. The board median wall is 95.1 minutes, and the last push to merge median is 477.4 minutes and p90 is 1023.9 minutes. The queue wait p90 is 40.4 minutes over 2873 jobs. The first failing job counts are 6 for integration / velocity, 5 for test-unit (2/4), 5 for test-unit (3/4), 3 for test-browser, 2 for integration / auto-exposure, 2 for integration / auto-exposure cold boot, 2 for test-unit (1/4), and 1 each for golden-path-template (platformer), integration / capture, native-platforms / iOS simulator runtime and no-Xcode consumer handoff, native-platforms / macOS desktop core, template-nonvisual (platformer) and test-playtest. Develop head is 6b18d913e and its CI run has finished with success. None of these PRs carry the phase 1 change on PR 476, which is still open, so this window cannot judge that change, and local commit 0a4ce848c adds a test only diagnostic and does not change any merge-group result, giving an inconclusive decision.
inconclusive2026-10-09477.41023.940%95.141.5
#182026-10-101652670eePR476 unmerged baseline refresh
PR476 remains unmerged. This is a fresh observation of develop, not a measured effect of PR476. Candidate CI run 38025989864 failed sailing diagnostics; ci-required correctly rejected it.
provisional2026-10-09744.51130.133.3%107.942.3
#192026-10-1077bb0ee57PR476 candidate 77bb in CI — unmerged baseline
Fresh develop baseline while PR476 remains unmerged. CI run38033595736 targets candidate77bb; verification is pending. This audit does not measure PR476 effect.
provisional2026-10-09475.91090.925%107.942
#202026-10-1077bb0ee57Current queue comparison — PR476 still unmerged
Refreshed after user requested current queue comparison and continued speed work. PR476 remains unmerged; no measured impact attribution.
provisional2026-10-09474.31071.322.2%95.139.6
#212026-10-1646c59e6b5historical reference: develop-push full board run 38824021621 (PRD-620 baseline)
Historical reference only: develop-push full-board run 38824021621 at 46c59e6b5, full wall 76m41s (4601s) from run_started_at 2026-10-15T23:30:48Z to 2026-10-16T00:47:29Z, including initial scope queue; job execution envelope 4553s is retained separately. V8 published-payload warm hit (job 116727720587, receipt check 405ms). Recipe differs in download-deps.mjs and PR508 changed workloads, so not a comparable current-candidate control; decision invalid, excluded from charts.
invalid2026-10-15n/an/an/an/an/a
#222026-10-16ad25e3cf3terminal diagnostic: develop-push full board run 38846474672 (PRD-620 cold V8 + cancelled; lint fixed)
Bounded diagnostic run terminated cancelled after lint failure and cold V8; retained failed lint + cancelled statuses honestly; later V8 payload asset available durably.
invalid2026-10-15n/an/an/an/an/a
#232026-10-16f02cc9e43hosted control: full-board workflow_dispatch run 38853722930 at f02cc9e43 (PRD-620 warm V8; cancelled after lint failure)
Hosted control only, not a qualifying sample. workflow_dispatch full-board run 38853722930 at f02cc9e43 (candidate f02cc9e43e04d13eebb3ae07710dc59e9a2d862f), run attempt 1, conclusion cancelled. V8 source payload job 116820922299 completed success with a warm published download (1708ms) matching recipe 1a2463b539e99c9fd7d87bd4bdd3ef86f2b61e42969a587f134e35f23d343607; all four recipe inputs are unchanged from run 38846474672, so the population is warm; no publisher mutation occurred. Lint job 116820830952 failed solely on Biome formatting of multiline JSON arrays in the existing ledger (fixed in this commit). The run was cancelled early, so the wall is short: overall run_started_at 2026-10-16T05:02:42Z to last completed job 2026-10-16T05:23:50Z is 1268s (21.1 min); the job envelope min started_at 2026-10-16T05:02:46Z to max completed_at 2026-10-16T05:23:50Z is 1264s, retained separately. All 72 job rows are embedded including labels, runner names, statuses, candidate and run attempt. Invalid, chart-excluded; generic merge-queue metrics null; no acceptance claim.
invalid2026-10-15n/an/an/an/an/a
#242026-10-16f8b2680a9matched hosted control: full-board workflow_dispatch run 38856119345 at f8b2680a9 (PRD-620 warm V8; all hosted, complete success incl. ci-required)
Matched hosted control, not a routed after-run sample: every job ran hosted, so no tn-local placement or routing gain is claimed. workflow_dispatch full-board run 38856119345 at f8b2680a9 (candidate f8b2680a99420ec49327fd009858c8dee07df293), run attempt 1, conclusion success. V8 source payload job 116828110559 completed success with a warm published download matching recipe 1a2463b539e99c9fd7d87bd4bdd3ef86f2b61e42969a587f134e35f23d343607 (published step 3307ms); all four recipe inputs are unchanged across ad25e3cf3/f02cc9e43/f8b2680, so the population is warm; no publisher mutation occurred. The run is full green including ci-required (job 116840273034, selection full, native matrix full, coverage receipts candidate f8b2680a99420ec49327fd009858c8dee07df293 runId 38856119345 attempt 1), which verifies AC-2's full-green criterion on this candidate. Whole wall: run_started_at 2026-10-16T05:30:20Z to last completed job 2026-10-16T06:16:24Z is 2764s (46.1 min); the job envelope min started_at 2026-10-16T05:30:24Z to max completed_at 2026-10-16T06:16:24Z is 2760s, retained separately. All 72 job rows are embedded with status, labels, runner names, created/started/completed timestamps, queue and duration, and a canonical class per row (46 HEAVY + 12 LIGHT + 14 HOSTED/skipped). The full-scope board is 72 rows from the final jobs API, not the 71 first expected. The AC-1 eligible population is the canonical HEAVY set: 30 keys / 46 instances (heavy queue p50 160.5s, p90 647.5s over all 46 including hosted overflow, not filtered by actual runner and not only the 19 new keys / 32 instances). Build/cache comparability remains unverified: native-ccache and native-third-party restore state is not measured here and differs per run key. Control only, chart-excluded; generic merge-queue metrics null; no acceptance claim.
invalid2026-10-15n/an/an/an/an/a
#252026-10-16f8b2680a9routed after-run attempt 1: full-board workflow_dispatch run 38861158440 at f8b2680a9 (PRD-620; all 46 HEAVY tn-local, cancelled after hosted iOS failure + local overload)
Routed after-run attempt 1, nonqualifying: full-board workflow_dispatch run 38861158440 at f8b2680a99420ec49327fd009858c8dee07df293 (same candidate and V8 recipe as iteration 24), run attempt 1, conclusion cancelled. All 46 canonical HEAVY instances routed tn-local (labels ['tn-local'], 0 hosted): 34 started, 12 cancelled before a slot. The run ended when the hosted iOS leg (job 116844117629, macos-15) failed with 'Expected exit 2 containing TN_PLAYTEST_BRIDGE_MISSING, got 0'; because the same-candidate control (iteration 24) passed this leg, the cause is UNVERIFIED and no stale-bundle or environment cause is asserted. Root then cancelled the run; cancellation cascaded into three post-cancellation failures that are consequences, not independent defects: integration required-receipts (116853277982), native required-coverage receipt (116853579336, missing release-reports leg) and ci-required (116853970840). The routed local V8 source payload (job 116844117556, tn-local) succeeded with a warm published download (v8.published 1777ms) on the unchanged recipe 1a2463b539e99c9fd7d87bd4bdd3ef86f2b61e42969a587f134e35f23d343607; no publisher mutated. Whole wall run_started_at 2026-10-16T06:25:53Z to the latest job completed_at 2026-10-16T07:02:06Z is 2173s (36.2 min); the job envelope min started_at to max completed_at is 2169s, retained separately; the run's terminal updated_at 2026-10-16T07:02:07Z is metadata only. Started-only eligible HEAVY queue (population 34 of 46; 12 cancelled-before-start excluded; cancelled/unstarted fake-0 queues NOT used): p50 629s, p90 1435s, max 1584s. Because the run is cancelled this is not an AC-1 sample and makes no claim. The mid-run snapshot (/tmp/prd620-after1-snapshot-*, 62 rows) is snapshot ONLY: 30 of 46 eligible HEAVY started with queue p50 568s and 20/46 >120s; 20 < 24, so the full-median-must-fail bound was not met and the eventual full median stayed UNKNOWN, not predicted. Pool: balancer logged TN_RUNNER=off @06:28:06 (heavy busy withdrawal) and TN_RUNNER_LIGHT=on @06:17:15 (LIGHT separated); the run's static tn-local assignment held despite the withdrawal. Runner API snapshot: 14 registered, heavy 5 online busy / 0 online idle (+3 offline), light 3 online idle (+3 offline); initial repo showed 6 heavy idle across unknown hosts, later 5 online busy / 0 idle, so the heavy pool is not only 3 total slots. All 72 job rows are embedded with status, labels, runner names, created/started/completed timestamps, queue/duration and a canonical class per row (46 HEAVY + 12 LIGHT + 14 HOSTED/skipped); iteration 24's rows are preserved. Build/cache comparability to the matched hosted control remains UNKNOWN: native-ccache and native-third-party restore state is not measured; only the V8 payload warm published hit is comparable. Invalid, chart-excluded; generic merge-queue metrics null; no acceptance or routing claim.
invalid2026-10-15n/an/an/an/an/a
#262026-10-16057f5626dhosted control: full-board workflow_dispatch run 38938825798 at 057f56 (PRD-620; all-hosted cancelled control after stale native contract + UI budget; not a routed after-run)
Invalid hosted control, chart-excluded. Full-board workflow_dispatch run 38938825798 at candidate 057f5626dc8598f5161193c35c9b21246fab9ebc (PR #513 ci/prd620-local-first, run attempt 1, conclusion cancelled, event workflow_dispatch, not a merge-group window). Every one of the 72 jobs ran hosted (46 canonical HEAVY instances all on ubuntu-24.04; 0 tn-local), so it is a control, not a routed after-run: no routing gain is claimed and no green is claimed. The run was cancelled after three named failures: (1) supply-chain job 117101384474 was cancelled during `Run actions/checkout@v7` while the initial fetch hung - fetch began 17:20:45Z and the runner logged `##[error]The operation was canceled.` at 17:30:44Z, about 10 min; (2) test-native job 117102568261 failed step `Run pnpm --filter @threenative/runtime-native test` because the native starter-row contract regex was stale: AssertionError at tests/starter-desktop.test.mjs:395 against the `native:starter-linux:linux-x64` admission clause in native-platforms.yml - now fixed by commit 2b204ea70 (`test(ci): match bounded native runner admission`); (3) native-platforms / Desktop web/native parity job 117104138305 failed step `Verify the web UI composites into the game's own frame` with `TN_FRAME_BUDGET_PHASE_OVER_BUDGET: the 'ui' phase is 2.36 ms at p95 over 12 frame(s), which is not under 2 ms` - cause UNKNOWN, not asserted. The remaining cancelled/failed jobs are cancellation consequences, not independent defects. Wall: job envelope max completed_at 17:55:32Z minus min started_at 17:19:59Z = 2133s (35.6 min); full board max completed_at 17:55:32Z minus run_started_at 17:19:54Z = 2138s (35.6 min), NOT updated_at 17:55:33Z. Both are retained for an invalid, nonqualifying run and make no AC-1 claim; targets stay full board <=60 min wall and eligible HEAVY queue p50 <=2 min over all 46 instances, unchanged. Eligible HEAVY queue over the 46 all-hosted instances (hosted overflow included by definition): p50 140.5s, p90 683.5s, max 741s, 25/46 >120s - recorded as a hosted-control population, not an AC-1 sample. All 72 job rows are embedded with status, labels, runner names, created/started/completed timestamps, queue/duration and a canonical class per row (46 HEAVY + 12 LIGHT + 14 HOSTED/skipped); classification is unchanged from iterations 24-25. Build/cache comparability and V8 warm/cold status are not re-measured in this cancelled control. No publisher mutated. Generic merge-queue metrics null; prs empty.
invalid2026-10-15n/an/an/an/an/a
#272026-10-162b204ea70hosted control: full-board workflow_dispatch run 38943971312 at 2b204e (PRD-620; all-hosted successful control incl. ci-required; not a routed after-run)
Valid all-hosted matched hosted control, chart-excluded (not a routed after-run). Full-board workflow_dispatch run 38943971312 at candidate 2b204ea704ba9deb567889476248bc925545d94e (PR #513 ci/prd620-local-first), run attempt 1, conclusion success, event workflow_dispatch, not a merge-group window. Every one of the 72 jobs ran hosted: all 46 canonical HEAVY instances on ubuntu-24.04, 0 tn-local, so it is a control, not a routed after-run, and claims no routing gain. Full green incl. ci-required: job 117144412209 success, step `Reject every unsuccessful or missing selected job` success; all 46 HEAVY success (69 success + 3 skipped in total; the skipped legs are the native-platforms V8 publish/payload jobs). Candidate 2b204e carries the startup REST fix 057f56 (`fix(ci): resolve runner repository through REST`, verified 17/17 in scripts/__tests__/ci-runners.spec.ts) and the native contract fix 2b204e (`test(ci): match bounded native runner admission`, verified 33/33 in packages/runtime-native/tests/starter-desktop.test.mjs), so this candidate is NOT unchanged versus the older controls (iterations 24-26). Wall: full board max completed_at 19:02:18Z minus run_started_at 18:00:09Z = 3729s (62.2 min); job envelope max completed_at minus min started_at 18:00:55Z = 3683s (61.4 min); both computed from run_started_at / min job started_at / max job completed_at, NOT the run updated_at 19:02:19Z. The full control wall exceeds 60 min; as a baseline control it makes no AC-1 claim and the target <=60 min wall applies to the routed after-runs. Eligible HEAVY queue over all 46 hosted instances (hosted overflow included by definition): p50 623.0s, p90 1147.0s, max 1277s, 41/46 >120s - a hosted-control population, not an AC-1 sample. Build/cache comparability is not claimed: native-ccache and native-third-party restore state is not measured and the V8 warm/cold hit is unverified without the step log. The four recipe input blobs at 2b204e equal those at 057f56/f8b2680, but the candidate itself changed, so no unchanged-candidate claim holds. No publisher mutated. Generic merge-queue metrics null; prs empty. All 72 job rows are embedded with status, labels, runner names, created/started/completed timestamps, queue/duration and a canonical class per row (46 HEAVY + 12 LIGHT + 14 HOSTED/skipped); classification is unchanged from iterations 24-26.
invalid2026-10-15n/an/an/an/an/a
#282026-10-162b204ea70routed after-run attempt 1: full-board run 38952177095 at 2b204e (PRD-620; force-cancelled after a git-core PPA 503 broke playwright install-deps before the native tests; nonqualifying)
Invalid routed after-run, chart-excluded. Full-board workflow_dispatch run 38952177095 at candidate 2b204ea704ba9deb567889476248bc925545d94e (PR #513 ci/prd620-local-first, run attempt 1, conclusion cancelled, event workflow_dispatch, not a merge-group window). Actual routing: 6 of the 46 canonical HEAVY instances ran tn-local (test, test-native, test-unit 1/4-4/4) and 40 ran hosted ubuntu-24.04 - the bounded admission plan admitted the 6 idle heavy slots the balancer published and overflowed the surplus to hosted, so it is a routed run with hosted overflow, not an all-local one. First real failure was not a test: test-native job 117147306782 (tn-local) failed step 8 `Run ./.github/actions/playwright-chromium` because `playwright install-deps chromium` exited 1 at 19:11:33Z fetching the git-core PPA (`.linchpin/prd620-routed-native-failure.log:1085` `E: Failed to fetch https://ppa.launchpadcontent.net/git-core/ppa/ubuntu/dists/noble/main/binary-amd64/Packages 503 Service Unavailable`, then `Failed to install browser dependencies` / `Process completed with exit code 1`). The jobs API step outcomes confirm the native work never ran: `Install Linux desktop build dependencies`, `Build the native host` (native build), `Run ./.github/actions/workspace-dist` and `Run pnpm --filter @threenative/runtime-native test` (tests) are all SKIPPED. The always-collector step 24 `Collect bounded native Linux performance evidence` then failed with TN_PROD_SCAFFOLDER_UNAVAILABLE (installed create-threenative lacks the built scaffolder); this is a cascade of the same broken job, not a missing workflow build, and its evidence upload still produced artifact 12069333011 (native-performance-evidence, 581 bytes). The prior stale native-contract fix 2b204ea70 (`test(ci): match bounded native runner admission`, 33/33) remains green and is not contradicted - the contract test was skipped here before it could run. Root first requested an ordinary cancel, then issued a force-cancel because the run did not reach a terminal state after about 10 minutes. No engine or workflow change is justified by this run; the same SHA was retried as run 38953871463 (routed after-run sample still open). Post-cancellation failures are consequences, not independent defects: integration / Verify required integration receipts 117149844258, native-platforms / Native required coverage receipt 117151003072 and ci-required 117151263607 (its own step left null/cancelled). Wall: job envelope max completed_at 19:17:15Z minus min started_at 19:04:27Z = 768s (12.8 min); full board max completed_at minus run_started_at 19:04:23Z = 772s (12.9 min), NOT the run updated_at 19:17:15Z. Both are retained for an invalid, nonqualifying run and make no AC-1 claim; targets stay full board <=60 min wall and eligible HEAVY queue p50 <=2 min over all 46 instances, for two comparable warm routed after-runs. Eligible HEAVY queue over the 32 started of 46 (14 cancelled before a slot; fake-0 queues NOT used): p50 4.0s, p90 185.7s, max 285s, 6/46 >120s - a partial, cancelled population, NOT an AC-1 acceptance sample. The run reported 71 job rows, not the expected 72: the LIGHT `run-summary` row was never created because the run was force-cancelled. All 71 raw rows are embedded with status, labels, runner names, created/started/completed timestamps, queue/duration and a canonical class per row (46 HEAVY + 11 LIGHT + 14 HOSTED/skipped). No publisher mutated. Generic merge-queue metrics null; prs empty.
invalid2026-10-15n/an/an/an/an/a
#292026-10-162b204ea70qualifying routed after-run sample 1: full-board workflow_dispatch run 38953871463 at 2b204e (PRD-620; 6 tn-local HEAVY + 40 hosted overflow, full green incl. ci-required; wall 39m05, eligible queue p50 79s)
Qualifying routed after-run sample 1 of 2, chart-excluded. Full-board workflow_dispatch run 38953871463 at candidate 2b204ea704ba9deb567889476248bc925545d94e (PR #513 ci/prd620-local-first, run attempt 1, conclusion success, event workflow_dispatch, not a merge-group window). Routing under the published runner_admission plan (scope job 117151729683 step 4 `Admit the eligible heavy instances to the idle pool` success): 6 of the 46 canonical HEAVY instances ran tn-local (test 117152538905, test-native 117153020449, test-unit 1/4 117153020796, 2/4 117153020393, 3/4 117153020398, 4/4 117153021044) and 40 ran hosted ubuntu-24.04 - the 6 idle heavy slots the balancer published, with the surplus overflowed to hosted; the 12 LIGHT joins ran tn-local-light. So the run is routed with hosted overflow, not all-local. Full green incl. ci-required (job 117166645960, tn-local-light) with all 46 canonical HEAVY successful (72 rows: 46 HEAVY + 12 LIGHT + 14 HOSTED/skipped; 69 success + 3 skipped, the skipped legs the V8 publish/payload jobs). Wall: full board max completed_at 2026-10-16T19:56:58Z minus run_started_at 2026-10-16T19:17:53Z = 2345s (39.1 min, 39m05); job envelope max completed_at minus min started_at 19:17:57Z = 2341s; NOT the run updated_at 19:56:59Z. AC-1 metrics on this sample: full board 2345s <= 60 min and eligible HEAVY queue p50 79s (p90 381s, max 520s, 20/46 >120s) <= 2 min, both over all 46 canonical instances including hosted overflow. This is sample 1 of the >=2 comparable after-runs AC-1 requires; the second sample (run 38958785956, same candidate) is underway, so no two-sample acceptance is claimed. Comparability vs the all-hosted control iteration 27 (same candidate 2b204e, same event, same workload): the Android V8 recipe is unchanged at 1a2463b539e99c9fd7d87bd4bdd3ef86f2b61e42969a587f134e35f23d343607 and warm (published step 3359ms vs control 1338ms); native cache is comparable - test-native restored native-third-party cache HIT (key native-third-party-Linux-X64-ae97fc39...) and native-ccache MISS (per-run key) on BOTH the routed run and the control - so the sample is comparable on candidate/recipe/workload/cache and the wall/queue win is a real before/after on this candidate; but only 6/46 HEAVY ran local and the two runs ran at different times, so the improvement is not attributable to local routing alone and no routing-only gain is claimed. Old invalid controls/after-runs (iterations 25, 26, 28) remain chart-excluded and are not counted. All 72 job rows embedded (46 HEAVY + 12 LIGHT + 14 HOSTED/skipped, 69 success + 3 skipped) with the exact canonical per-row classification from iterations 27-28; generic merge-queue metrics null; prs empty.
invalid2026-10-15n/an/an/an/an/a
#302026-10-162b204ea70routed after-run attempt 1: full-board run 38958785956 at 2b204e (PRD-620; scope checkout of all branches+tags exceeded the 5-minute limit and was cancelled before any product job ran; nonqualifying)
Invalid routed after-run, chart-excluded. Full-board workflow_dispatch run 38958785956 at candidate 2b204ea704ba9deb567889476248bc925545d94e (PR #513 ci/prd620-local-first, run attempt 1, conclusion cancelled, event workflow_dispatch, not a merge-group window). The run never reached any product job: the LIGHT scope job 117168283489 (Change scope, tn-local-light) hung in `Run actions/checkout@v7` fetching the full history (`git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules origin +refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/*`) from 2026-10-16T19:58:18Z to 2026-10-16T20:03:15Z, about 4m57s, then was cancelled at the 5-minute limit (`##[error]The operation was canceled.`, .linchpin/prd620-sample2-scope-failure.log:81-86). The `Classify the complete pull-request diff` and `Admit the eligible heavy instances to the idle pool` steps were SKIPPED, so the 46 canonical HEAVY instances were never created or admitted. GitHub then created 18 downstream jobs at 20:03:19Z, all skipped; ci-required job 117170451456 failed its own `Reject every unsuccessful or missing selected jobs` step (a cancellation consequence, not an independent defect) and run-summary 117172399484 was cancelled. Wall: full board max completed_at 2026-10-16T20:08:06Z minus run_started_at 2026-10-16T19:58:09Z = 597s (9.9 min); job envelope max completed_at minus min started_at 19:58:13Z = 593s, NOT the run updated_at 2026-10-16T20:08:06Z (coincident here because run-summary completed at that instant). Both are retained and make no AC-1 claim. No eligible queue value is recorded (no instance was created/started; NOT 0). All 21 observed job rows are embedded with status, labels, runner names, created/started/completed timestamps, queue/duration and a canonical class per row; the 46 canonical HEAVY instances are expected only and are NOT fabricated. Root fix: `.github/workflows/ci.yml` scope checkout now uses `fetch-depth: ${{ github.event_name == 'workflow_dispatch' && 1 || 0 }}` (manual full runs check out depth 1; automatic PR/push/merge_group/schedule keep depth 0), verified red-green by scripts/__tests__/ci-structure.spec.ts (164/164 at 2b204e). No engine, heavy-workload recipe, workflow or scheduler change. A fresh final-candidate hosted control plus two warm routed after-runs on the identical candidate/recipe/workload will follow for comparability; the qualifier 38953871463 (iteration 29) stays the qualifying sample 1 of 2 and the two-sample AC-1 claim stays open. Generic merge-queue metrics null; prs empty.
invalid2026-10-15n/an/an/an/an/a
#312026-10-16b611829famatched hosted control: full-board workflow_dispatch run 38961509705 at b611829fa (PRD-620; all-hosted successful control incl. ci-required; not a routed after-run)
Matched hosted control on the final candidate, chart-excluded. Full-board workflow_dispatch run 38961509705 at candidate b611829fa0702ee52c48f027c1db76b4040dddb3 (PR #513 ci/prd620-local-first, run attempt 1, conclusion success, event workflow_dispatch, not a merge-group window). All 72 rows embedded (46 HEAVY + 12 LIGHT + 14 HOSTED/skipped; 69 success + 3 skipped); all 46 canonical HEAVY instances ran hosted ubuntu-24.04 (0 tn-local), so this is a control, not a routed after-run. Wall: full board 3177s (52.95 min) from run_started_at 20:20:20Z to max completed_at 21:13:17Z; job envelope 3172s from min started_at 20:20:25Z, NOT the run updated_at 21:13:18Z. Eligible HEAVY queue over all 46 hosted instances p50 490s / p90 1394s / max 1466s / 31 >120s; a hosted-control population, not an AC-1 sample. ci-required job 117198043339 succeeded; warm V8 published hit on the unchanged recipe 1a2463b539e99c9fd7d87bd4bdd3ef86f2b61e42969a587f134e35f23d343607 (published step 1688ms). Generic merge-queue metrics null, prs empty. This ticks the Phase 2 full-green box on the final candidate; the two warm routed after-run boxes stay open.
invalid2026-10-15n/an/an/an/an/a
#322026-10-16b611829fainvalid routed after-run attempt 2: full-board workflow_dispatch run 38968104847 at b611829fa (PRD-620; 6 tn-local HEAVY incl. the failing test-unit 3/4 on the Debian laptop, force-cancelled; nonqualifying)
Invalid routed after-run attempt 2, chart-excluded. Full-board workflow_dispatch run 38968104847 at candidate b611829fa0702ee52c48f027c1db76b4040dddb3 (PR #513 ci/prd620-local-first, run attempt 1, conclusion cancelled, event workflow_dispatch, not a merge-group window). 71 rows embedded (46 HEAVY + 11 LIGHT + 14 HOSTED/skipped; the 72nd run-summary row was never created because of the force-cancel). The first real failure was test-unit (3/4) job 117200083728 on runner tn-ed7c3ecba926 (Debian laptop): `Run impacted unit contracts` failed when the build.spec.ts `audio worker pool remains referenced after build completion` test hit the deliberate 60s child ceiling and resolved {code:null, timedOut:true}; the output tail shows asset-encoding lines but does not prove ongoing activity for 60s nor a root cause, so the cause is UNKNOWN with runner slowness suspected. The required-receipt jobs (integration 117204640878, native coverage 117205695871) and ci-required 117205918769 failed or were cancelled as a consequence. A desktop probe on the unchanged source reproduced neither: build exit 0 in 31s and the focused test exit 0 in 21s. No runtime/test change and no timer raised. The run was force-cancelled by @jonit-dev; a fresh retry of the same head is run 38970276044 (underway). Wall: full board 1074s from run_started_at 21:15:32Z to max completed_at 21:33:26Z; job envelope 1070s. Generic merge-queue metrics null, prs empty. No blind rerun claim and no routing or wall acceptance; the two warm routed after-run boxes stay open.
invalid2026-10-15n/an/an/an/an/a
#332026-10-16b611829farouted after-run attempt 3 on the final candidate: full-board workflow_dispatch run 38970276044 at b611829fa (PRD-620; 6 tn-local HEAVY + 40 hosted overflow, full green incl. ci-required; wall 43m44 passes but eligible queue p50 662.5s FAILS the <=2 min target)
Routed after-run attempt 3, chart-excluded, fails the AC-1 queue target. Full-board workflow_dispatch run 38970276044 at candidate b611829fa0702ee52c48f027c1db76b4040dddb3 (PR #513 ci/prd620-local-first, run attempt 1, conclusion success, event workflow_dispatch, not a merge-group window). Routing: 6 of the 46 canonical HEAVY instances ran tn-local (test 117206787669, test-native 117208785273, test-unit 1/4 117208785198, 2/4 117208785147, 3/4 117208785195, 4/4 117208785255 - the 6 idle heavy slots the balancer published) and 40 ran hosted ubuntu-24.04 overflow; the 12 LIGHT joins ran tn-local-light. Full green incl. ci-required (job 117220943975, tn-local-light) with all 46 canonical HEAVY successful (72 rows: 46 HEAVY + 12 LIGHT + 14 HOSTED/skipped; 69 success + 3 skipped, the skipped legs the V8 publish/payload jobs). Wall: full board max completed_at 2026-10-16T22:18:05Z minus run_started_at 2026-10-16T21:34:21Z = 2624s (43m44); job envelope 2620s from min started_at 21:34:25Z; NOT the run updated_at (same instant here but the definition excludes it). AC-1 metrics: full board 2624s <= 60 min PASSES, but eligible HEAVY queue p50 662.5s (p90 929s, max 1029s, 38/46 >120s) FAILS the <=2 min target over all 46 canonical instances incl. hosted overflow, so this is a real queue-target failure, not a technical invalid/cancelled run, and it is NOT counted as sample 2 of 2. Evidence points to hosted contention: the 6 local heavy slots had flat 3s queues while hosted lint queued 265s, build-artifacts 274s, performance-contracts 407s, despite no long needs chain; the prior comparable after-run at 2b204e (iteration 29) had the same 6-local/40-hosted shape with eligible queue p50 79s. Run start 21:34 overlapped the repo develop-push run 38968480858 and asset-mcp runs 38968477198/38968476890 created 21:18; no global GitHub incident and no exclusive cause is claimed, so priority and budget are unchanged. Comparability vs the matched hosted control iteration 31 (same candidate b611829fa, same event/workload): Android V8 recipe unchanged and warm (1a2463b539e99c9fd7d87bd4bdd3ef86f2b61e42969a587f134e35f23d343607, published step 2202ms vs control 1688ms); test-native native-third-party cache HIT on the primary key and native-ccache primary MISS with a restore-key prefix HIT from run 38968104847 on both the sample and the control (control prefix from 38943971312). Generic merge-queue metrics null, prs empty.
invalid2026-10-15n/an/an/an/an/a
#342026-10-17b611829farouted after-run on the final candidate, quiet window: full-board workflow_dispatch run 38975472149 at b611829fa (PRD-620; 6 tn-local HEAVY + 40 hosted overflow, full green incl. ci-required; wall 36.82 min and eligible queue p50 4s PASS)
Routed after-run, chart-excluded. Full-board workflow_dispatch run 38975472149 at candidate b611829fa0702ee52c48f027c1db76b4040dddb3 (PR #513, run attempt 1, conclusion success), started in a quiet repository window: no other repository run overlapped 2026-10-16T22:23:36Z to 2026-10-16T23:00:25Z (iteration 33's run ended 22:18:05Z; develop push 38968480858 ended 22:12:23Z). Routing: 6 of 46 canonical HEAVY ran tn-local (test 117223874109, test-native 117224272682, test-unit (4/4) 117224272732, test-unit (3/4) 117224272744, test-unit (2/4) 117224272814, test-unit (1/4) 117224273055), 40 ran hosted ubuntu-24.04 overflow; the 12 LIGHT joins ran tn-local-light. ci-required job 117234269821 succeeded; all 46 HEAVY green (72 rows, 69 success + 3 skipped). Wall: full board 2209s from run_started_at to max completed_at 2026-10-16T23:00:25Z <= 60 min PASSES; job envelope 2205s. Eligible HEAVY queue over all 46 incl. hosted overflow p50 4s / p90 258s / max 324s / 14 >120s; p50 <= 120s PASSES. Comparability vs control iteration 31 (same candidate/event/workload): Android V8 recipe 1a2463b539e99c9fd7d87bd4bdd3ef86f2b61e42969a587f134e35f23d343607 unchanged and warm (published step 1628ms); test-native native-third-party primary HIT and native-ccache restore-key prefix HIT from run 38970276044, the control's shape. Iteration 33 on this same candidate failed the queue target in a contended window, so this is the first quiet-window sample of the predeclared pair, not a pair. Generic merge-queue metrics null, prs empty.
invalid2026-10-15n/an/an/an/an/a
#352026-10-176234d68381db960de04cfb9956232e647aa30455routed after-run, quiet window, RED: run 39066107986 at 6234d6838 (develop-synced); wall 46.67 min and queue p50 5s meet the targets but the board failed, so it is not a sample
Routed after-run on the develop-synced candidate 6234d68381db960de04cfb9956232e647aa30455, chart-excluded and NOT counted. Quiet window: no other repository run overlapped 2026-10-17T20:17:52Z to 2026-10-17T21:04:32Z. 6 of 46 HEAVY ran tn-local, 40 hosted overflow. Timing would pass (full board 2800s, job envelope 2796s; eligible HEAVY queue p50 5s / p90 344s / max 428s / 15 >120s) but the board is red, so it is not an AC-1 sample. Root failures: (1) test-unit (3/4) on tn-local runner tn-f5a5d3bd4a00 (desktop pool, not the laptop): scripts/__tests__/check-capability-docs.spec.ts "should scan subpath exports" exceeded its 15 s hang guard (file 23406ms; the same test took 4976ms on tn-local in run 38975472149). Fix: the guard is 60 s; the assertions are unchanged. (2) native-platforms / Android emulator visual parity on hosted ubuntu-24.04 (not a PRD-620 routed job): conformance 79 pass / 0 fail / 29 blocked, but supplemental androidMultitouch exited 1 after its first proof passed (300 frames, screenshot captured) with "FAIL: Device playtest cleanup failed."; the same job passed on develop runs 39056576003 and 39059885969, so this is an emulator cleanup failure outside this PRD, with no cause claimed. Native required coverage receipt and ci-required failed as consequences.
invalid2026-10-15n/an/an/an/an/a
#362026-10-174dd04c76bcurrent window refresh (develop since 2026-10-14)
Fresh develop window measured 2026-10-17 after iterations 21-35 (PR #513 full-board samples, chart-excluded). Not an effect of PR #513, which is unmerged; it reflects develop changes since 2026-10-14.
provisional2026-10-14147.844350%4618.2
#372026-10-174dd04c76ba2c1f4d230ccf500981dfb0180e24d1routed after-run sample 2, quiet window: run 39070176677 at 4dd04c76b (develop-synced); full green incl. ci-required; wall 38.58 min and eligible queue p50 9.5s PASS
Routed after-run sample 2 of the predeclared quiet-window pair, chart-excluded. Run 39070176677 at 4dd04c76ba2c1f4d230ccf500981dfb0180e24d1 (PR #513, run attempt 1, success). Quiet at dispatch (repo active and queued lists empty twice 60 s apart); PR run 39072036343 started at 21:41:55Z, after every HEAVY job had started, so no HEAVY queue overlapped it. 6 of 46 HEAVY ran tn-local (test 117506068541, test-native 117506556550, test-unit (2/4) 117506556569, test-unit (1/4) 117506556579, test-unit (3/4) 117506556621, test-unit (4/4) 117506556654), 40 hosted overflow; 12 LIGHT on tn-local-light. ci-required job 117513267216 succeeded; all 46 HEAVY green (72 rows). Full board 2315s <= 60 min PASSES (job envelope 2311s). Eligible HEAVY queue over all 46 incl. hosted overflow p50 9.5s / p90 297s / max 388s / 16 >120s; p50 <= 120s PASSES. Comparability vs sample 1 (iteration 34, run 38975472149 at b611829fa): same event and full workload (72 rows, 46 HEAVY, 6 local); Android V8 recipe 1a2463b539e99c9fd7d87bd4bdd3ef86f2b61e42969a587f134e35f23d343607 unchanged and warm (published 1898ms); test-native native-third-party primary HIT and native-ccache prefix HIT (from run 39066107986), the same cache shape. b611829fa..4dd04c76b changes no runs-on, runner_admission or TN_RUNNER line: it adds develop (Dawn build job, a Windows temp path, the rain template timeout) and the 60 s capability-scan hang guard. Generic merge-queue metrics null, prs empty.
invalid2026-10-15n/an/an/an/an/a
-

Merged PRs, iteration #36 window

-
PRTitlePush to mergeEnqueue to mergeAttemptsFirst attemptFirst failing job
#516A character never shows its bind pose (PRD-623)443 min53.4 min1cancelled
#515chore(deps): pin threenative-asset-mcp 0.14.0 (earshot prompt-fit backend)2.4 minn/a0n/a
#514Reduce temporary allocations in render bundle safety checks60.4 minn/a0n/a
#512Smooth physics and camera motion between fixed ticks (PRD-619)678.6 min45.7 min1success
#511chore(assets): pin threenative-asset-mcp 0.13.0 (Unity Asset Store import)0.4 minn/a0n/a
#509fix(core): PRD-616 - warm the output pass before startup and streamed draws147.8 min205 min3failurenative-platforms / Windows desktop core
#508Use cached V8 by default on Linux ARM64228.4 min162.2 min2cancelled
#507fix(native): complete v0.1.6 qualification and verifier cleanup94.7 min48.5 min1success
#505feat(PRD-473): enable automatic world impostors and finish GPU-world acceptance149.9 min81.8 min2failurenative-platforms / Desktop web/native parity
#488feat(assets): PRD-568 cook mobile textures to GPU-ready ASTC91.3 min45.9 min1success
#486Use transient MSAA depth for renderer output passes193.2 min39.2 min1success
-
diff --git a/docs/kit-loop/ledger.json b/docs/kit-loop/ledger.json new file mode 100644 index 0000000000..651e41086e --- /dev/null +++ b/docs/kit-loop/ledger.json @@ -0,0 +1,158 @@ +{ + "schemaVersion": 1, + "iterations": [ + { + "id": 1, + "at": "2026-10-27T03:37:30.259Z", + "decision": "baseline", + "kit": "collectathon", + "reference": "sm64", + "fixes": [], + "notes": "baseline on the post-merge collectathon kit (no revamp; PRD-749 kit base not landed). Cold prompt: 'Make me a game like Super Mario 64 with ThreeNative (the create-threenative command is installed). I will not answer questions; finish when it is playable.' claude -p opus-5-5, global CLAUDE.md excluded. K1 to the first result. Perf gate on a loaded host (load 12-30): cost p95 17.27 ms.", + "metrics": { + "k1Tokens": 97840, + "k1WallMin": 19, + "k1Turns": 37, + "k2ScenarioPct": 95, + "k2PresentedP95Ms": 17.7, + "k2CostP95Ms": 17.27, + "k2BudgetMs": 16.7, + "k2BudgetMet": false, + "k2Visual": "CLOSE", + "k3Friction": 7, + "k4Grade": 4 + }, + "friction": { + "searchMisses": 0, + "duplicates": 0, + "wrongApi": 1, + "retries": 6 + }, + "retro": { + "worked": [ + "`create-threenative --genre mario` picked the collectathon kit on the first call (L7)", + "21 of 22 kit scenarios green with no gameplay edit" + ], + "failed": [ + "performance gate red (cost p95 17.27 ms vs 16.7 ms) on a loaded host; the agent cut the high tier's look to chase it" + ], + "frictions": [ + { + "text": "perf gate red under host GPU contention: ~25 turns of quality cuts; the capture lock said 'no competing runner detected' while two foreign Playwright GPU processes ran", + "transcriptLine": 46 + }, + { + "text": "engine MCP absent: .mcp.json is written after the session starts, so no capability search ran; the agent grepped agent-docs instead", + "transcriptLine": 154 + }, + { + "text": "45 KB CLAUDE.md + AGENTS.md mirror read twice before any work", + "transcriptLine": 11 + } + ] + } + }, + { + "id": 2, + "at": "2026-10-27T04:11:59.495Z", + "decision": "iteration", + "kit": "collectathon", + "reference": "sm64", + "fixes": [ + "a4d5345ef: a cost bound failed beside foreign GPU processes names them and says rerun", + "42cbf971c + c5bc052e7: the scaffold's last line says start the agent in the project, and engine.mjs answers one engine tool call from the command line" + ], + "notes": "Cold build on tarballs built after row 1's fixes; same prompt and isolation as row 1 (claude -p, --setting-sources project, claudeMdExcludes). The agent changed no code: src/ is byte-identical to the kit. Gates rerun by the recorder: 22/22 green, performance gate cost p95 10.03 ms (GPU 10.03, CPU 6.5, 26 GPU samples) vs 16.7 ms on the stock high tier, load 14 with 3 foreign GPU processes. K2 presented p95 17.5 ms is the runner's private Xvfb cadence (the gate reports it as not gated), so it reads over budget whatever the game costs; the gate's cost p95 met the budget. Visual: fresh Opus judge CLOSE vs the reference, NEUTRAL vs row 1; wall-kick hides the hero behind the wall (both rows). K4 carried from row 1: Hero.ts unchanged.", + "metrics": { + "k1Tokens": 44483, + "k1WallMin": 7, + "k1Turns": 15, + "k2ScenarioPct": 100, + "k2PresentedP95Ms": 17.5, + "k2CostP95Ms": 10.03, + "k2BudgetMs": 16.7, + "k2BudgetMet": true, + "k2Visual": "CLOSE", + "k3Friction": 2, + "k4Grade": 4 + }, + "friction": { + "searchMisses": 0, + "duplicates": 0, + "wrongApi": 1, + "retries": 1 + }, + "retro": { + "worked": [ + "`create-threenative --genre mario` picked the collectathon kit on the first call (L7)", + "no code edit: the agent ran check, build and all 22 playtests and stopped (15 turns, 6.6 min)", + "row 1's fixes landed: no performance chase; the scaffold line named the engine call (the agent needed no search)" + ], + "failed": [], + "frictions": [ + { + "text": "--scenario with a quoted shell brace list failed TN_PLAYTEST_SCENARIO_UNREADABLE; the agent reran the whole suite (fixed in bac933cdc)", + "transcriptLine": 42 + }, + { + "text": "45 KB CLAUDE.md + AGENTS.md pair read, then AGENTS.md again (row 1's friction 3 recurred; fixed in 59d741907)", + "transcriptLine": 11 + }, + { + "text": "the kit AGENTS.md asks for a design plan and approval before changes; under a no-questions prompt the agent had to decide to skip it", + "transcriptLine": 86 + } + ] + } + }, + { + "id": 3, + "at": "2026-10-27T04:28:26.532Z", + "decision": "iteration", + "kit": "collectathon", + "reference": "sm64", + "fixes": [ + "bac933cdc: --scenario takes a shell brace list", + "59d741907: the scaffold's last line says CLAUDE.md copies AGENTS.md, read one" + ], + "notes": "Cold build on tarballs built after row 2's fixes; same prompt and isolation. The agent read AGENTS.md only (no CLAUDE.md), ran all 22 playtests in one glob run, and added a win toast on the third Sunseed (src/scenes/Play.ts:389,393; the kit has no win state). Gates rerun by the recorder: 22/22 green, performance gate cost p95 12.59 ms (GPU 12.59, CPU 6.6, 22 GPU samples) vs 16.7 ms, load 7 with 4 foreign GPU processes; presented p95 17.6 ms is Xvfb cadence (see the PRD K2 note). Visual: fresh Opus judge CLOSE vs the reference, NEUTRAL vs row 2; wall-kick hides the hero (all rows). K1 +6% vs row 2: AGENTS.md (22 KB) with package.json and a file list overflowed one 30 KB tool result and was read again. K4 carried: Hero.ts unchanged.", + "metrics": { + "k1Tokens": 47107, + "k1WallMin": 8, + "k1Turns": 17, + "k2ScenarioPct": 100, + "k2PresentedP95Ms": 17.6, + "k2CostP95Ms": 12.59, + "k2BudgetMs": 16.7, + "k2BudgetMet": true, + "k2Visual": "CLOSE", + "k3Friction": 0, + "k4Grade": 4 + }, + "friction": { + "searchMisses": 0, + "duplicates": 0, + "wrongApi": 0, + "retries": 0 + }, + "retro": { + "worked": [ + "picked the collectathon kit on the first call (L7, --template collectathon)", + "read AGENTS.md only, not the CLAUDE.md copy (row 2 fix 59d741907)", + "one --scenario glob run for all 22 playtests, no retry (row 2 fix bac933cdc not needed: no brace list)" + ], + "failed": [], + "frictions": [ + { + "text": "the kit's 22 KB AGENTS.md plus package.json and a file list overflowed one 30 KB tool result, so AGENTS.md was read a second time (PRD-749 kit lane: trim the kit AGENTS.md)", + "transcriptLine": 11 + }, + { + "text": "kit gap: the third Sunseed shows only 'SUNSEED!' with no win state; the agent added a win toast (Play.ts:389,393)", + "transcriptLine": 74 + } + ] + } + } + ] +} diff --git a/package.json b/package.json index 87bb2498d4..e4379c6113 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "bench:engines": "tsx scripts/engine-load-test/cli.ts", "bench:engines:report": "tsx scripts/engine-load-test/cli.ts --compare", "bench:engines:monitor": "tsx scripts/engine-load-test/monitor.ts", - "perf:live": "tsx scripts/perf-live.ts", + "dashboard": "tsx scripts/perf-live.ts", + "kit:loop": "node scripts/kit-loop.mjs", "bench:scoreboard": "sh scripts/engine-load-test/scoreboard.sh", "budgets": "tsx scripts/workspace-packages.ts && tsx scripts/check-version-pins.ts && tsx scripts/check-native-shims.ts && tsx scripts/check-core-boundary.ts && tsx scripts/check-template-conventions.ts && tsx scripts/check-capability-docs.ts --census && pnpm caps:recall && tsx scripts/check-evidence-budget.ts && tsx scripts/vfx-niagara-coverage.ts && tsx scripts/check-budgets.ts && tsx scripts/generate-ctx-surface-table.ts --check && tsx scripts/generate-capability-reference.ts --check && tsx scripts/realism-effects-docs.ts --check && tsx scripts/realism-effects-conformance.ts && tsx scripts/realism-effects-matrix.ts && tsx scripts/detect-capability-duplicates.ts examples --strict && tsx scripts/check-template-quality.ts && tsx scripts/sync-mcp-configs.ts --check", "build": "tsx scripts/build-capability-manifest.ts && tsx scripts/generate-ctx-surface-table.ts && tsx scripts/generate-capability-reference.ts && tsx scripts/realism-effects-docs.ts && pnpm -r --if-present run build", diff --git a/packages/core/__tests__/mcp-install.spec.ts b/packages/core/__tests__/mcp-install.spec.ts index 53c652ce57..426153143d 100644 --- a/packages/core/__tests__/mcp-install.spec.ts +++ b/packages/core/__tests__/mcp-install.spec.ts @@ -1,4 +1,4 @@ -import { execFileSync, spawn } from "node:child_process"; +import { execFileSync, spawn, spawnSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -528,4 +528,29 @@ describe("the sculpt server the engine ships", () => { child.kill(); } }, 30_000); + + // Authoring-layer regression guard (PRD-750 row 1): a session started outside the game never + // loads .mcp.json, and the scaffold tells it to call the shim by absolute path instead. + it("answers one engine call through the shim from outside the project, and fails closed", () => { + const outside = makeTempDirSync("engine-cli-cwd"); + const shim = path.join(packageRoot, "mcp", "engine.mjs"); + const env = { ...process.env, THREENATIVE_ENGINE_MCP_LOG: "off" }; + const call = (...args: string[]) => + spawnSync(process.execPath, [shim, ...args], { + cwd: outside, + encoding: "utf8", + env, + input: "", + timeout: 20_000, + }); + const found = call( + "engine_search_capabilities", + '{"situation":"make me a game like super mario 64"}', + ); + expect(found.status, found.stderr).toBe(0); + expect(JSON.parse(found.stdout).starterKits?.[0]?.name).toBe("collectathon"); + const unknown = call("engine_no_such_tool", "{}"); + expect(unknown.status).toBe(1); + expect(unknown.stderr).toMatch(/engine_no_such_tool/u); + }, 30_000); }); diff --git a/packages/core/mcp/engine-server.mjs b/packages/core/mcp/engine-server.mjs index c87062c68a..25d4067241 100755 --- a/packages/core/mcp/engine-server.mjs +++ b/packages/core/mcp/engine-server.mjs @@ -7,7 +7,7 @@ import { createHash } from 'crypto'; import os from 'os'; import { gunzipSync } from 'zlib'; -var DEFAULT_REFERENCES_URL = "https://github.com/jonit-dev/threenative-game-refs/releases/latest/download/"; +var DEFAULT_REFERENCES_URL = "https://github.com/ThreeNativeHQ/threenative-reverse-engineer-mcp/releases/latest/download/"; var FETCH_TIMEOUT_MS = 5e3; var GAME_FETCH_TIMEOUT_MS = 3e4; var ID = /^[a-z0-9][a-z0-9-]*$/u; @@ -95,7 +95,17 @@ function validateReferenceIndex(value, where = "index") { const index = record(value, where, ["version", "games", "stubs"]); if (index.version !== 1) fail(`${where}.version`, "expected 1"); const games = list(index.games, `${where}.games`, (entry, at) => { - const game = record(entry, at, ["id", "title", "platform", "repo", "tier"]); + const game = record(entry, at, [ + "id", + "title", + "platform", + "repo", + "tier", + "kind", + "rating", + "genre", + "subgenre" + ]); if (game.tier !== 1 && game.tier !== 2) fail(`${at}.tier`, "expected 1 or 2"); return { id: id(game.id, `${at}.id`), @@ -372,7 +382,7 @@ function atomicWrite(file, value) { function shellQuote(value) { return `'${value.replaceAll("'", `'"'"'`)}'`; } -var CONTRIBUTION_REPO = "jonit-dev/threenative-game-refs"; +var CONTRIBUTION_REPO = "ThreeNativeHQ/threenative-reverse-engineer-mcp"; function saveReferenceGame(value, cwd = process.cwd()) { const incoming = record(value, "reference", [ "version", @@ -792,7 +802,7 @@ function searchCapabilities(situation, manifestFile = defaultManifestPath(), sco throw new Error("engine_search_capabilities requires a non-empty situation string."); const manifest = loadCapabilityManifest(manifestFile); const starterKits = matchedStarterKits(situation, manifest.starterKits ?? []); - const response = searchMechanics(situation, manifest, scope); + const response = withReferenceTools(situation, searchMechanics(situation, manifest, scope)); if (starterKits.length === 0) return response; return { starterKits, @@ -803,6 +813,20 @@ function searchCapabilities(situation, manifestFile = defaultManifestPath(), sco } : {} }; } +var REVERSE_ENGINEER = /\breverse[\s-]?engineer|\bdecomp|\bdisassembl/iu; +var REFERENCE_TOOLS = [ + "engine_search_references", + "engine_reference_detail", + "engine_save_reference" +]; +function withReferenceTools(situation, response) { + if (!REVERSE_ENGINEER.test(situation)) return response; + return { + referenceTools: REFERENCE_TOOLS, + ...response, + guidance: `Reverse engineer the game with engine_search_references, then engine_reference_detail for its pinned cards. ${response.guidance}` + }; +} function searchMechanics(situation, manifest, scope) { const query = tokens(situation); const limit = scope === "request" ? MAX_COMPLETE_REQUEST_RESULTS : MAX_SITUATION_RESULTS; @@ -900,7 +924,7 @@ var TOOL_DEFINITIONS = [ }, { name: "engine_search_references", - description: "Search mechanic cards and repository map nodes from classic games. Tier-2 titles link to repos to explore and save. References describe feel only; borrow algorithms, cite pinned sources, copy no code or art.", + description: "Reverse engineer classic games: search mechanic cards and repository map nodes from their decomps. Tier-2 titles link to repos to explore and save. References describe feel only; borrow algorithms, cite pinned sources, copy no code or art.", annotations: { destructiveHint: false, openWorldHint: true, readOnlyHint: true }, inputSchema: { type: "object", @@ -911,7 +935,7 @@ var TOOL_DEFINITIONS = [ }, { name: "engine_reference_detail", - description: "Read a game's pinned map or mechanic card. A node includes files, key functions, dependencies and child nodes; a card includes constants and sources.", + description: "Reverse engineer a game: read its pinned map or mechanic card. A node includes files, key functions, dependencies and child nodes; a card includes constants and sources.", annotations: { destructiveHint: false, openWorldHint: true, readOnlyHint: true }, inputSchema: { type: "object", @@ -926,7 +950,7 @@ var TOOL_DEFINITIONS = [ }, { name: "engine_save_reference", - description: "Validate and save a pinned game's repository map and mechanic cards to .threenative/refs, merged by id at the same SHA; conflicting revisions are refused. Pass a game object with version:1, id, title, repo, sha, nodes and cards. Returns a POSIX-shell contribution command for review; does not send it. Unknown fields and fenced code are refused.", + description: "Save what you reverse engineer: validate and save a pinned game's repository map and mechanic cards to .threenative/refs, merged by id at the same SHA; conflicting revisions are refused. Pass a game object with version:1, id, title, repo, sha, nodes and cards. Returns a POSIX-shell contribution command for review; does not send it. Unknown fields and fenced code are refused.", annotations: { destructiveHint: false, openWorldHint: false, readOnlyHint: false }, inputSchema: { type: "object", @@ -1164,7 +1188,33 @@ async function handleLine(line2, manifestFile) { return jsonRpcError(request.id, -32e3, message); } } +async function runCli(argv, manifestFile = defaultManifestPath(), io = { + stderr: (text2) => void process.stderr.write(text2), + stdout: (text2) => void process.stdout.write(text2) +}) { + const [name, json = "{}"] = argv; + try { + const result = await handleToolCall( + { arguments: JSON.parse(json), name }, + manifestFile + ); + io.stdout(`${result.content[0].text} +`); + return 0; + } catch (error) { + io.stderr(`${name}: ${error instanceof Error ? error.message : String(error)} +`); + return 1; + } +} function runServer(manifestFile = defaultManifestPath()) { + const tool = process.argv[2]; + if (tool?.startsWith("engine_") === true) { + void runCli(process.argv.slice(2), manifestFile).then((code) => { + process.exitCode = code; + }); + return; + } loadCapabilityManifest(manifestFile); const input = createInterface({ input: process.stdin }); void loadReferenceIndex().catch((error) => process.stderr.write(`${String(error)} @@ -1186,4 +1236,4 @@ if (entryPath !== void 0 && realpathSync(path2.resolve(entryPath)) === realpathS } } -export { ENGINE_MCP_TOOL_NAMES, RELEVANCE_FLOOR, capabilityDetail, capabilityDetails, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions }; +export { ENGINE_MCP_TOOL_NAMES, RELEVANCE_FLOOR, capabilityDetail, capabilityDetails, capabilitySituationTokens, defaultManifestPath, handleLine, loadCapabilityManifest, runCli, runServer, searchCapabilities, toolDefinitions }; diff --git a/packages/create-threenative/__tests__/scaffold.spec.ts b/packages/create-threenative/__tests__/scaffold.spec.ts index 74ee10ed46..3da7b871f8 100644 --- a/packages/create-threenative/__tests__/scaffold.spec.ts +++ b/packages/create-threenative/__tests__/scaffold.spec.ts @@ -294,7 +294,7 @@ const PRD_201_PARENT_SCAFFOLD_HASHES: Readonly> = { // `threenative.config.ts`, the field core reads, instead of the untyped `renderer` block. // Recomputed 2026-10-26 (PR #574 merges #572): the action-rpg draw fix, the rain terrain cache, // the sailing and snow core-look migrations on top of the on-demand asset loader. - "action-rpg": "4c7c4056e666c4bb57f4be9796c64cd844622db00896d2cf288e134355ba5f5d", + "action-rpg": "2595a51284d1ec8eaeee853035cda111544d87b9392e5001f04270cebc3e18f9", // PRD-732 (2026-10-23): the new Hop Garden kit, measured through createProject (method checked // against platformer's unchanged value). Recomputed 2026-10-24 for the PRD-732 polish round: the // new hero, the dust and sparkle effects and the effects scenario. Recomputed 2026-10-24 for @@ -308,7 +308,7 @@ const PRD_201_PARENT_SCAFFOLD_HASHES: Readonly> = { // Recomputed 2026-10-24 for the shadow window: the 52 m map now travels 14 m ahead of the hero, // so the castle and trees the camera sees keep their shadows; the 2048 map and shadow pass cost // stay the same. - collectathon: "06609ef0dcae09aca28853044b1e460a1f26c35cf8610c28d4c8621dad9189c9", + collectathon: "07a00c5ccc4b33ae30cbbca156f585ec32f2bc3dae030bff6e5123916aa687b7", // PRD-733 (2026-10-23): the new Lantern Vale kit, measured through createProject. // Recomputed 2026-10-24 for the owner perf round: the fixed-step catch-up cap and the gentler // wind on the crowns, clusters and grass tufts. Only this tree moves. @@ -317,28 +317,30 @@ const PRD_201_PARENT_SCAFFOLD_HASHES: Readonly> = { // Re-pinned 2026-10-26 (PRD-748 loading cover): adventure loses its `loaded` gate, rain // reads state through `useUiStateDuringStartup`. // Re-pinned 2026-10-27 (PRD-749 hud-clean): adventure, collectathon and rain HUD text reaches 3:1. - adventure: "dcc5df44789579c4442d73a3b317803b4a9e8a745dea4e2fb0dc7c7153e9ecc6", + // Recomputed 2026-10-27 (#575 merges develop #579): every template AGENTS.md names classic-game + // mechanics references beside capability search, so all sixteen trees move. + adventure: "66f885599f0a92837d0879747dabff74f13c665aa9f3b056f92cc8fbb89f9c30", // PRD-734 (2026-10-24): the new Clover Cup kart kit, measured through createProject. - kart: "10af477d1a520fdc1ce9303d488a9218177360231d7082a2093e3d397310e23c", - minimal: "772a616dca234349aa4f555678bb9b0493f679c22c1a65388a3954ab6e26a042", - platformer: "7df262eb519137cd5cfbcd3cb210549a52c637fb94e2ebc3aaf0975bc66f50e3", - puzzle: "2c8d1a41dae19b90cddb7e1037178ca2f91c7fe9fdd5fe72b24a9334bf72ba3a", - racing: "2b1c4913ad3c60fd9e6a08d74c1edcc7646a6711c09b6742b60b78dbae9ee839", - rain: "453965d590003d04e928fb6274454fb58fe0c1f9124c8438aecf161686d5ce75", - rts: "152fdfb0fbf75055b98b184f040f819b0661c74056cfa78dea6e32142733f434", - runner: "dcabe5962307f070fd387390e68df03a599400c90302068edc38d49c7bec60c4", + kart: "4d0884a98ccfe97c0aeaaf6a64ed62b195154666123444539acf56ba5563963e", + minimal: "e157e66fdf5e14761655fb39ef95af1fd005df68a6e8ecc6cb1159c0d501d227", + platformer: "6694c571a16d48ee33d9a9eac081fe1982be389595824163192f9e489e7d4c52", + puzzle: "40aeabf6ed77b2dc644acf0a71360ef2bc976d018a99f43523894179f7b9eaef", + racing: "e1aacca0748b6a219d7573eedd228c9d1e0d5a67c1372dc8851117f635e03f2f", + rain: "6e86e9dc5e1a413f84cd46977b5bef9c181fedd6e9c38ec15e288fed7a5061a3", + rts: "44917927d738f4c2e56a1fb6972d5d3514592839f4df9fb80abc9c3b0624364f", + runner: "3c5e0dd218b0cd82208e0eb9da236da358715b879076a1c169e688561f87e5f5", // Finite-height readiness plus hull, sail, and buoy interpolation from the current swell pose. - sailing: "1429611e2da4350df3281cf6883b4273cf833ea9c8b03c38356ba25f93cfdb71", + sailing: "fb9d08250cabb500cbfa70c8ce78e739cc7dbe720c782cf8ffc0bb84284266f5", // Recomputed 2026-10-26 (PRD-746): the scene merges the town through mergeByMaterial (src/townBatch.ts). - shooter: "b281400f4a039ab993e13332a2ed9d0c939fcd732fac9040fce8d4dfe8b28f8d", - snow: "44540fc309fab668b63491b002c73f8e64b9d1c6d4b4d9d5f533f9eb812e0d34", + shooter: "c02a1b7333fe4f010bdce808cd0796d10fd5b762ef36091c97508440a6ce2393", + snow: "61d81b402214c8378d6b4e5acb926b2b25aff4629414c1760c6cbfc625d5ac3e", // Measured 2026-10-16 through createProject after starter's completed-work quality fallback fix. // Recomputed 2026-10-16: starter's adaptiveQuality split its policy resolution into qualityPolicy.ts // to stay under the 200-line render-file cap; only starter's tree moves. // Recomputed 2026-10-23 on the merge of origin/develop into the PRD-570 lane: measured by this test on the merged tree. // Recomputed 2026-10-23: the native render-chain scenario expects the Unreal finishing chain (grade and grain off). - starter: "66cf05bad1f49c11a260357898655d0f8d548e56b30fd26072b7487401b8dbc4", - "tower-defense": "b675065d408bf7aea598ab2b962c24b7b96e0789ec3662940ff2f54bd675d25f", + starter: "09313c615c2ea0840a91118691cf6b5c4f462795c3be7fc92c006c227995cd64", + "tower-defense": "798dae4422ea48da95629e6e3de73a95facce66d022ccb776424cf952ff7f4d7", }; const GENERATED_SCAFFOLD_METADATA = diff --git a/packages/create-threenative/engine-mcp-tools.json b/packages/create-threenative/engine-mcp-tools.json index 4f22e93b16..40461fd391 100644 --- a/packages/create-threenative/engine-mcp-tools.json +++ b/packages/create-threenative/engine-mcp-tools.json @@ -80,8 +80,8 @@ "descriptions": { "engine_search_capabilities": "Search the installed engine by concrete gameplay mechanic. Decompose genres first. Use scope \"request\" for the mechanically explicit full request and \"mechanic\" for each focused search; matchedSituation and score explain every result. The response verdict \"none\" is an actionable answer with guidance, not a failed search.", "engine_capability_detail": "Inspect one engine capability's import, signature, example, constraints, and overrides. When several packages export the symbol, every match is returned under `matches`.", - "engine_search_references": "Search mechanic cards and repository map nodes from classic games. Tier-2 titles link to repos to explore and save. References describe feel only; borrow algorithms, cite pinned sources, copy no code or art.", - "engine_reference_detail": "Read a game's pinned map or mechanic card. A node includes files, key functions, dependencies and child nodes; a card includes constants and sources.", - "engine_save_reference": "Validate and save a pinned game's repository map and mechanic cards to .threenative/refs, merged by id at the same SHA; conflicting revisions are refused. Pass a game object with version:1, id, title, repo, sha, nodes and cards. Returns a POSIX-shell contribution command for review; does not send it. Unknown fields and fenced code are refused." + "engine_search_references": "Reverse engineer classic games: search mechanic cards and repository map nodes from their decomps. Tier-2 titles link to repos to explore and save. References describe feel only; borrow algorithms, cite pinned sources, copy no code or art.", + "engine_reference_detail": "Reverse engineer a game: read its pinned map or mechanic card. A node includes files, key functions, dependencies and child nodes; a card includes constants and sources.", + "engine_save_reference": "Save what you reverse engineer: validate and save a pinned game's repository map and mechanic cards to .threenative/refs, merged by id at the same SHA; conflicting revisions are refused. Pass a game object with version:1, id, title, repo, sha, nodes and cards. Returns a POSIX-shell contribution command for review; does not send it. Unknown fields and fenced code are refused." } } diff --git a/packages/create-threenative/src/index.ts b/packages/create-threenative/src/index.ts index a291953d92..fa281067fa 100644 --- a/packages/create-threenative/src/index.ts +++ b/packages/create-threenative/src/index.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { spawn } from "node:child_process"; +import { execFileSync, spawn } from "node:child_process"; import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs"; import { cp, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises"; import path from "node:path"; @@ -55,6 +55,8 @@ export interface IScaffoldOptions { packageSources?: PackageSources; target: string; template?: ScaffoldTemplate; + /** The `--genre` word the user asked with; the next-steps line searches references for it. */ + genre?: string; } export interface IScaffoldResult { @@ -349,6 +351,59 @@ export function scaffoldCompletionMessage( return `Templates: ${names}. Choose with --template .\n`; } +/** + * The scaffold's last line. The engine MCP reaches an agent only through the project's `.mcp.json`, + * which a session started in another folder never loads (PRD-750 kit-loop row 1). + */ +export function scaffoldNextSteps(target: string, installed: boolean): string { + const agent = `start your coding agent in ${target} so it loads the engine MCP from .mcp.json; a session started elsewhere calls a tool with node ${target}/node_modules/@threenative/core/mcp/engine.mjs engine_search_capabilities '{"situation":""}'`; + return installed + ? `Next: ${agent}. Read AGENTS.md once; CLAUDE.md is its copy. pnpm dev plays the game; pnpm test proves it.\n` + : `Skipped install (--no-install). Run pnpm install in ${target}, then ${agent}.\n`; +} + +const REFERENCE_DIGEST_SIZE = 5; + +/** + * The mechanics references for the game the user named with `--genre`, printed by the scaffold. + * Cold agents skipped the search even when the next-steps line gave the command (PRD-750 + * sandboxes 4 and 5), so the scaffold runs it through the installed engine shim and shows the result. + */ +export function referenceDigest( + target: string, + genre: string, + run: (args: string[]) => string = (args) => + execFileSync(process.execPath, args, { encoding: "utf8", timeout: 60_000 }), +): string { + const engine = `${target}/node_modules/@threenative/core/mcp/engine.mjs`; + let results: { + game: string; + id: string; + kind: string; + name: string; + summary: string; + title: string; + }[]; + try { + results = JSON.parse( + run([engine, "engine_search_references", JSON.stringify({ situation: genre })]), + ).results; + } catch (error) { + return `References unavailable: ${String((error as Error).message).split("\n")[0]}\n`; + } + if (results.length === 0) return `No mechanics references match ${JSON.stringify(genre)}.\n`; + const [first] = results; + const detail = JSON.stringify({ game: first?.game, id: first?.id, kind: first?.kind }); + return [ + `Mechanics references for ${JSON.stringify(genre)} (borrow the mechanics, not code, layouts or art):`, + ...results + .slice(0, REFERENCE_DIGEST_SIZE) + .map(({ title, name, summary }) => `- ${title} / ${name}: ${summary}`), + `Open one with node ${engine} engine_reference_detail '${detail}'.`, + "", + ].join("\n"); +} + export function cliHelp(): string { const manifests = discoverKitManifests(); return `${[ @@ -896,6 +951,7 @@ export function parseArgs(argv: readonly string[]): IScaffoldOptions { target, ...(Object.keys(packageSources).length === 0 ? {} : { packageSources }), ...(resolvedTemplate === undefined ? {} : { template: resolvedTemplate }), + ...(genre === undefined ? {} : { genre }), }; } @@ -913,12 +969,13 @@ async function main(): Promise { process.stdout.write(cliHelp()); return; } - const result = await createProject(parseArgs(argv)); + const options = parseArgs(argv); + const result = await createProject(options); process.stdout.write(`Created ${result.template} project at ${result.target}\n`); process.stdout.write(scaffoldCompletionMessage(discoverKitManifests())); - if (!result.installed) - process.stdout.write("Skipped install (--no-install). Run pnpm install, then pnpm dev.\n"); - else process.stdout.write("Next: cd into the project and run pnpm dev.\n"); + if (options.genre !== undefined && result.installed) + process.stdout.write(referenceDigest(result.target, options.genre)); + process.stdout.write(scaffoldNextSteps(result.target, result.installed)); } // `realpathSync`, not `path.resolve`. A package manager installs a `bin` as a symlink, so the diff --git a/packages/create-threenative/templates/action-rpg/AGENTS.md b/packages/create-threenative/templates/action-rpg/AGENTS.md index cbf21bfee7..519420611e 100644 --- a/packages/create-threenative/templates/action-rpg/AGENTS.md +++ b/packages/create-threenative/templates/action-rpg/AGENTS.md @@ -19,7 +19,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/action-rpg/CLAUDE.md b/packages/create-threenative/templates/action-rpg/CLAUDE.md index 1714607d38..a1344d5407 100644 --- a/packages/create-threenative/templates/action-rpg/CLAUDE.md +++ b/packages/create-threenative/templates/action-rpg/CLAUDE.md @@ -21,7 +21,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/adventure/AGENTS.md b/packages/create-threenative/templates/adventure/AGENTS.md index 79a0e85b25..d966bbacf1 100644 --- a/packages/create-threenative/templates/adventure/AGENTS.md +++ b/packages/create-threenative/templates/adventure/AGENTS.md @@ -19,7 +19,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/adventure/CLAUDE.md b/packages/create-threenative/templates/adventure/CLAUDE.md index c772cc43a9..3db6d09f67 100644 --- a/packages/create-threenative/templates/adventure/CLAUDE.md +++ b/packages/create-threenative/templates/adventure/CLAUDE.md @@ -21,7 +21,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/collectathon/AGENTS.md b/packages/create-threenative/templates/collectathon/AGENTS.md index d6c3272bab..c1994bd5d7 100644 --- a/packages/create-threenative/templates/collectathon/AGENTS.md +++ b/packages/create-threenative/templates/collectathon/AGENTS.md @@ -19,7 +19,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/collectathon/CLAUDE.md b/packages/create-threenative/templates/collectathon/CLAUDE.md index 0e4d0fb30b..ecffbec5d3 100644 --- a/packages/create-threenative/templates/collectathon/CLAUDE.md +++ b/packages/create-threenative/templates/collectathon/CLAUDE.md @@ -21,7 +21,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/kart/AGENTS.md b/packages/create-threenative/templates/kart/AGENTS.md index 9c39959542..d555fa7748 100644 --- a/packages/create-threenative/templates/kart/AGENTS.md +++ b/packages/create-threenative/templates/kart/AGENTS.md @@ -19,7 +19,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/kart/CLAUDE.md b/packages/create-threenative/templates/kart/CLAUDE.md index 779c141436..f7d440eb4d 100644 --- a/packages/create-threenative/templates/kart/CLAUDE.md +++ b/packages/create-threenative/templates/kart/CLAUDE.md @@ -21,7 +21,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/minimal/AGENTS.md b/packages/create-threenative/templates/minimal/AGENTS.md index 962255496c..8b8f7c644e 100644 --- a/packages/create-threenative/templates/minimal/AGENTS.md +++ b/packages/create-threenative/templates/minimal/AGENTS.md @@ -26,7 +26,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/minimal/CLAUDE.md b/packages/create-threenative/templates/minimal/CLAUDE.md index b5d64cd344..84d4ce8c74 100644 --- a/packages/create-threenative/templates/minimal/CLAUDE.md +++ b/packages/create-threenative/templates/minimal/CLAUDE.md @@ -28,7 +28,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/platformer/AGENTS.md b/packages/create-threenative/templates/platformer/AGENTS.md index a70c722ca3..ad7be618ca 100644 --- a/packages/create-threenative/templates/platformer/AGENTS.md +++ b/packages/create-threenative/templates/platformer/AGENTS.md @@ -19,7 +19,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/platformer/CLAUDE.md b/packages/create-threenative/templates/platformer/CLAUDE.md index d5bbcacd04..800b7994f4 100644 --- a/packages/create-threenative/templates/platformer/CLAUDE.md +++ b/packages/create-threenative/templates/platformer/CLAUDE.md @@ -21,7 +21,7 @@ When an `@threenative/*` API is broken, missing, or does not do what you need, r ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/puzzle/AGENTS.md b/packages/create-threenative/templates/puzzle/AGENTS.md index 40e47294c1..fb12eaf339 100644 --- a/packages/create-threenative/templates/puzzle/AGENTS.md +++ b/packages/create-threenative/templates/puzzle/AGENTS.md @@ -27,7 +27,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/puzzle/CLAUDE.md b/packages/create-threenative/templates/puzzle/CLAUDE.md index c6a6ec6d87..d8f4a536a0 100644 --- a/packages/create-threenative/templates/puzzle/CLAUDE.md +++ b/packages/create-threenative/templates/puzzle/CLAUDE.md @@ -29,7 +29,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/racing/AGENTS.md b/packages/create-threenative/templates/racing/AGENTS.md index b154865fa1..54dff812ae 100644 --- a/packages/create-threenative/templates/racing/AGENTS.md +++ b/packages/create-threenative/templates/racing/AGENTS.md @@ -24,7 +24,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/racing/CLAUDE.md b/packages/create-threenative/templates/racing/CLAUDE.md index 837c13c55d..f60b01772e 100644 --- a/packages/create-threenative/templates/racing/CLAUDE.md +++ b/packages/create-threenative/templates/racing/CLAUDE.md @@ -26,7 +26,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/rain/AGENTS.md b/packages/create-threenative/templates/rain/AGENTS.md index a34e41956f..b28bdb039e 100644 --- a/packages/create-threenative/templates/rain/AGENTS.md +++ b/packages/create-threenative/templates/rain/AGENTS.md @@ -23,7 +23,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/rain/CLAUDE.md b/packages/create-threenative/templates/rain/CLAUDE.md index adb697f4ce..81de68e46d 100644 --- a/packages/create-threenative/templates/rain/CLAUDE.md +++ b/packages/create-threenative/templates/rain/CLAUDE.md @@ -25,7 +25,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/rts/AGENTS.md b/packages/create-threenative/templates/rts/AGENTS.md index 1f304d2779..08ecc8499d 100644 --- a/packages/create-threenative/templates/rts/AGENTS.md +++ b/packages/create-threenative/templates/rts/AGENTS.md @@ -26,7 +26,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/rts/CLAUDE.md b/packages/create-threenative/templates/rts/CLAUDE.md index 0df207c9b3..c610a5c7f7 100644 --- a/packages/create-threenative/templates/rts/CLAUDE.md +++ b/packages/create-threenative/templates/rts/CLAUDE.md @@ -28,7 +28,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/runner/AGENTS.md b/packages/create-threenative/templates/runner/AGENTS.md index da27091a52..e5c4bb252f 100644 --- a/packages/create-threenative/templates/runner/AGENTS.md +++ b/packages/create-threenative/templates/runner/AGENTS.md @@ -29,7 +29,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/runner/CLAUDE.md b/packages/create-threenative/templates/runner/CLAUDE.md index 8fac07f9e0..1eee6f248f 100644 --- a/packages/create-threenative/templates/runner/CLAUDE.md +++ b/packages/create-threenative/templates/runner/CLAUDE.md @@ -31,7 +31,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/sailing/AGENTS.md b/packages/create-threenative/templates/sailing/AGENTS.md index f1d68d39fe..c809aab2b9 100644 --- a/packages/create-threenative/templates/sailing/AGENTS.md +++ b/packages/create-threenative/templates/sailing/AGENTS.md @@ -26,7 +26,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/sailing/CLAUDE.md b/packages/create-threenative/templates/sailing/CLAUDE.md index 2e3022ae1e..3a296193c7 100644 --- a/packages/create-threenative/templates/sailing/CLAUDE.md +++ b/packages/create-threenative/templates/sailing/CLAUDE.md @@ -28,7 +28,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/shooter/AGENTS.md b/packages/create-threenative/templates/shooter/AGENTS.md index 5cd8465f0d..f0f801ceca 100644 --- a/packages/create-threenative/templates/shooter/AGENTS.md +++ b/packages/create-threenative/templates/shooter/AGENTS.md @@ -28,7 +28,7 @@ result, replacement); never stall the game. ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/shooter/CLAUDE.md b/packages/create-threenative/templates/shooter/CLAUDE.md index a87692daf8..65485c8922 100644 --- a/packages/create-threenative/templates/shooter/CLAUDE.md +++ b/packages/create-threenative/templates/shooter/CLAUDE.md @@ -30,7 +30,7 @@ result, replacement); never stall the game. ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/snow/AGENTS.md b/packages/create-threenative/templates/snow/AGENTS.md index 3438fbe6f0..48151f2c0e 100644 --- a/packages/create-threenative/templates/snow/AGENTS.md +++ b/packages/create-threenative/templates/snow/AGENTS.md @@ -28,7 +28,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/snow/CLAUDE.md b/packages/create-threenative/templates/snow/CLAUDE.md index a1a6c04eac..0635a4c9fa 100644 --- a/packages/create-threenative/templates/snow/CLAUDE.md +++ b/packages/create-threenative/templates/snow/CLAUDE.md @@ -30,7 +30,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/starter/AGENTS.md b/packages/create-threenative/templates/starter/AGENTS.md index 12abcf31e9..699949c120 100644 --- a/packages/create-threenative/templates/starter/AGENTS.md +++ b/packages/create-threenative/templates/starter/AGENTS.md @@ -20,7 +20,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/starter/CLAUDE.md b/packages/create-threenative/templates/starter/CLAUDE.md index 748931ef33..9d3686b488 100644 --- a/packages/create-threenative/templates/starter/CLAUDE.md +++ b/packages/create-threenative/templates/starter/CLAUDE.md @@ -22,7 +22,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/tower-defense/AGENTS.md b/packages/create-threenative/templates/tower-defense/AGENTS.md index 28ec63e1b0..176b61e449 100644 --- a/packages/create-threenative/templates/tower-defense/AGENTS.md +++ b/packages/create-threenative/templates/tower-defense/AGENTS.md @@ -27,7 +27,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/create-threenative/templates/tower-defense/CLAUDE.md b/packages/create-threenative/templates/tower-defense/CLAUDE.md index cee8c54dad..8b003d8e13 100644 --- a/packages/create-threenative/templates/tower-defense/CLAUDE.md +++ b/packages/create-threenative/templates/tower-defense/CLAUDE.md @@ -29,7 +29,7 @@ bridge; avoid DOM globals, dynamic `import()`, and raw physics handles. **Report ## Workflow skills - `.agents/skills/prd-creator/SKILL.md` / `.claude/skills/prd-creator/SKILL.md` — game plan and approval gate. -- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search. +- `.agents/skills/threenative-capabilities/SKILL.md` / `.claude/skills/threenative-capabilities/SKILL.md` — capability search and classic-game mechanics references. - `.agents/skills/threenative-playtest/SKILL.md` / `.claude/skills/threenative-playtest/SKILL.md` — diagnosis and proof. - `.agents/skills/threenative-assets/SKILL.md` / `.claude/skills/threenative-assets/SKILL.md` — assets and sculpting. - `.agents/skills/threenative-visuals/SKILL.md` / `.claude/skills/threenative-visuals/SKILL.md` — captures and look. diff --git a/packages/engine-mcp/README.md b/packages/engine-mcp/README.md index 605772fa14..2fb9638c13 100644 --- a/packages/engine-mcp/README.md +++ b/packages/engine-mcp/README.md @@ -22,7 +22,7 @@ the manifest's `starterKits` section (verdict `"starterKit"` when no mechanic al lexical coincidence as engine guidance. Reference search reads a gzipped stub index; detail fetches one pinned game's map and cards lazily. -The source is the latest release of [threenative-game-refs](https://github.com/jonit-dev/threenative-game-refs), +The source is the latest release of [threenative-reverse-engineer-mcp](https://github.com/ThreeNativeHQ/threenative-reverse-engineer-mcp), separate CC-BY 4.0 data. Downloads validate before replacing the cache in `$XDG_CACHE_HOME/threenative/refs` (default `~/.cache/threenative/refs`), refresh with ETags, and work offline from that cache. The index has a 5-second fetch budget; game files have 30 seconds, @@ -37,6 +37,12 @@ The save tool returns a `gh issue create` command for a POSIX shell; run it in t It does not send the contribution. Reference guardrail: mechanics and feel only, with pinned `file:line` sources; no code, level layouts, character names or art. +## One call from the command line + +A session started outside the game folder never loads its `.mcp.json`. It calls a tool once +instead: `node /node_modules/@threenative/core/mcp/engine.mjs engine_search_capabilities +'{"situation":"triple jump"}'` prints the tool's answer and exits 0, or names the error and exits 1. + ## Which manifest the server reads The first that exists wins, so the manifest can never drift from the engine the project runs: diff --git a/packages/engine-mcp/__tests__/references.spec.ts b/packages/engine-mcp/__tests__/references.spec.ts index cf0ff3ddb3..4c82d1b693 100644 --- a/packages/engine-mcp/__tests__/references.spec.ts +++ b/packages/engine-mcp/__tests__/references.spec.ts @@ -191,6 +191,20 @@ describe("reference release cache", () => { validateReferenceIndex({ ...index, stubs: [{ ...index.stubs[0], game: "missing" }] }), ).toThrow(/missing/); }); + it("accepts the editorial catalog fields the data repository adds to a release game", async () => { + const { validateReferenceIndex } = await refs(); + const [row] = index.games; + const catalogued = { + ...row, + genre: "platformer", + kind: "decompilation", + rating: 5, + subgenre: "3d platformer", + }; + expect( + validateReferenceIndex({ ...index, games: [catalogued, ...index.games.slice(1)] }).games[0], + ).toMatchObject({ id: row.id, tier: row.tier }); + }); it("merges a card that references an already saved node", async () => { const { saveReferenceGame, loadReferenceGame } = await refs(); saveReferenceGame(game, project); @@ -229,12 +243,19 @@ describe("reference release cache", () => { ).toThrow(/sha|revision/iu); expect(readFileSync(file, "utf8")).toBe(previous); }); + it("reads releases from the renamed reverse-engineer data repo by default (PRD-735)", async () => { + const { referencesBaseUrl } = await refs(); + vi.stubEnv("THREENATIVE_REFERENCES_URL", ""); + expect(referencesBaseUrl()).toBe( + "https://github.com/ThreeNativeHQ/threenative-reverse-engineer-mcp/releases/latest/download/", + ); + }); it("saves and merges by id, prioritizes local cards and returns a shell-safe contribution command", async () => { const { saveReferenceGame, loadReferenceGame } = await refs(); const saved = saveReferenceGame(game, project); expect(existsSync(path.join(project, saved.saved))).toBe(true); expect(saved.contribution.command).toContain( - "gh issue create --repo jonit-dev/threenative-game-refs", + "gh issue create --repo ThreeNativeHQ/threenative-reverse-engineer-mcp", ); saveReferenceGame( { diff --git a/packages/engine-mcp/__tests__/search.spec.ts b/packages/engine-mcp/__tests__/search.spec.ts index 6604ede97c..0a1238d52d 100644 --- a/packages/engine-mcp/__tests__/search.spec.ts +++ b/packages/engine-mcp/__tests__/search.spec.ts @@ -50,6 +50,21 @@ describe("threenative-engine-mcp", () => { ]); }); + it("leads a reverse-engineer request with the reference tools (PRD-735)", () => { + for (const situation of ["reverse engineer the mario long jump", "read the sm64 decomp"]) { + const response = searchCapabilities(situation, workspaceManifest); + expect(response.referenceTools).toEqual([ + "engine_search_references", + "engine_reference_detail", + "engine_save_reference", + ]); + expect(response.guidance).toContain("engine_search_references"); + } + expect(searchCapabilities("double jump", workspaceManifest).referenceTools).toBeUndefined(); + for (const tool of toolDefinitions().filter((t) => t.name.includes("reference"))) + expect(tool.description.toLowerCase()).toContain("reverse engineer"); + }); + it("returns a matched verdict and exposes a numeric score on every result", () => { const response = searchCapabilities("enemy walks around a wall", workspaceManifest); @@ -530,6 +545,26 @@ describe("threenative-engine-mcp", () => { }); }); + // Authoring-layer regression guard (PRD-750): every lookup word the shipped kits declare sends a + // cold "game like " request to its own kit first, so a new kit cannot take another's word. + it("routes every shipped kit word to its own kit first", () => { + const { starterKits = [] } = JSON.parse(readFileSync(workspaceManifest, "utf8")) as { + starterKits?: { name: string; words: string[] }[]; + }; + expect(starterKits.length).toBeGreaterThan(0); + const misrouted = starterKits.flatMap(({ name, words }) => + words + .map((word) => ({ + first: searchCapabilities(`make me a game like ${word}`, workspaceManifest) + .starterKits?.[0]?.name, + word, + })) + .filter(({ first }) => first !== name) + .map(({ first, word }) => `${word} -> ${first ?? "no kit"} (want ${name})`), + ); + expect(misrouted).toEqual([]); + }); + it("finds a cross-system set from a mechanically explicit complete request", () => { const results = searchResults( "sailing ship on ocean waves with buoyancy, cloth sails in wind, cannonball physics and smoke particles, crew navigating a deck with swords, islands and coastlines, and positional sound", diff --git a/packages/engine-mcp/__tests__/server.spec.ts b/packages/engine-mcp/__tests__/server.spec.ts index d5c2b2db9b..7aeabdbbb9 100644 --- a/packages/engine-mcp/__tests__/server.spec.ts +++ b/packages/engine-mcp/__tests__/server.spec.ts @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { makeTempDirSync } from "../../../test-support/temp-dir.js"; -import { handleLine } from "../src/index.js"; +import { handleLine, runCli } from "../src/index.js"; const manifestFile = path.resolve("packages/create-threenative/capabilities.json"); @@ -319,4 +319,26 @@ describe("threenative-engine-mcp stdio contract", async () => { ); expect(() => readFileSync(logFile, "utf8")).toThrow(); }); + + // Kit-loop row 1 (PRD-750): a session started outside the project never loads .mcp.json, so the + // same tools answer one call from the command line. + it("answers one tool call from the command line, and fails closed on a bad call", async () => { + const out: string[] = []; + const err: string[] = []; + const io = { + stderr: (text: string) => err.push(text), + stdout: (text: string) => out.push(text), + }; + expect( + await runCli( + ["engine_search_capabilities", '{"situation":"make me a game like mario"}'], + manifestFile, + io, + ), + ).toBe(0); + expect(JSON.parse(out.join("")).starterKits?.[0]?.name).toBe("collectathon"); + expect(await runCli(["engine_search_capabilities", "{not json"], manifestFile, io)).toBe(1); + expect(await runCli(["engine_no_such_tool", "{}"], manifestFile, io)).toBe(1); + expect(err.join("")).toMatch(/engine_no_such_tool/u); + }); }); diff --git a/packages/engine-mcp/src/index.ts b/packages/engine-mcp/src/index.ts index ef54135ba1..32de138b17 100644 --- a/packages/engine-mcp/src/index.ts +++ b/packages/engine-mcp/src/index.ts @@ -72,6 +72,8 @@ export interface ICapabilitySearchResult { export interface ICapabilitySearchResponse { /** Present when the request names a genre or game a starter kit covers; it leads the response. */ readonly starterKits?: readonly IStarterKitResult[]; + /** Present when the request asks to reverse engineer a game: the tools that read its pinned decomp. */ + readonly referenceTools?: readonly string[]; /** "starterKit": a kit matched but no engine capability did; scaffold the kit, then search its gaps. */ readonly verdict: "matched" | "none" | "starterKit"; readonly results: readonly ICapabilitySearchResult[]; @@ -721,7 +723,7 @@ export function searchCapabilities( throw new Error("engine_search_capabilities requires a non-empty situation string."); const manifest = loadCapabilityManifest(manifestFile); const starterKits = matchedStarterKits(situation, manifest.starterKits ?? []); - const response = searchMechanics(situation, manifest, scope); + const response = withReferenceTools(situation, searchMechanics(situation, manifest, scope)); if (starterKits.length === 0) return response; // The kit leads the response; the mechanics it does not cover follow unchanged. return { @@ -736,6 +738,23 @@ export function searchCapabilities( }; } +const REVERSE_ENGINEER = /\breverse[\s-]?engineer|\bdecomp|\bdisassembl/iu; +const REFERENCE_TOOLS = [ + "engine_search_references", + "engine_reference_detail", + "engine_save_reference", +]; + +/** A reverse-engineer request leads with the reference tools (PRD-735); the mechanics follow unchanged. */ +function withReferenceTools(situation: string, response: ICapabilitySearchResponse) { + if (!REVERSE_ENGINEER.test(situation)) return response; + return { + referenceTools: REFERENCE_TOOLS, + ...response, + guidance: `Reverse engineer the game with engine_search_references, then engine_reference_detail for its pinned cards. ${response.guidance}`, + }; +} + function searchMechanics( situation: string, manifest: ICapabilityManifest, @@ -872,7 +891,7 @@ const TOOL_DEFINITIONS: readonly IEngineTool[] = [ { name: "engine_search_references", description: - "Search mechanic cards and repository map nodes from classic games. Tier-2 titles link to repos to explore and save. References describe feel only; borrow algorithms, cite pinned sources, copy no code or art.", + "Reverse engineer classic games: search mechanic cards and repository map nodes from their decomps. Tier-2 titles link to repos to explore and save. References describe feel only; borrow algorithms, cite pinned sources, copy no code or art.", annotations: { destructiveHint: false, openWorldHint: true, readOnlyHint: true }, inputSchema: { type: "object", @@ -884,7 +903,7 @@ const TOOL_DEFINITIONS: readonly IEngineTool[] = [ { name: "engine_reference_detail", description: - "Read a game's pinned map or mechanic card. A node includes files, key functions, dependencies and child nodes; a card includes constants and sources.", + "Reverse engineer a game: read its pinned map or mechanic card. A node includes files, key functions, dependencies and child nodes; a card includes constants and sources.", annotations: { destructiveHint: false, openWorldHint: true, readOnlyHint: true }, inputSchema: { type: "object", @@ -900,7 +919,7 @@ const TOOL_DEFINITIONS: readonly IEngineTool[] = [ { name: "engine_save_reference", description: - "Validate and save a pinned game's repository map and mechanic cards to .threenative/refs, merged by id at the same SHA; conflicting revisions are refused. Pass a game object with version:1, id, title, repo, sha, nodes and cards. Returns a POSIX-shell contribution command for review; does not send it. Unknown fields and fenced code are refused.", + "Save what you reverse engineer: validate and save a pinned game's repository map and mechanic cards to .threenative/refs, merged by id at the same SHA; conflicting revisions are refused. Pass a game object with version:1, id, title, repo, sha, nodes and cards. Returns a POSIX-shell contribution command for review; does not send it. Unknown fields and fenced code are refused.", annotations: { destructiveHint: false, openWorldHint: false, readOnlyHint: false }, inputSchema: { type: "object", @@ -1194,7 +1213,41 @@ export async function handleLine(line: string, manifestFile: string): Promise ''` prints the tool's + * text and exits 0, or names the error and exits 1. A session started outside the project never + * loads its `.mcp.json`, and this is how it still searches (PRD-750 kit-loop row 1). + */ +export async function runCli( + argv: readonly string[], + manifestFile = defaultManifestPath(), + io: { stderr: (text: string) => void; stdout: (text: string) => void } = { + stderr: (text: string) => void process.stderr.write(text), + stdout: (text: string) => void process.stdout.write(text), + }, +): Promise { + const [name, json = "{}"] = argv; + try { + const result = await handleToolCall( + { arguments: JSON.parse(json) as unknown, name }, + manifestFile, + ); + io.stdout(`${result.content[0].text}\n`); + return 0; + } catch (error) { + io.stderr(`${name}: ${error instanceof Error ? error.message : String(error)}\n`); + return 1; + } +} + export function runServer(manifestFile = defaultManifestPath()): void { + const tool = process.argv[2]; + if (tool?.startsWith("engine_") === true) { + void runCli(process.argv.slice(2), manifestFile).then((code) => { + process.exitCode = code; + }); + return; + } // Load before accepting requests. A missing manifest is a startup failure, never an empty tool // response that an authoring agent could mistake for the engine having no capabilities. loadCapabilityManifest(manifestFile); diff --git a/packages/engine-mcp/src/references.ts b/packages/engine-mcp/src/references.ts index c0ab0715c3..c2f97f2e55 100644 --- a/packages/engine-mcp/src/references.ts +++ b/packages/engine-mcp/src/references.ts @@ -89,7 +89,7 @@ export interface IReferenceGame { } const DEFAULT_REFERENCES_URL = - "https://github.com/jonit-dev/threenative-game-refs/releases/latest/download/"; + "https://github.com/ThreeNativeHQ/threenative-reverse-engineer-mcp/releases/latest/download/"; const FETCH_TIMEOUT_MS = 5_000; const GAME_FETCH_TIMEOUT_MS = 30_000; const ID = /^[a-z0-9][a-z0-9-]*$/u; @@ -198,7 +198,20 @@ export function validateReferenceIndex(value: unknown, where = "index"): IRefere const index = record(value, where, ["version", "games", "stubs"]); if (index.version !== 1) fail(`${where}.version`, "expected 1"); const games = list(index.games, `${where}.games`, (entry, at) => { - const game = record(entry, at, ["id", "title", "platform", "repo", "tier"]); + // `kind`, `rating`, `genre` and `subgenre` are editorial catalog fields the data repository + // publishes and search never reads; a new catalog field is one name here and one schema change + // there, never a rejection of the whole release. + const game = record(entry, at, [ + "id", + "title", + "platform", + "repo", + "tier", + "kind", + "rating", + "genre", + "subgenre", + ]); if (game.tier !== 1 && game.tier !== 2) fail(`${at}.tier`, "expected 1 or 2"); return { id: id(game.id, `${at}.id`), @@ -509,7 +522,7 @@ function shellQuote(value: string): string { return `'${value.replaceAll("'", "'\"'\"'")}'`; } -const CONTRIBUTION_REPO = "jonit-dev/threenative-game-refs"; +const CONTRIBUTION_REPO = "ThreeNativeHQ/threenative-reverse-engineer-mcp"; /** * Saves what an agent mapped in a game it explored, merged by id over its earlier save, and diff --git a/packages/playtest/__tests__/bridgeClient.spec.ts b/packages/playtest/__tests__/bridgeClient.spec.ts index 3f2221c22f..f86995dd2f 100644 --- a/packages/playtest/__tests__/bridgeClient.spec.ts +++ b/packages/playtest/__tests__/bridgeClient.spec.ts @@ -58,6 +58,10 @@ test("scenario flags accumulate and expand a project glob in sorted order", asyn projectPath, ); expect(hyphenatedGlob.scenarioPaths).toEqual(["playtests/terminal-menu.playtest.json"]); + + // Kit-loop row 2 (PRD-750): a cold agent quoted a shell brace list and got SCENARIO_UNREADABLE. + const braces = parseStandalonePlaytestArgs(["--scenario", "playtests/{b,terminal-*}.playtest.json"], projectPath); + expect(braces.scenarioPaths).toEqual(["playtests/b.playtest.json", "playtests/terminal-menu.playtest.json"]); }); test("a scenario glob that matches nothing fails closed", async () => { diff --git a/packages/playtest/__tests__/frame-budget.spec.ts b/packages/playtest/__tests__/frame-budget.spec.ts index 6553f2c230..61811494ae 100644 --- a/packages/playtest/__tests__/frame-budget.spec.ts +++ b/packages/playtest/__tests__/frame-budget.spec.ts @@ -1,7 +1,12 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { evaluateRichPlaytestAssertions } from "../src/index.js"; +import { foreignGpuProcesses } from "../src/evaluators/frame-cost.js"; +import { evaluatePerformanceAssertion } from "../src/evaluators/render-evidence.js"; import { validatePlaytestScenario } from "../src/scenario.js"; +import { makeTempDirSync } from "../../../test-support/temp-dir.js"; import type { IPlaytestCaptureProvenance, IPlaytestReport } from "../src/report.js"; const RTX_2080 = { architecture: "turing", description: "NVIDIA", device: "NVIDIA GeForce RTX 2080", vendor: "nvidia" }; @@ -198,3 +203,47 @@ describe("frame bound by display", () => { expect(messages.join(" ")).toMatch(/did not measure the CPU frame split/u); }); }); + +describe("a cost bound measured beside other GPU work", () => { + it("names the other GPU processes and says to rerun, not to cut the game", () => { + const { diagnostics } = evaluatePerformanceAssertion( + { maxFrameMsP95: 16.7 }, + frames(16.7, 3), + "perf.json", + { display: "virtual", foreignGpu: [4242, 4343], gpuP95: 20, gpuSamples: 8 }, + ); + const failed = diagnostics.find(({ code }) => code === "TN_PLAYTEST_PERFORMANCE_ASSERTION_FAILED"); + expect(failed?.message).toMatch(/while 2 other GPU processes ran \(4242, 4343\)/u); + expect(failed?.suggestion).toMatch(/rerun .* when they exit/u); + }); + + it("does not call an unmeasured cost contended", () => { + const { diagnostics } = evaluatePerformanceAssertion( + { maxFrameMsP95: 16.7 }, + frames(16.7, 3), + "perf.json", + { display: "virtual", foreignGpu: [4242] }, + ); + const failed = diagnostics.find(({ code }) => code === "TN_PLAYTEST_PERFORMANCE_ASSERTION_FAILED"); + expect(failed?.message).toMatch(/cannot be judged/u); + expect(failed?.suggestion).not.toMatch(/rerun/u); + }); + + it("finds Chromium GPU processes that are not this run's descendants", () => { + const proc = makeTempDirSync("foreign-gpu"); + const task = (pid: number, ppid: number, cmd: string) => { + mkdirSync(path.join(proc, String(pid))); + writeFileSync(path.join(proc, String(pid), "cmdline"), cmd.split(" ").join("\0")); + writeFileSync(path.join(proc, String(pid), "stat"), `${pid} (chrome) S ${ppid} 0 0`); + }; + task(100, 1, "node runner.js"); + task(101, 100, "chrome --type=browser"); + task(102, 101, "/home/u/.cache/ms-playwright/chromium-1200/chrome --type=gpu-process --use-angle"); + task(200, 1, "/home/u/.cache/ms-playwright/chromium-1200/chrome --type=browser"); + task(201, 200, "/home/u/.cache/ms-playwright/chromium-1200/chrome --type=gpu-process --use-angle"); + task(400, 1, "/usr/share/code/code --type=gpu-process"); + task(300, 1, "chrome --type=renderer"); + expect(foreignGpuProcesses(100, proc, "linux")).toEqual([201]); + expect(foreignGpuProcesses(100, proc, "darwin")).toEqual([]); + }); +}); diff --git a/packages/playtest/src/evaluators/frame-cost.ts b/packages/playtest/src/evaluators/frame-cost.ts index 64b297b0b9..15d39ae647 100644 --- a/packages/playtest/src/evaluators/frame-cost.ts +++ b/packages/playtest/src/evaluators/frame-cost.ts @@ -1,3 +1,6 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + import { hostDisplayRequested } from "../runner/captureEnvironment.js"; const FRAME_BUDGET = "TN_FRAME_BUDGET:"; @@ -14,6 +17,44 @@ export function frameDisplay(env: NodeJS.ProcessEnv = process.env, platform: str return platform === "linux" && !hostDisplayRequested(env) ? "virtual" : "real"; } +/** + * Playwright Chromium GPU processes on this host that this run did not start (Linux `/proc`; elsewhere none + * are seen). Each one shares the GPU with the measured game, so a cost bound it fails is + * contended: the number holds their load too. A process this run started descends from `ownPid`. + */ +export function foreignGpuProcesses(ownPid: number = process.pid, procRoot = "/proc", platform: string = process.platform): readonly number[] { + if (platform !== "linux") return []; + const read = (pid: string, file: string): string | undefined => { + try { + return readFileSync(join(procRoot, pid, file), "utf8"); + } catch { + return undefined; // the process exited between the listing and the read + } + }; + const parentOf = (pid: number): number | undefined => { + const stat = read(String(pid), "stat"); + // `pid (comm) state ppid ...`; comm may hold spaces, so split after its closing parenthesis. + const ppid = stat === undefined ? undefined : Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]); + return ppid === undefined || !Number.isSafeInteger(ppid) ? undefined : ppid; + }; + const ours = (pid: number): boolean => { + for (let current: number | undefined = pid, hops = 0; current !== undefined && current > 1 && hops < 64; hops++) { + if (current === ownPid) return true; + current = parentOf(current); + } + return false; + }; + return readdirSync(procRoot) + .filter((entry) => { + const argv = /^\d+$/u.test(entry) ? (read(entry, "cmdline") ?? "").split("\0") : []; + // Playwright's own Chromium only: an editor's or a desktop browser's GPU process idles here. + return argv.includes("--type=gpu-process") && (argv[0] ?? "").includes("ms-playwright"); + }) + .map(Number) + .filter((pid) => !ours(pid)) + .sort((a, b) => a - b); +} + /** * The fewest GPU timestamp samples a window may hold and still be judged. No measured value says * how many are enough: windows seen on the kits hold 1 to 6 (the engine skips static frames, so an diff --git a/packages/playtest/src/evaluators/perf-signals-world.ts b/packages/playtest/src/evaluators/perf-signals-world.ts index 742817fa5f..d8c4009043 100644 --- a/packages/playtest/src/evaluators/perf-signals-world.ts +++ b/packages/playtest/src/evaluators/perf-signals-world.ts @@ -2,7 +2,7 @@ import { readPath, jsonEqual, textValue } from '../assertion-report.js'; // Extracted verbatim from assertion-evaluators.ts (PRD-182 Phase 2); do not edit semantics here. import type { IEvaluationContext } from "./context.js"; import { classifyAdapter, type IAdapterClassification } from "./adapter-class.js"; -import { frameDisplay, gpuWindows } from "./frame-cost.js"; +import { foreignGpuProcesses, frameDisplay, gpuWindows } from "./frame-cost.js"; import { evaluatePerformanceAssertion, evaluateResourceAnyOfAssertion, hasFinalPathExpectation, evaluatePathAssertion, pathValuePass, matchingSignals, evaluateWorldAssertion, evaluateAudioAssertion } from "./helpers.js"; function gpuLane(consoleTexts: readonly unknown[]): { gpuP95?: number; gpuSamples?: number } { @@ -20,7 +20,7 @@ export function emitPerfSignalsWorld(ctx: IEvaluationContext): void { budget.maxFrameMsP95 === undefined ? scenarioAssertions.performance : { ...scenarioAssertions.performance, maxFrameMsP95: budget.maxFrameMsP95 }, input.report.observations?.performanceSeries, input.scenario.sourcePath, - { display: frameDisplay(), ...gpuLane((input.report.observations?.console ?? []).map(({ text }) => text)) }, + { display: frameDisplay(), foreignGpu: foreignGpuProcesses(), ...gpuLane((input.report.observations?.console ?? []).map(({ text }) => text)) }, ); // The budget rides on the rows that always exist, so a run says which budget held it, or why none did. assertions.push(...result.assertions.map((row) => diff --git a/packages/playtest/src/evaluators/render-evidence.ts b/packages/playtest/src/evaluators/render-evidence.ts index d643dc3c4f..7e6b584d1c 100644 --- a/packages/playtest/src/evaluators/render-evidence.ts +++ b/packages/playtest/src/evaluators/render-evidence.ts @@ -241,6 +241,8 @@ export interface IFrameLane { readonly gpuP95?: number; /** The timestamp samples that GPU p95 rests on; absent when its window did not count them. */ readonly gpuSamples?: number; + /** Other GPU processes on the host during the run (`foreignGpuProcesses`); a cost they share is contended. */ + readonly foreignGpu?: readonly number[]; } /** `DEFAULT_HITCH_MS` in the engine's frame budget: the present gap from which a frame is a hitch. */ @@ -301,10 +303,12 @@ function emitFrameBound( pass, }); if (pass) return; + const foreign = lane.foreignGpu ?? []; + const contended = missing.length === 0 && !costPass && foreign.length > 0; const detail = missing.length > 0 ? `cannot be judged: the run did not measure ${missing.join(" or ")}` : !costPass - ? `cost p95 ${round(costP95)} ms (CPU ${round(cpuP95)}, GPU ${round(lane.gpuP95)}) is over ${ceiling} ms` + ? `cost p95 ${round(costP95)} ms (CPU ${round(cpuP95)}, GPU ${round(lane.gpuP95)}) is over ${ceiling} ms${contended ? ` while ${foreign.length} other GPU process${foreign.length === 1 ? "" : "es"} ran (${foreign.join(", ")})` : ""}` : `rAF interval p95 ${round(intervalP95)} ms on a real display is over ${ceiling} ms`; const fewSamples = missing.length === 0 && costPass && thinGpu; diagnostics.push({ @@ -315,6 +319,8 @@ function emitFrameBound( sourcePath, suggestion: fewSamples ? "Keep the scene moving through the judged window (hold a move input or loop a route): the engine skips static frames and takes no GPU sample on them." + : contended + ? "Other GPU work shared this measurement, so the cost holds its load too: rerun this scenario when they exit, before you change the game. A contended number is not the game's cost, so do not trade its look to pass it." : missing.length > 0 ? "Keep the engine frame budget installed and run on an adapter with timestamp queries: an unmeasured cost is not a zero." : "Read the TN_FRAME_BUDGET marker for this run: it names which frame phase or GPU pass owns the time.", diff --git a/packages/playtest/src/runner/config.ts b/packages/playtest/src/runner/config.ts index e2477bfa2d..04d1ccd861 100644 --- a/packages/playtest/src/runner/config.ts +++ b/packages/playtest/src/runner/config.ts @@ -405,7 +405,7 @@ export function parseStandalonePlaytestArgs(argv: readonly string[], cwd = proce }; } -const SCENARIO_GLOB = /[*?[]/u; +const SCENARIO_GLOB = /[*?[{]/u; function expandScenarioPattern(projectPath: string, pattern: string): string[] { if (!SCENARIO_GLOB.test(pattern)) return [pattern]; @@ -449,14 +449,24 @@ function matchGlobParts(pattern: readonly string[], file: readonly string[]): bo } function segmentMatches(pattern: string, value: string): boolean { - let expression = "^"; + return new RegExp(`^${segmentExpression(pattern)}$`, "u").test(value); +} + +// `{a,b}` is the shell's brace list; an agent quotes one and expects it to work (PRD-750 row 2). +// ponytail: one level, no nesting; nest when a scenario name needs it. +function segmentExpression(pattern: string): string { + let expression = ""; for (let index = 0; index < pattern.length; index += 1) { const character = pattern[index]!; - if (character === "*") expression += "[^/]*"; + const close = character === "{" ? pattern.indexOf("}", index) : -1; + if (close > index) { + expression += `(?:${pattern.slice(index + 1, close).split(",").map(segmentExpression).join("|")})`; + index = close; + } else if (character === "*") expression += "[^/]*"; else if (character === "?") expression += "[^/]"; else expression += escapeRegExp(character); } - return new RegExp(`${expression}$`, "u").test(value); + return expression; } function escapeRegExp(value: string): string { diff --git a/packages/playtest/src/runner/sampling.ts b/packages/playtest/src/runner/sampling.ts index 6d97736278..e316c2e68b 100644 --- a/packages/playtest/src/runner/sampling.ts +++ b/packages/playtest/src/runner/sampling.ts @@ -863,7 +863,7 @@ export async function sampleHudAudit(page: Page): Promise<{ items: IPlaytestHudA if (element.closest("script, style, canvas, noscript") !== null) continue; let ownText = ""; for (const node of element.childNodes) { - if (node.nodeType === Node.TEXT_NODE) ownText += node.textContent ?? ""; + if (node.nodeType === 3 /* Node.TEXT_NODE; the global is browser-only */) ownText += node.textContent ?? ""; } const text = ownText.replace(/\s+/gu, " ").trim(); if (text === "") continue; diff --git a/scripts/__tests__/asset-perf-loop.spec.ts b/scripts/__tests__/asset-perf-loop.spec.ts index 37a69c67b6..b4e762a5a0 100644 --- a/scripts/__tests__/asset-perf-loop.spec.ts +++ b/scripts/__tests__/asset-perf-loop.spec.ts @@ -68,8 +68,8 @@ describe("asset perf loop", () => { expect(html).toContain("Latest iteration #1 of 2: cooked."); expect(html).toContain("pending"); expect(html).not.toContain("#2: 99"); - expect(html).not.toContain("/state"); - expect(renderHtml(ledger, new Date(0), { serving: true })).toContain('fetch("/state"'); + expect(html).not.toContain("/api/asset-perf"); + expect(renderHtml(ledger, new Date(0), { serving: true })).toContain('fetch("/api/asset-perf"'); expect(() => appendIteration(ledger, { measurement, diff --git a/scripts/__tests__/ci-efficiency.spec.ts b/scripts/__tests__/ci-efficiency.spec.ts index 6a4e698d5a..2bbf1731ae 100644 --- a/scripts/__tests__/ci-efficiency.spec.ts +++ b/scripts/__tests__/ci-efficiency.spec.ts @@ -543,11 +543,7 @@ describe("PRD-373 fail-closed required verdict", () => { it("classifies the generated speed-loop record as prose, not a full board", () => { // Only scripts/ci-speed-loop.mjs reads these files, and no CI job runs that script. - for (const file of [ - "docs/ci-speed/ledger.json", - "docs/ci-speed/index.html", - "docs/ci-speed/runs/39070176677.jobs.json", - ]) { + for (const file of ["docs/ci-speed/ledger.json", "docs/ci-speed/runs/39070176677.jobs.json"]) { expect(prosePlan(file).selection).toBe("prose"); } }); diff --git a/scripts/__tests__/kit-loop.spec.ts b/scripts/__tests__/kit-loop.spec.ts new file mode 100644 index 0000000000..b3d03fb20d --- /dev/null +++ b/scripts/__tests__/kit-loop.spec.ts @@ -0,0 +1,151 @@ +import { rmSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { makeTempDirSync } from "../../test-support/temp-dir.js"; + +const { appendRow, goalsFor, measureDir, renderHtml } = await import( + new URL("../kit-loop.mjs", import.meta.url).href +); +const { emptyLedger } = await import(new URL("../ci-speed-loop.mjs", import.meta.url).href); + +const inputs = { + "transcript.json": { + type: "result", + num_turns: 41, + duration_ms: 1_380_000, + usage: { + input_tokens: 1_000, + output_tokens: 20_000, + cache_creation_input_tokens: 79_000, + cache_read_input_tokens: 2_000_000, + }, + }, + "gates.json": { + scenarios: { total: 4, green: 3 }, + presentedP95Ms: 15.2, + costP95Ms: 10.1, + budgetMs: 16.7, + visualVerdict: "CLOSE", + }, + "friction.json": { searchMisses: 2, duplicates: 1, wrongApi: 3, retries: 4 }, + "play-grade.json": { grade: 3, judge: "opus fresh judge" }, + "retro.json": { + worked: ["kit found by the mario alias"], + failed: ["coin ring scenario red"], + frictions: [{ text: "searched 'triple jump' and missed", transcriptLine: 212 }], + }, +}; + +function coldBuild(overrides: Record = {}) { + const dir = makeTempDirSync("kit-loop"); + for (const [name, value] of Object.entries({ ...inputs, ...overrides })) { + if (value !== undefined) writeFileSync(path.join(dir, name), JSON.stringify(value)); + } + return dir; +} + +describe("kit-loop", () => { + it("turns a cold build into K1-K4 and the retro", () => { + const row = measureDir(coldBuild()); + expect(row.metrics).toEqual({ + k1Tokens: 100_000, + k1WallMin: 23, + k1Turns: 41, + k2ScenarioPct: 75, + k2PresentedP95Ms: 15.2, + k2CostP95Ms: 10.1, + k2BudgetMs: 16.7, + k2BudgetMet: true, + k2Visual: "CLOSE", + k3Friction: 10, + k4Grade: 3, + }); + expect(row.friction).toEqual({ searchMisses: 2, duplicates: 1, wrongApi: 3, retries: 4 }); + expect(row.retro.frictions[0].transcriptLine).toBe(212); + }); + + it("fails closed on a missing input file or a missing field, never a zero", () => { + for (const name of Object.keys(inputs)) { + const dir = coldBuild(); + rmSync(path.join(dir, name)); + expect(() => measureDir(dir)).toThrow(name); + } + const noUsage = coldBuild({ "transcript.json": { num_turns: 3, duration_ms: 1 } }); + expect(() => measureDir(noUsage)).toThrow(/usage/u); + const { costP95Ms: _, ...noCost } = inputs["gates.json"]; + expect(() => measureDir(coldBuild({ "gates.json": noCost }))).toThrow(/costP95Ms/u); + const badGrade = coldBuild({ "play-grade.json": { grade: 0 } }); + expect(() => measureDir(badGrade)).toThrow(/grade/u); + const badVerdict = coldBuild({ "gates.json": { ...inputs["gates.json"], visualVerdict: "" } }); + expect(() => measureDir(badVerdict)).toThrow(/visualVerdict/u); + const fourFrictions = coldBuild({ + "retro.json": { + ...inputs["retro.json"], + frictions: Array(4).fill({ text: "x", transcriptLine: 1 }), + }, + }); + expect(() => measureDir(fourFrictions)).toThrow(/top 3/u); + }); + + it("numbers rows, marks the first as the baseline and sets targets from it", () => { + const one = appendRow(emptyLedger(), measureDir(coldBuild()), { + kit: "collectathon", + reference: "sm64", + }); + const two = appendRow(one, measureDir(coldBuild()), { kit: "shooter", reference: "quake" }); + expect(two.iterations.map((r: { id: number; decision: string }) => [r.id, r.decision])).toEqual( + [ + [1, "baseline"], + [2, "iteration"], + ], + ); + const goals = Object.fromEntries( + goalsFor(two.iterations[0].metrics).map((g: { key: string; goal: number }) => [ + g.key, + g.goal, + ]), + ); + expect(goals).toEqual({ k1Tokens: 60_000, k2ScenarioPct: 100, k3Friction: 5, k4Grade: 4 }); + expect(() => appendRow(one, measureDir(coldBuild()), { kit: "", reference: "x" })).toThrow( + /kit/u, + ); + }); + + it("renders the KPIs against the baseline and targets, and an empty ledger as baseline pending", () => { + const ledger = appendRow(emptyLedger(), measureDir(coldBuild()), { + kit: "collectathon", + reference: "sm64", + }); + const html = renderHtml(ledger, new Date(0)); + expect(html).toContain("Kit authoring loop"); + expect(html).toContain("≤ 60000 tokens"); + expect(html).toContain("searched 'triple jump' and missed"); + expect(renderHtml(emptyLedger(), new Date(0))).toContain("Baseline pending"); + }); + + // Rows 2-3 of the real ledger: 22/22 scenarios, frame budget over. K2 is not met on scenarios alone. + it("marks K2 met only with all scenarios green, the frame budget met and a CLOSE verdict", () => { + const k2Pill = (gates: Record) => { + const ledger = appendRow(emptyLedger(), measureDir(coldBuild({ "gates.json": gates })), { + kit: "collectathon", + reference: "sm64", + }); + const row = renderHtml(ledger, new Date(0)) + .split("") + .find((r: string) => r.includes("K2 kit scenarios")); + return row?.match(/class="pill[^"]*">(met|open) { expect(html).toContain(label); }); }); + + describe("the hub tabs (PRD-751)", () => { + const ledgerFile = async (text: string) => { + const file = path.join(await makeTempDir("dashboard-tab"), "ledger.json"); + await writeFile(file, text); + return file; + }; + + it("puts every tab in the nav and marks the current one", async () => { + const html = withNav( + await readFile(new URL("../perf-live.html", import.meta.url), "utf8"), + "/ci-speed", + ); + for (const href of ["/", "/midway", "/shadows", "/ci-speed", "/asset-perf", "/kit-loop"]) + expect(html).toContain(`href="${href}"`); + expect(html).toContain(''); + expect(html.match(/