# AGENTS.md: building OpenGroove firmware with a coding agent

<!-- In the OpenGroove monorepo, testing and the agent loop: ../AGENTS.md (scripts/og-check fast | full | e2e). -->

This is the one authoritative recipe. It is pinned to these versions:

| | Version |
|---|---|
| Firmware ABI | **0.1** (`include/opengroove.h`, `OG_ABI_VERSION` = 0x00000001) |
| Toolkit | **1.0.0** (`toolkit/include/og/*.h`, `OG_TK_VERSION`) |
| Compiler | Emscripten SDK **3.1.61** (clang 19 with `wasm-ld` and `llvm-ar`); `og doctor` shows which one is used |
| Node | 20 or newer |

A firmware is a C program compiled to one wasm32 module that runs on a virtual
groovebox: pads, knobs, buttons, a screen and stereo audio, and on hardware without
some of them. You build and check it headlessly. No browser is needed.

## Recipe

From the SDK root, run `npm install` once. Then use `og`, which is
`node cli/bin/og.mjs` (or `npm link` it):

```sh
og new my-synth --template blank        # or: drums, mono-synth, effect   (start here, always)
og build my-synth                       # -> my-synth/build/my-synth.wasm (toolkit linked in)
og verify my-synth/build/my-synth.wasm --profile all
og render my-synth/build/my-synth.wasm --profile generic --png g.png --out g.wav
og render my-synth/build/my-synth.wasm --profile fire --png f.png --out f.wav
og render my-synth/build/my-synth.wasm --profile rotary --png r.png --out r.wav
og dev my-synth                         # browser preview with live reload (for the human)
```

The loop:

1. Edit the template.
2. Run `og build` and fix every error and warning.
3. Run `og verify --profile all`. It must print `"ok": true`. If not, read
   `profiles.<p>.errors`.
4. Run `og render --png` on a large screen (generic), the 128×64 one (fire) and the
   128×32 one (rotary), and **look at the PNGs**.
5. Check the `[fw] reach …` line that `og render` prints. Repeat from step 1.

`og verify` checks:
- that the firmware boots;
- 10 s of fuzzed input with no trap and no NaN;
- that pads produce the outcomes declared in `og.json` (`"outcomes"`, default `["audio"]`);
- real-time headroom;
- output levels;
- a byte-identical state round trip and transactional loads;
- sane parameters.

`--profile all` runs ten device profiles: three with skins (generic, kick, fire) and
seven synthetic ones (fm1like, rotary, touchonly, keys, mixed, padsonly, headless).
The kick profile is the real KICK-class panel ([docs/KICK-PROFILE.md](docs/KICK-PROFILE.md)):
no arrow keys (use the jog), `-`/`+` are LEFT/RIGHT, and most of its 32 buttons are
panel buttons with ids 16–34. Switch on button ids; ignore ids you don't use.

## How a firmware is put together (follow the templates)

- **Memory:** `og_arena` in `og_init` for anything sized by the device, such as the
  framebuffer (`og_gfx_init_device`). Everything else is `static`. There is no libc:
  no `malloc`, `printf`, `sinf` or `memcpy`.
- **Input:** `og_event` and `og_midi_in` only call `og_input_push` /
  `og_input_push_midi`. Everything is applied in `og_process` at its sample offset.
  Never act inside the callbacks.
- **`og_process`:**
  1. `og_input_begin(ctx.sample_pos)` and `og_seq_begin(&ctx, frames)`.
  2. Split the block at each intent and sequencer event (`og_input_next`,
     `og_seq_next`).
  3. Render audio between the splits.
  4. `og_input_end(frames)`.
- **Parameters:** declare an `og_param_info` table (stable ids, plain units, `step`,
  `curve`, enumeration `labels`), call `og_params_init`, and use
  `OG_PARAMS_EXPORTS(params)`.
  - Every change goes through `og_params_apply`: UI actions, `OG_I_PARAM`
    automation, knob slots, restore.
  - Recompute derived coefficients in the `on_change` callback.
- **UI:**
  - Describe views with `og_view` and `og_item`: menus, virtualized lists, a step
    grid. Feed every intent to `og_ui_handle` and apply the `og_action` it returns.
  - After each block, call `og_input_set_context(og_ui_context(&ui))` and
    `og_input_set_pad_mode(og_ui_pad_mode(&ui))`.
  - Draw with `og_ui_draw` in `og_render`, only when `og_ui_take_dirty` returns true
    or your own visuals changed.
  - `og_render` must never change musical state.
- **Controls:** export `og_controls_ptr` (`static og_controls c = OG_CONTROLS_INIT;`)
  and pass the host's descriptors, `og_bindings_for_profile(dev.profile)` and
  `og_firmware_bindings` to `og_input_init`. To change mappings, add a
  `bindings.json` next to `og.json` (docs/TOOLKIT.md, "Bindings").
- **Reachability:** log `reach ok` / `reach missing: …` from
  `og_ui_unreachable(&ui, og_input_reachable(&in))` in `og_init`, as the templates do.
- **Sequencing:** `og_seq` (free-running, or following the host with
  `OG_CTX_SYNC`/`COUNT_IN`). Envelopes come from `og_env`, DSP from `og/dsp.h`, and
  4-voice SIMD blocks from `og/vec.h`.
- **State:** `og_state_*` writes fields one by one under a FOURCC tag and schema 1.
  - Load into temporaries and commit only when `og_state_close` returns `OG_OK`.
  - Save the document (params, patterns), never the playhead or voices.

## Effects and racks

- **Effects.** Start from `--template effect` (`og_kind` returns `OG_KIND_EFFECT`;
  stereo in, stereo out).
  - Declare `og_latency` exactly: verify measures it.
  - Declare `og_tail_frames`, and flush the tail to exact zeros.
  - `og_reset` must silence the effect.
  - Set `"outcomes": ["none"]` in og.json.
- **Racks.** Declare `OG_CAP_RACK` and export `og_instance_ptr`
  (`static og_instance_info inst = OG_INSTANCE_INIT;`), or the firmware can never be
  part of a rack. Check a rack with `og verify --rack x.rack.json` (format in
  docs/ABI.md §17); the output must be bit-exact for any thread count.

## Rules (verify enforces most of them)

1. Export everything in "Required exports" of `opengroove.h`. `og_init` returns 0.
2. Adapt to `og_device`: pads, knobs, the button bitmask, the display size and format
   (it can be 0×0), the sample rate and `max_block`. The audio stride is always
   `og_device.max_block`. Read the device into a private copy; never write the shared
   struct.
3. **Never trap.** Clamp every index from an event, never divide by zero, and handle
   knob deltas of ±50 and fractions.
4. **No NaN or Inf, and keep levels sane:** peak at most 1.0, pad RMS at most −6 dBFS,
   no DC. Soft-clip and DC-block the master.
5. A pad press (or a MIDI note on a device without pads) must produce your declared
   outcome immediately, even with the transport stopped.
6. PLAY starts something musical out of the box: ship a default pattern.
7. **Real time while activated, in every callback:**
   - No blocking, no allocation, no `memory.grow`, no `og_asset_*`.
   - `og_process` takes less than half the block duration.
   - `og_render` takes under 2 ms and draws only when something changed.
8. Events, `OG_PARAM_CHANGE` included, take effect at their sample offset, including
   recomputed coefficients. An offset-0 change must sound bit-identical to
   `og_param_set` just before the block.
9. State envelopes are magic, `fw_tag`, `schema` and size. Return `OG_ERR_FORMAT` or
   `OG_ERR_VERSION` for foreign or unknown blobs. Loads are transactional.
10. Every source file starts with `// SPDX-License-Identifier: Apache-2.0`.

## Debugging

- `og_log("text", len)` prints `[fw] text` in `og render` and to the log panel in
  `og dev`.
- A trap names its function (`firmware trapped in og_process: unreachable`). Build
  with `og build --debug` and replay it with `og render --script events.json` (see
  docs/ABI.md §14) for a deterministic run.
- `og verify` prints per-profile `metrics` (peaks, RMS, real-time factors, render
  time) and `errors`.
- If something only fails on one profile, run
  `og render --profile <p> --png x.png` and look at the screen.

## Where things are

| Path | What |
|---|---|
| `include/opengroove.h` | the ABI (normative); `docs/ABI.md` explains it |
| `toolkit/` | the library; `docs/TOOLKIT.md` is its reference, `docs/TOOLKIT-SIZES.md` its size measurements |
| `cli/templates/` | `blank`, `drums`, `mono-synth`: complete, verified toolkit firmwares |
| `examples/` | larger demos written without the toolkit (pocket-drums, acid-line, drift-arp) |
| `runtime-core/`, `runtime-node/`, `runtime-web/` | the hosts |
| `tests/` | the Node tests (`npm test`), the toolkit CI matrix, compiled doc snippets, browser smoke |
