The method
Standing rules, small milestones, and a review by someone who did not write the code.
The spec says what to build. It says nothing about how to work, and how you work is what decides whether the thing ever finishes.
Three pieces do that job. A file of standing rules the agent reads at the top of every session. A list of milestones small enough that you can actually read the diff at the end of one. And a loop with a reviewer in it who did not write the code.
None of this is clever. It is the discipline a team already uses on a shared codebase, applied to a codebase where your collaborator starts every morning with no memory of yesterday.
Standing conventions belong in a file
Every time you type "remember, the compiler has to stay pure," you are paying interest on something you should have written down once. And you will not always remember to type it. Session one you explain the rule carefully. Session four you are tired, you skip it, the agent does the reasonable thing, and the reasonable thing is the wrong thing.
8BEETY's CLAUDE.md is short. This is the shape of what it says: read SPEC.md before doing anything. TypeScript strict, no any. The compiler is a pure function and never reads global state. The worklet imports nothing from the rest of the app. All timing is 60 fps frames. Do not add dependencies without asking. Run npm test and npm run build before declaring a milestone done. Work one milestone at a time.
Notice what is not in there: nothing about tabs, quote style, or where the braces go, because every line that made the file is either an architectural invariant or a verification gate, and each one was paid for.
- "The compiler never reads global state" is what makes the WAV export sound like the playback. A compile that quietly reaches into the store works fine in a browser and cannot be tested in Node, and you find that out at the moment you want a hundred fast tests, which is later than you want to find it out.
- "The worklet imports nothing from the rest of the app" is what lets one DSP file load as an AudioWorklet in the browser and get imported by the test runner under Node. One rule, and the audio engine becomes something you can assert on.
- "All timing is 60 fps frames" stops the agent from doing the obvious thing, which is scheduling audio in seconds from the main thread. That version looks correct and quietly destroys determinism, which means it also destroys the export path and most of the test suite.
- "Run the tests and the build before declaring a milestone done" converts "done" from an opinion into two commands with real output under them.
- "Do not add dependencies without asking" keeps the dependency list a decision rather than a side effect of some task nobody reviewed closely.
Five rules, five categories of expensive mistake. None of them are preferences.
# CLAUDE.md
Standing rules for this repo. Read this file and `SPEC.md` at the start of
every session, before you touch anything.
## Project
8BEETY turns a MIDI file into chiptune in the browser: NES, Game Boy, SNES,
or Sega Genesis. The file never leaves the user's machine. Static site, no
backend, no accounts.
The mental model that settles arguments: this is not a synthesizer that
receives notes. It is a compiler that produces a register script, plus a
player that renders that script.
## Core rules
These are architectural invariants. Breaking one is a rewrite, not a fix.
1. Read `SPEC.md` before doing anything. It is the source of truth. If the
code and the spec disagree, the spec is right until someone changes the
spec first.
2. All timing is 60 fps frames. Parameter changes land on frame boundaries
and nowhere else. Never schedule audio events in seconds from the main
thread.
3. `compile(song, project, profile)` is a pure function. No global state,
no audio APIs, no randomness, no clock reads. Same inputs, same
FrameScript, every time.
4. `src/audio/apu-worklet.ts` imports nothing from the rest of the app and
touches no worklet globals at the top level. It has to load as an
AudioWorklet in the browser and import cleanly into Vitest under Node.
Both, always.
5. TypeScript strict. No `any`. No `@ts-expect-error` without a comment
naming what it is waiting on.
6. Playback, WAV export, and video export all go through the same `ApuCore`.
If you are about to write a second render path, stop and ask.
## Stack
- TypeScript, strict.
- Vitest, run under Node. Every test has to pass without a browser.
- Zustand store for user edits. Edits trigger a debounced recompile that is
hot-swapped into the running worklet without stopping playback.
- `@tonejs/midi` for import, `zod` for project-file validation, `lz-string`
for share links, and nothing else you didn't ask for. The list is closed.
Adding to it takes an explicit decision, not a passing convenience.
## Layout
```
SPEC.md source of truth
CLAUDE.md this file
docs/superpowers/plans/ implementation plans, as executed
src/engine/midi-import.ts .mid -> Song
src/engine/compile.ts Song + Project -> FrameScript (the pure one)
src/engine/pitch.ts register math, all four chips
src/engine/instruments.ts macro presets and tweaks
src/engine/chip-profiles.ts channel layouts per chip
src/audio/apu-worklet.ts all DSP, plus ApuCore
src/audio/render.ts offline render -> WAV
src/viz/ lane renderer, console shells, video export
src/store.ts Zustand store
tests/ Vitest, Node only
```
## What NOT to do
- Do NOT add a dependency. If a task seems to need one, stop and ask.
- Do NOT import anything from `src/` inside the worklet.
- Do NOT put Nintendo or Sega logos, characters, or trade dress in the UI,
and do not bundle copyrighted music as a demo. Asked and answered.
- Do NOT add a server, an account system, or an upload. There is no backend
and there is not going to be one.
- Do NOT "fix" the NES timer floor. Timers below 8 go silent on real
hardware and that is preserved on purpose. See `SPEC.md`, domain model.
- Do NOT reformat or refactor files the task did not ask you to change.
## Verification gates
Before you say a task or a milestone is done, run both of these and paste
the real output:
```bash
npm test
npm run build
```
"Should pass" is not passing. Work one milestone at a time, and one task
inside it at a time.That is the filled-in one: 8BEETY's rules and stack, in the shape the file takes once the eight lines quoted above grow into something a session reads. The blank version, with bracketed slots and notes on what belongs under each heading, is CLAUDE.md. Start from the blank one and use this one to calibrate how specific the answers have to get before they stop being decoration.
Milestones, not monoliths
Ask an agent for the whole app and it will give you one. The trouble arrives at review time. What lands is too large to hold in your head at once, and you cannot separate the parts that are right from the parts that merely compile, so you approve the whole thing, because the alternative is spending your evening reading code you did not write. That is not review. That is surrender with a commit message on top.
8BEETY went M0 to M9: scaffold, MIDI import, the compiler, poly modes, the UI, the Game Boy chip, exports, regions, chord assist, polish. The bigger post-launch features, the 16-bit chips and the console-art video export, used the same structure.
A milestone has three properties, and one that misses any of them is not a milestone.
Small enough to review. You can read the entire diff in one sitting and form an actual opinion about it. If you cannot describe the deliverable in one sentence without an "and" in it, you have two milestones and you are going to find that out later, at a worse time.
Ends green. The full test suite and a production build, both run, both passing, output in front of you. Not at the end of the project. At the end of every milestone.
Ships something a person can perceive. For 8BEETY that was literal: every milestone changed what came out of the speakers or what was on the screen. "Refactor the compiler" is a task inside a milestone, never a milestone, because there is nothing at the end of it to look at.
The repo keeps all twelve implementation plans in docs/superpowers/plans/, exactly as they were executed. Reading one of those is worth more than reading this section.
The repo does not keep a file like the one below. Its plans live in docs/superpowers/plans/, one per feature, and the acceptance criteria live in SPEC.md. What follows pulls the ten milestones into the one table you should actually write. The names and the standing gate are 8BEETY's. The per-row gates are reconstructed: some are checks the project really ran, put on the milestone they belong to, and some are what that row's gate should have been.
# Milestone plan (reconstructed from 8BEETY's ten milestones)
Spec: `SPEC.md`. Acceptance criteria live there, per milestone. This file is
the order and the gates. The names are the real ones; the per-row gates are
written for this guide, not copied out of the repo.
**Standing gate, every row:** `npm test` green and `npm run build` green,
both actually run, output pasted. Rows below name what is checked on top of
that.
| ID | Deliverable | Ships | Extra gate |
| --- | --- | --- | --- |
| M0 | Scaffold: repo, TypeScript strict, Vitest, production build, one empty screen | The app boots and shows an empty state | none, the standing gate is the point of M0 |
| M1 | MIDI import: `.mid` to a normalized `Song` with notes and a tempo map | You drop in a file and see its tracks and notes on screen | a fixture `.mid` imports to the notes and tempo map that fixture actually has |
| M2 | The compiler: `compile(song, project, profile)` to a `FrameScript`, NES only | The first thing you can hear: a MIDI file playing back as an NES | NES timer round-trips through the spec's formula; LFSR bit sequence for the noise channel; the fixture pattern compared by ear against a FamiTracker export of the same thing |
| M3 | Poly modes: top note, bottom note, arp, split. GM drum map | Chords stop losing notes, arps do the chiptune shimmer, drums land on noise | arp cycling, drum priority, and octave-fold warnings pinned by unit tests |
| M4 | The real UI: chip rack, channel cards, track list, transport, bar ruler | Someone who has never seen it can load a MIDI and arrange it without you in the room | a person who is not you drives it start to finish, unprompted |
| M5 | Game Boy: a second chip profile behind the same FrameScript | You switch consoles mid-playback and the arrangement remaps onto the new channels | GB period math round-trips; switching chips does not stop playback |
| M6 | Exports: offline render through the same `ApuCore`, WAV encoder | A file on disk that sounds like the browser did | chunked realtime rendering and whole-file offline rendering are sample-exact identical |
| M7 | Regions: per-track splits, layer modes | The chorus stops having to sound like the verse | compiler tests over region boundaries and layer modes |
| M8 | Chord Assist: key and progression detection, diatonic 7ths and 9ths, substitutions | Suggestions you can accept and immediately hear | detection tested against fixture progressions |
| M9 | Polish | The thing you would show someone | the end-to-end smoke test compiles a two-track song for every chip and renders two seconds of audio that is non-silent and in range |
Post-launch features run the same way, one at a time, same gates. The 16-bit
chips and the console-art video export were each planned, built, and
reviewed as their own sequence.
## Rules for this file
1. One milestone at a time. M3 does not start because M2 got boring.
2. A milestone you cannot write in one sentence without an "and" is two
milestones. Split it in this file before you start, not halfway through.
3. M0 is scaffold and nothing else. Its whole job is proving the two gate
commands exist and pass.
4. The "Ships" column is not optional. If nobody can hear or see the result,
you have planned a chunk of code instead of a slice of product.
5. When a milestone lands, tick its acceptance criteria off in `SPEC.md` and
start the next one in a fresh session.The blank version of that table, with the rows empty and notes on how to size a milestone, is Milestone plan. This one is what the rows look like once you fill them in for a real project.
Plan, build, review, and the review is not optional
For anything bigger than a single file, 8BEETY ran four steps.
- A design doc. Decisions and trade-offs written down and approved by the human before any plan exists.
- An implementation plan. Bite-sized tasks. Each task names the failing test to write first, the code to write, and the exact commit message.
- A fresh-context implementation, one per task. The implementer gets the task brief, not the conversation history.
- An independent review, one per task, against the spec, by a reviewer that did not write the code. Then a whole-feature review at the end.
Step 3 is the one people resist, because throwing away a warm session feels wasteful. It is the reverse. A long session is carrying a half-abandoned approach from two hours ago, a correction it is still quietly routing around, and a summary of a summary of what you decided this morning. A fresh context inherits none of that. It reads the standing rules, reads one section of the spec, does the one thing, and stops.
# Task: add NES pitch conversion to the compiler
## Read first
1. `CLAUDE.md`. The standing rules apply here and are not repeated below.
2. `SPEC.md`, the domain model section. It is the source of truth for this
task. If anything below contradicts it, the spec is right and you should
stop and say so.
3. `src/engine/chip-profiles.ts`, for the NES channel layout and its ranges.
Do not read the rest of the repo. Do not summarize what you read back to me.
## Where this sits
- Milestone: M2, the compiler, NES only.
- Task 2 of 6 in that milestone.
- Already built and working: MIDI import produces a normalized `Song`, and
`compile()` already emits `trig` and `volume` for the pulse channels.
- Deliberately not built yet, and not your problem: the Game Boy, the 16-bit
chips, poly modes, and anything in `src/audio/`.
## The task
Add `noteToNesTimer()` to `src/engine/pitch.ts` and use it in `compile()` so
the `period` array carries real NES timer values instead of the placeholder.
## Write the failing test first
- File: `tests/pitch.test.ts`
- It asserts three things, using the numbers in the spec, not numbers you
derive from the code you are about to write:
1. A round trip. For every MIDI note the NES profile advertises,
`noteToNesTimer(note)` produces a timer whose frequency, recovered
through `1789773 / (16 * (t + 1))`, is the closest the chip can get to
that note. Not "close enough". The closest available integer timer.
2. High notes come back detuned, and that is correct. Assert the drift is
present rather than asserting it is absent.
3. A timer below 8 is emitted as silence, because that is what the real
hardware does. It is not clamped up to 8 and it is not an error.
- Run the test and show me the failure, with the real output, before you
write any implementation. A test that passes before the code exists is
testing nothing.
## Then write the code
You may create or modify:
- `src/engine/pitch.ts`
- `src/engine/compile.ts`
- `tests/pitch.test.ts`
Anything not on that list is out of scope.
## Do not
- Do not touch `src/audio/`. The worklet is not part of this task.
- Do not add a dependency, including a music theory or frequency package.
Stop and ask.
- Do not "fix" the below-8 silence. It is in the spec on purpose.
- Do not reformat or rename anything you were not asked to change.
## Green gate
Run these, in this order, and paste the actual output:
```bash
npm test
npm run build
```
Both must pass. "Passes locally" without the output is not a pass.
## Commit
One commit, this message exactly:
```
feat(engine): NES timer pitch conversion in compile()
```
## If the spec and this task disagree, stop
Do not reconcile them yourself and do not pick whichever is easier to build.
Quote both, say which you think is right and why, and wait.That is what one row of the plan looks like turned into a session. The blank version, with every slot bracketed and notes on when a fresh context is worth it, is Milestone build prompt.
Step 4 is the one people skip, and it is where the bugs died. Real catches from those review passes: a codec level that corrupted exported videos, two octaves silently dropped on the SNES, a double-gain bug that clipped all 16-bit audio, WAV exports that lost a hard-panned channel entirely, and an FM release envelope that could never sound.
Look at what those five have in common. Nothing crashed, nothing threw, the feature demoed fine. The output was quietly wrong in a way you would have shipped and a user would have found for you. Every one was caught by a review or a test before that happened.
The context that wrote the code cannot catch these. It knows what it meant, so it reads its own diff and sees its intention instead of its output, and it grades that intention generously every single time. A reviewer that did not write the code has no intention to protect. Hand it the spec section, the diff, and an instruction to verify by running things rather than by reading them, and it will go and check. That last instruction is the one people leave out, and it is the one that turns a review from an opinion into a result.
# Review: 16-bit chips (SNES voices, Genesis FM)
You did not write this code. Do not assume it works. Your job is not to be
agreeable, and it is not to find something either. If nothing is wrong, say
so and show the commands you ran to get there.
## What to read
1. `SPEC.md`, the domain model and the export sections. The spec is the
standard. Code that disagrees with it is wrong even when it is good code.
2. The diff: `git diff main..feat/16-bit`, files listed in the plan.
3. The implementation summary below. Those are claims, not facts.
## Claims to check
- Claim 1: SPC pitch conversion covers the full note range the SNES profile
advertises in `chip-profiles.ts`.
- Claim 2: the four-operator FM engine implements the YM2612 algorithms,
including release.
- Claim 3: 16-bit output sits at the same level as the 8-bit chips, with no
clipping on a full-scale song.
- Claim 4: hard-panned channels survive the offline render into the WAV.
## How to verify
Verify each claim by running something. Not by reading the code and agreeing
with it. Running.
- Quote the exact command and its real output. Truncate long output, do not
paraphrase it.
- End every claim with exactly one of: VERIFIED, COULD NOT VERIFY, FALSE.
- "Looks correct" is not a verdict.
For this diff specifically:
- Compile a fixture song for SNES and count the distinct notes that go in
against the distinct pitches that come out. If the counts differ, find
every note that disappeared and say what happened to it.
- Render the same fixture on NES and on SNES and compare peak sample values.
A 16-bit chip that is louder than the 8-bit ones is a gain bug, not a
feature of the chip.
- Render a song with one channel panned hard left and one hard right, write
the WAV, and read both channels back. Assert both carry signal.
- Trigger an FM note and hold it, then release it, and assert the envelope
actually reaches zero. An envelope that mathematically cannot complete
will pass every test that only checks attack.
## Look specifically for
- **Data that gets dropped silently.** Notes filtered out, octaves folded or
clamped without a warning, a channel lost in a mapping, an early return on
a case that really happens. Count what goes in and count what comes out.
- **Numbers that do not match the spec.** Check every constant in the diff
against `SPEC.md`, one at a time. Not "the math looks right".
- **Gain staging.** Anything that multiplies a sample. Find every place a
gain is applied and check no signal path applies two of them.
- **Envelopes and boundaries.** First frame, last frame, a one-frame note,
the longest release the spec allows, and one past it.
- **Tests that pass without testing.** Snapshots regenerated to match the
new output, assertions against a mock, a test that would still pass if the
implementation were deleted. Try deleting it and see.
## Report format
Per claim: verdict, command run, real output, and if it is not verified,
what would make it verifiable.
Then a defect list, worst first. Each defect gets the file, what is wrong,
what the spec says instead, and why a user would care.
Do not summarize the feature back to me. I know what it is supposed to do.
Tell me what it does.Note how much of that prompt is 8BEETY-shaped: it names the fixture comparisons, the gain check, the pan check, the envelope check. Those exist because those are the bugs this project turned out to have. The blank version, with the generic checklist and notes on running the reviewer as a separate context, is Fresh-context review prompt. Yours should start generic and grow the specific checks as your project teaches you what it gets wrong.
Tests are how the agent proves it
Nearly 300 tests, written test-first for anything with behavior worth pinning: register-math round-trips, LFSR bit sequences, drum priority, deterministic sample generation, layout invariants for the drawn graphics, and end-to-end smoke tests that compile a song for every chip and render two seconds of non-silent, in-range audio.
The count is the least interesting part of that. Two habits matter more.
"Done" means the verification commands ran and passed. Not that the agent said it is done. An agent reporting "all tests pass" is making a claim about a run you did not see, and the claim is free to make. The convention lives in CLAUDE.md rather than in your memory, which is why it survived past the point where you would have stopped typing it.
Design for testability up front. The pure compiler and the Node-runnable DSP core were spec decisions, and they were made because they make the agent's work checkable. That is the part worth stealing. Testable architecture is a prompt-engineering choice before it is an engineering one, because an architecture you cannot test is an architecture where every claim the agent makes has to be taken on faith.
Make it show its evidence
When it says something works, ask for the run. When it debugs, make it reproduce first, instrument second, and fix only what the evidence names.
"Video exports are glitchy" is exactly the kind of report that invites a confident guess, and a guess would have been cheap and wrong. What actually resolved it: measure browser timer throttling with a probe page, reproduce the codec error in a harness, decode the produced files with ffprobe and AVFoundation, and reject the obvious fix (avc3) because the evidence showed QuickTime could not play it. The fix that shipped was the one the measurements pointed at.
Your job is taste, constraints, and rulings
The agent wrote nearly all of the code. Here is the list of what the human did.
Decided what the product is. Decided what it must never do: no Nintendo or Sega trade dress, no servers, and no bundled copyrighted music, which had to be declined more than once. Decided which trade-offs to take, like giving the drawn Game Boy a screen bigger than the real one, because on a phone legibility beats fidelity. Decided when good was good enough, which is a judgment no test suite makes for you.
Every judgment call the agent made on the human's behalf got surfaced in a written list of rulings, for veto. That list is the mechanism. An agent that makes fifty small decisions silently has redesigned your product by Friday, and you will not be able to name the day it happened. An agent that writes them down has given you fifty one-line decisions to make in ten minutes.
The division of labor, in one line: you own intent and acceptance, the agent owns execution, the spec and the tests own the truth.