SideQuest Plugin Shell Setup
Clone a working clap-first plugin (submodules + cmake + installer + CI + the
engine/UI architecture) into a fresh git init'd directory, then strip it to a minimal,
named, parameterized shell the user can code against. The SideQuest plugins are sibling
checkouts of each other, so paths below like ../two-filters assume the new directory
sits alongside them.
The shell is always patch/patchMain
Every new plugin comes out of this skill on the engine-owned patch / patchMain
model. There is no longer a legacy variant to produce. That means, in the finished shell:
- The engine owns
Patch patch (audio thread) and Patch patchMain (main thread).
- The editor holds
Patch &patchMainRef bound to engine->patchMain — never an owned copy.
- Every CLAP main-thread call —
stateSave, stateLoad, paramsValue, paramsInfo,
paramsValueToText, paramsTextToValue, and paramsFlush when inactive — reads
patchMain. The one exception is the param cookie, which must keep pointing into
patch.
stateSave does not quiesce the engine. There is no SEND_PREP_FOR_STREAM, no
readyForStream spin-wait, no EDITOR_ATTACH_DETATCH, no pushFullUIRefresh, no
sneakyStartupGrabFrom.
The patch-to-main skill is the full statement of this design, its invariants and its
pitfalls. Read it before §5 of this document — it is the specification; this skill
just clones something that already satisfies it.
0. Gather inputs (ask if not given)
Source plugin to clone. It must already be on patch/patchMain. Pick the
closest in shape to the target from among the migrated siblings — ../two-filters
(stereo effect, the reference port), ../freaqy-verb (effect), ../six-sines
(polyphonic instrument, the richest).
Confirm before cloning rather than trusting a list that will age:
grep -rl patchMain ../<source>/src | head -1 # must hit
grep -rl patchCopy ../<source>/src | head -1 # must be empty
Several older siblings are still on the legacy patchCopy model. If the user names one
of those, say so and offer the choice: clone a migrated sibling instead, or clone theirs
and run the patch-to-main skill as step 5.5, before stripping anything. Do not
produce a legacy shell and leave it for later — the strip in §5 assumes the new model.
Product name (e.g. "Gain Changer"), namespace (baconpaul::gainchanger),
dir/target name (gain-changer), patch extension (.gnch, ~4 chars),
AU subtype (4 chars, e.g. gnCh), bundle/clap id
(org.baconpaul.gainchanger).
Parameters: name, type (float/bool/int), range, default, and what each does
in the DSP.
Whether presets start empty or carry the source's factory set.
1. Read the source thoroughly first
Read the source's CMakeLists.txt, libs/CMakeLists.txt, cmake/*, src/**
(engine, patch, clap, presets, ui), tests/*, .gitmodules, .github/*,
resources/*. Identify the minimal kept core vs. the strip list (see §5). Note
which sst-jucegui widgets the source uses (Knob, ToggleButton, VUMeter,
JogUpDownButton, NamedPanel).
2. Reconstitute submodules by LOCAL clone (fast, no big downloads)
The source already has every submodule checked out at a pinned SHA. Local-clone
from its working tree (same filesystem ⇒ hardlinked objects) instead of cloning
upstream, then point origins back at upstream. Handle nested submodules
(sst-plugininfra has ghc-filesystem + miniz) with a local-url-override
submodule update --init. Write and run a script like this (fill TF/GC and the
path↔url list from the source's .gitmodules):
#!/bin/bash
set -euo pipefail
TF=<source-abs> ; GC=<new-abs>
GIT="git -c protocol.file.allow=always"
cd "$GC"
# path<TAB>upstream-url per top-level submodule (copy from source/.gitmodules)
while IFS=$'\t' read -r path url; do
[ -z "$path" ] && continue
sha=$(git -C "$TF" rev-parse "HEAD:$path")
rm -rf "$GC/$path"
$GIT clone --quiet "$TF/$path" "$GC/$path"
git -C "$GC/$path" checkout --quiet --detach "$sha"
git -C "$GC/$path" remote set-url origin "$url"
done < subs.tsv
# nested subs inside sst-plugininfra, sourced locally then synced to upstream
PI="$GC/libs/sst/sst-plugininfra" ; TFPI="$TF/libs/sst/sst-plugininfra"
git -C "$PI" -c protocol.file.allow=always \
-c submodule.libs/filesystem/filesystem.url="$TFPI/libs/filesystem/ghc-filesystem" \
-c submodule.libs/miniz.url="$TFPI/libs/miniz" submodule update --init
git -C "$PI" submodule sync ; git -C "$PI" submodule init
cp "$TF/.gitmodules" "$GC/.gitmodules"
git -C "$GC" add .gitmodules
while IFS=$'\t' read -r path url; do [ -z "$path" ] && continue; git -C "$GC" add "$path"; done < subs.tsv
$GIT -C "$GC" submodule absorbgitdirs
git -C "$GC" submodule init
git -C "$GC" submodule status
Verify a couple of working trees are populated (e.g. libs/JUCE/CMakeLists.txt,
libs/sst/sst-plugininfra/libs/miniz/). Top-level status should show a space
prefix (checked out), not -.
3. Copy/adapt build + resources
- Verbatim:
cmake/CmakeRC.cmake, cmake/compile-options.cmake,
libs/CMakeLists.txt, .clang-format, .gitignore, resources/LICENSE_GPL3,
resources/SideQuest*, resources/installer_mac/{License.txt,entitlements.plist,icns.rsrc,SideQuestIcon.icns}.
- Renamed copies (placeholders): icons
XIcon.{png,ico,icns} →
<New>Icon.{png,ico,icns} (and installer_mac/<New>Icon.icns). These are fake
until real art exists — list in TODO_PORT.md.
- Adapt: top
CMakeLists.txt (project/product/cmrc namespaces/patch glob/
impl source list/link libs/bundle id/standalone id/installer prefix+icons),
cmake/basic_installer_clapfirst.cmake (icon path + fresh Inno GUID),
.github/workflows/build-plugin.yml (TARGET_NAME/PLUGIN_NAME/titles),
resources/{ReadmeZip.txt,NightlyBlurb.md}, doc/ack.md, README.md,
LICENSE.md. Keep an empty resources/factory_patches/ (cmrc handles zero
files; the GLOB just returns empty).
4. Namespace / id remap (apply everywhere)
baconpaul::<old> → baconpaul::<new>; product string; patch ext; cmrc
namespaces <old>_patches/<old>_assets → <new>_*; bundle/clap id; AU
subtype; include-guard macros BACONPAUL_<OLD>_* → BACONPAUL_<NEW>_*; file
header comment block. Patch::id may keep a hyphen (org.baconpaul.gain-changer)
while clap/bundle id has none — mirror the source's convention.
5. Strip the engine + UI, keep the plumbing
Keep — the patchMain plumbing, in full. This is the part that must survive the strip
intact, because it is the architecture:
- Both patches on the engine, plus
Patch::copyValuesFrom (a value copy — never
operator=, which would alias params/paramMap across objects).
- The two ring buffers (
audioToMain / mainToAudio) and processUIQueue.
static bool handleAudioToMainMessage(Patch &dest, const AudioToMainMsg &) and
drainAudioToMainInto(Patch &dest) — static so the editor, holding no engine, can share
the same handler as the headless drain.
static sendEntirePatchToAudio(...) — the load funnel, on the engine, not the preset
manager.
paramsFlushMainThread and clapCookieFor.
editorActive, mainThreadDrainRequested, uiForceRebuild atomics, and the editor's
rebuildFromPatchMain().
REQUEST_NON_PATCH_STATE → SEND_SAMPLE_RATE. Keep this idiom even when the sample rate
is its only rider: it is the channel for engine-owned, non-patch UI state.
Param + Lag smoothing + paramLagSet + lagHandler + snapAllParams, preset/state
save-load, preset-manager, patch-data-bindings.h (PatchContinuous/PatchDiscrete +
createComponent), preset-data-binding.h, ui-defaults.h, about-screen, plugin-editor
skeleton, preset jog button + VU meter, and the clap entry/factory/descriptor +
audio-port + params + state glue in plugin-clap.cpp.
Never carry across, even if the source somehow still has them: pushFullUIRefresh,
doFullRefresh, prepForStream / readyForStream / SEND_PREP_FOR_STREAM,
EDITOR_ATTACH_DETATCH, sneakyStartupGrabFrom, a hand-rolled engineActivated flag
(isActive() answers it), the SEND_REQUEST_RESCAN / DO_PARAM_RESCAN rescan bounce, and
the patch-name / dirty echo-and-forward messages. If the source has any of these it is not
migrated — go back to §0.
Strip: everything domain-specific — extra DSP, its panels, and any
message-enum members only those used (e.g. drop SET_FILTER_MODEL,
UPDATE_LFOSTEP, SEND_FILTER_CONFIG). Replace the templated process() with a
plain per-sample loop calling engine->processAudio(...). Reduce the engine to
the chosen params; smooth continuous gains with a lipol<float, blockSize, true>
(the true snaps on first value — no startup ramp). Fold related controls into
one lipol where it avoids clicks (e.g. gain×polarity ramps through zero).
patch.h: a single node struct holding the params (each via
floatMd()/boolMd() builders with .withName/.withGroupName/.withID/.withDefault),
pushed in the Patch ctor. Param must keep isTemposynced() (the
patch_support::ValidPar concept requires it). Provide no-op
migrateParamValueFromVersion / migratePatchFromVersion.
UI: one MainPanel : NamedPanel wiring createComponent(...) for each param
(Knob+PatchContinuous, ToggleButton+PatchDiscrete with DrawMode::LABELED).
Knobs draw their own label from the data source. Pick a compact edWidth/edHeight.
6. Commit, configure, build, fix the known gotchas
- Make the initial commit BEFORE configuring — the version lib static-asserts
on
GIT_COMMIT_HASH, and a no-commit repo fails to build. Ask the user before
committing (their commit cadence is theirs); they'll usually say yes here since
the build needs it. Use an "Assisted-by:" trailer, 'what' not 'how' message.
cmake -S . -B $B -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCOPY_AFTER_BUILD=FALSE
then cmake --build $B -j ($B is your configure dir). Build -impl first to validate all
sources compile/link, then the full set.
- Test target that includes
patch.h must link sst-plugininfra::patchbase
(defines SST_PLUGININFRA_PATCHBASE, which gates the whole patch_base.h) plus
sst-cpputils — otherwise sst::plugininfra::patch_support "expected namespace
name". The source's own test often dodges this by not including patch.h.
- Confirm artifacts in
$B/<target>_assets/ (.clap/.vst3/ .component/.app), nm -gU the clap for _clap_entry, run the test exe.
- A clang-format hook reformats C/C++ on Write — expect it.
6.5 Verify the shell is actually on patchMain
Run these in the new directory before declaring the port done. Each one is a grep for a
construct that must or must not exist; none should need judgement.
grep -rn 'patchMain' src | head # must hit: engine owns it
grep -rn 'patchCopy' src # must be EMPTY
grep -rn 'prepForStream\|readyForStream\|SEND_PREP_FOR_STREAM' src # must be EMPTY
grep -rn 'pushFullUIRefresh\|EDITOR_ATTACH_DETATCH\|sneakyStartupGrabFrom' src # must be EMPTY
grep -rn 'copyValuesFrom\|clapCookieFor\|drainAudioToMainInto\|paramsFlushMainThread' src # all must hit
grep -rn 'engine->patch\b' src/*/plugin-clap.cpp src/clap/*.cpp 2>/dev/null # only the cookie helper
The last one is the one that actually bites. The CLAP adapter must read patchMain
everywhere except clapCookieFor, and getting it wrong is silent: the host's automation
writes patchMain from the audio thread, the engine never hears it, and the audio-thread
lag set ends up ticking a main-thread param every block. Sweep every .cookie = and every
patchParamsInfo call site.
Then port the patchMain tests from the source (patch-to-main §Tests lists all six). At
minimum carry across the two that catch the silent failures:
copyValuesFrom value equality plus the pointer-identity guard — after the copy,
dst.paramMap[id] != src.paramMap[id].
- Cookie routing — a cookie from
clapCookieFor must equal patch.paramMap.at(pid) and
differ from patchMain.paramMap.at(pid); drive an event through it and assert patch
moved while patchMain did not.
7. Leave a port checklist
Write .claude/TODO_PORT.md with the placeholders/unverified items: real icons
(+ make_rsrc.sh regen), GitHub repo + remote + CI secrets, missing
doc/manual.md, AU-subtype/clap-id collision checks, Inno GUID, gain law /
defaults / plugin-feature category decisions, any uncommitted post-init fixes,
clap-validator + standalone smoke test. Record the porting recipe + open items in
project memory.
Do not list "migrate to patchMain" as an open item — a shell that needs that is not
finished. §6.5 is a gate, not a checklist entry.
1---2name: sidequest-setup3description: Clone a new SideQuest audio-plugin shell from an existing clap-first sibling. Sets up submodules via fast local clones, adapts cmake + installer + CI, remaps the namespace/ids, strips the DSP and UI to a chosen set of params, and gets all four plugin formats building on the engine-owned patch/patchMain architecture.4---56# SideQuest Plugin Shell Setup78Clone a working clap-first plugin (submodules + cmake + installer + CI + the9engine/UI architecture) into a fresh `git init`'d directory, then strip it to a minimal,10named, parameterized shell the user can code against. The SideQuest plugins are sibling11checkouts of each other, so paths below like `../two-filters` assume the new directory12sits alongside them.1314## The shell is always patch/patchMain1516**Every new plugin comes out of this skill on the engine-owned `patch` / `patchMain`17model. There is no longer a legacy variant to produce.** That means, in the finished shell:1819- The engine owns `Patch patch` (audio thread) **and** `Patch patchMain` (main thread).20- The editor holds `Patch &patchMainRef` bound to `engine->patchMain` — never an owned copy.21- Every CLAP main-thread call — `stateSave`, `stateLoad`, `paramsValue`, `paramsInfo`,22 `paramsValueToText`, `paramsTextToValue`, and `paramsFlush` when inactive — reads23 `patchMain`. The one exception is the param **cookie**, which must keep pointing into24 `patch`.25- `stateSave` does **not** quiesce the engine. There is no `SEND_PREP_FOR_STREAM`, no26 `readyForStream` spin-wait, no `EDITOR_ATTACH_DETATCH`, no `pushFullUIRefresh`, no27 `sneakyStartupGrabFrom`.2829The `patch-to-main` skill is the full statement of this design, its invariants and its30pitfalls. **Read it before §5 of this document** — it is the specification; this skill31just clones something that already satisfies it.3233## 0. Gather inputs (ask if not given)3435- **Source plugin** to clone. **It must already be on patch/patchMain.** Pick the36 closest in shape to the target from among the migrated siblings — `../two-filters`37 (stereo effect, the reference port), `../freaqy-verb` (effect), `../six-sines`38 (polyphonic instrument, the richest).3940 Confirm before cloning rather than trusting a list that will age:4142 ```bash43 grep -rl patchMain ../<source>/src | head -1 # must hit44 grep -rl patchCopy ../<source>/src | head -1 # must be empty45 ```4647 Several older siblings are still on the legacy `patchCopy` model. If the user names one48 of those, say so and offer the choice: clone a migrated sibling instead, or clone theirs49 and **run the `patch-to-main` skill as step 5.5, before stripping anything**. Do not50 produce a legacy shell and leave it for later — the strip in §5 assumes the new model.51- **Product name** (e.g. "Gain Changer"), **namespace** (`baconpaul::gainchanger`),52 **dir/target name** (`gain-changer`), **patch extension** (`.gnch`, ~4 chars),53 **AU subtype** (4 chars, e.g. `gnCh`), **bundle/clap id**54 (`org.baconpaul.gainchanger`).55- **Parameters**: name, type (float/bool/int), range, default, and what each does56 in the DSP.57- Whether presets start **empty** or carry the source's factory set.5859## 1. Read the source thoroughly first6061Read the source's `CMakeLists.txt`, `libs/CMakeLists.txt`, `cmake/*`, `src/**`62(engine, patch, clap, presets, ui), `tests/*`, `.gitmodules`, `.github/*`,63`resources/*`. Identify the minimal kept core vs. the strip list (see §5). Note64which sst-jucegui widgets the source uses (`Knob`, `ToggleButton`, `VUMeter`,65`JogUpDownButton`, `NamedPanel`).6667## 2. Reconstitute submodules by LOCAL clone (fast, no big downloads)6869The source already has every submodule checked out at a pinned SHA. Local-clone70from its working tree (same filesystem ⇒ hardlinked objects) instead of cloning71upstream, then point origins back at upstream. Handle nested submodules72(sst-plugininfra has ghc-filesystem + miniz) with a local-url-override73`submodule update --init`. Write and run a script like this (fill TF/GC and the74path↔url list from the source's `.gitmodules`):7576```bash77#!/bin/bash78set -euo pipefail79TF=<source-abs> ; GC=<new-abs>80GIT="git -c protocol.file.allow=always"81cd "$GC"82# path<TAB>upstream-url per top-level submodule (copy from source/.gitmodules)83while IFS=$'\t' read -r path url; do84 [ -z "$path" ] && continue85 sha=$(git -C "$TF" rev-parse "HEAD:$path")86 rm -rf "$GC/$path"87 $GIT clone --quiet "$TF/$path" "$GC/$path"88 git -C "$GC/$path" checkout --quiet --detach "$sha"89 git -C "$GC/$path" remote set-url origin "$url"90done < subs.tsv91# nested subs inside sst-plugininfra, sourced locally then synced to upstream92PI="$GC/libs/sst/sst-plugininfra" ; TFPI="$TF/libs/sst/sst-plugininfra"93git -C "$PI" -c protocol.file.allow=always \94 -c submodule.libs/filesystem/filesystem.url="$TFPI/libs/filesystem/ghc-filesystem" \95 -c submodule.libs/miniz.url="$TFPI/libs/miniz" submodule update --init96git -C "$PI" submodule sync ; git -C "$PI" submodule init97cp "$TF/.gitmodules" "$GC/.gitmodules"98git -C "$GC" add .gitmodules99while IFS=$'\t' read -r path url; do [ -z "$path" ] && continue; git -C "$GC" add "$path"; done < subs.tsv100$GIT -C "$GC" submodule absorbgitdirs101git -C "$GC" submodule init102git -C "$GC" submodule status103```104105Verify a couple of working trees are populated (e.g. `libs/JUCE/CMakeLists.txt`,106`libs/sst/sst-plugininfra/libs/miniz/`). Top-level status should show a space107prefix (checked out), not `-`.108109## 3. Copy/adapt build + resources110111- **Verbatim:** `cmake/CmakeRC.cmake`, `cmake/compile-options.cmake`,112 `libs/CMakeLists.txt`, `.clang-format`, `.gitignore`, `resources/LICENSE_GPL3`,113 `resources/SideQuest*`, `resources/installer_mac/{License.txt,entitlements.plist,icns.rsrc,SideQuestIcon.icns}`.114- **Renamed copies (placeholders):** icons `XIcon.{png,ico,icns}` →115 `<New>Icon.{png,ico,icns}` (and `installer_mac/<New>Icon.icns`). These are fake116 until real art exists — list in TODO_PORT.md.117- **Adapt:** top `CMakeLists.txt` (project/product/cmrc namespaces/patch glob/118 impl source list/link libs/bundle id/standalone id/installer prefix+icons),119 `cmake/basic_installer_clapfirst.cmake` (icon path + **fresh Inno GUID**),120 `.github/workflows/build-plugin.yml` (TARGET_NAME/PLUGIN_NAME/titles),121 `resources/{ReadmeZip.txt,NightlyBlurb.md}`, `doc/ack.md`, `README.md`,122 `LICENSE.md`. Keep an empty `resources/factory_patches/` (cmrc handles zero123 files; the GLOB just returns empty).124125## 4. Namespace / id remap (apply everywhere)126127`baconpaul::<old>` → `baconpaul::<new>`; product string; patch ext; cmrc128namespaces `<old>_patches`/`<old>_assets` → `<new>_*`; bundle/clap id; AU129subtype; include-guard macros `BACONPAUL_<OLD>_*` → `BACONPAUL_<NEW>_*`; file130header comment block. Patch::id may keep a hyphen (`org.baconpaul.gain-changer`)131while clap/bundle id has none — mirror the source's convention.132133## 5. Strip the engine + UI, keep the plumbing134135**Keep — the patchMain plumbing, in full.** This is the part that must survive the strip136intact, because it is the architecture:137138- Both patches on the engine, plus `Patch::copyValuesFrom` (a *value* copy — never139 `operator=`, which would alias `params`/`paramMap` across objects).140- The two ring buffers (`audioToMain` / `mainToAudio`) and `processUIQueue`.141- `static bool handleAudioToMainMessage(Patch &dest, const AudioToMainMsg &)` and142 `drainAudioToMainInto(Patch &dest)` — static so the editor, holding no engine, can share143 the same handler as the headless drain.144- `static sendEntirePatchToAudio(...)` — the load funnel, on the engine, not the preset145 manager.146- `paramsFlushMainThread` and `clapCookieFor`.147- `editorActive`, `mainThreadDrainRequested`, `uiForceRebuild` atomics, and the editor's148 `rebuildFromPatchMain()`.149- `REQUEST_NON_PATCH_STATE` → `SEND_SAMPLE_RATE`. Keep this idiom even when the sample rate150 is its only rider: it is the channel for engine-owned, non-patch UI state.151- `Param` + Lag smoothing + `paramLagSet` + `lagHandler` + `snapAllParams`, preset/state152 save-load, `preset-manager`, `patch-data-bindings.h` (`PatchContinuous`/`PatchDiscrete` +153 `createComponent`), `preset-data-binding.h`, `ui-defaults.h`, about-screen, plugin-editor154 skeleton, preset jog button + VU meter, and the clap entry/factory/descriptor +155 audio-port + params + state glue in `plugin-clap.cpp`.156157**Never carry across**, even if the source somehow still has them: `pushFullUIRefresh`,158`doFullRefresh`, `prepForStream` / `readyForStream` / `SEND_PREP_FOR_STREAM`,159`EDITOR_ATTACH_DETATCH`, `sneakyStartupGrabFrom`, a hand-rolled `engineActivated` flag160(`isActive()` answers it), the `SEND_REQUEST_RESCAN` / `DO_PARAM_RESCAN` rescan bounce, and161the patch-name / dirty echo-and-forward messages. If the source has any of these it is not162migrated — go back to §0.163164**Strip:** everything domain-specific — extra DSP, its panels, and any165message-enum members only those used (e.g. drop `SET_FILTER_MODEL`,166`UPDATE_LFOSTEP`, `SEND_FILTER_CONFIG`). Replace the templated `process()` with a167plain per-sample loop calling `engine->processAudio(...)`. Reduce the engine to168the chosen params; smooth continuous gains with a `lipol<float, blockSize, true>`169(the `true` snaps on first value — no startup ramp). Fold related controls into170one lipol where it avoids clicks (e.g. gain×polarity ramps through zero).171172`patch.h`: a single node struct holding the params (each via173`floatMd()/boolMd()` builders with `.withName/.withGroupName/.withID/.withDefault`),174pushed in the `Patch` ctor. `Param` **must** keep `isTemposynced()` (the175`patch_support::ValidPar` concept requires it). Provide no-op176`migrateParamValueFromVersion` / `migratePatchFromVersion`.177178UI: one `MainPanel : NamedPanel` wiring `createComponent(...)` for each param179(Knob+PatchContinuous, ToggleButton+PatchDiscrete with `DrawMode::LABELED`).180Knobs draw their own label from the data source. Pick a compact `edWidth/edHeight`.181182## 6. Commit, configure, build, fix the known gotchas183184- **Make the initial commit BEFORE configuring** — the version lib static-asserts185 on `GIT_COMMIT_HASH`, and a no-commit repo fails to build. Ask the user before186 committing (their commit cadence is theirs); they'll usually say yes here since187 the build needs it. Use an "Assisted-by:" trailer, 'what' not 'how' message.188- `cmake -S . -B $B -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCOPY_AFTER_BUILD=FALSE`189 then `cmake --build $B -j` (`$B` is your configure dir). Build `-impl` first to validate all190 sources compile/link, then the full set.191- **Test target that includes `patch.h`** must link `sst-plugininfra::patchbase`192 (defines `SST_PLUGININFRA_PATCHBASE`, which gates the whole `patch_base.h`) plus193 `sst-cpputils` — otherwise `sst::plugininfra::patch_support` "expected namespace194 name". The source's own test often dodges this by not including patch.h.195- Confirm artifacts in `$B/<target>_assets/` (`.clap/.vst3/196 .component/.app`), `nm -gU` the clap for `_clap_entry`, run the test exe.197- A clang-format hook reformats C/C++ on Write — expect it.198199## 6.5 Verify the shell is actually on patchMain200201Run these in the new directory before declaring the port done. Each one is a grep for a202construct that must or must not exist; none should need judgement.203204```bash205grep -rn 'patchMain' src | head # must hit: engine owns it206grep -rn 'patchCopy' src # must be EMPTY207grep -rn 'prepForStream\|readyForStream\|SEND_PREP_FOR_STREAM' src # must be EMPTY208grep -rn 'pushFullUIRefresh\|EDITOR_ATTACH_DETATCH\|sneakyStartupGrabFrom' src # must be EMPTY209grep -rn 'copyValuesFrom\|clapCookieFor\|drainAudioToMainInto\|paramsFlushMainThread' src # all must hit210grep -rn 'engine->patch\b' src/*/plugin-clap.cpp src/clap/*.cpp 2>/dev/null # only the cookie helper211```212213The last one is the one that actually bites. **The CLAP adapter must read `patchMain`214everywhere except `clapCookieFor`**, and getting it wrong is silent: the host's automation215writes `patchMain` from the audio thread, the engine never hears it, and the audio-thread216lag set ends up ticking a main-thread param every block. Sweep every `.cookie =` and every217`patchParamsInfo` call site.218219Then port the patchMain tests from the source (`patch-to-main` §Tests lists all six). At220minimum carry across the two that catch the silent failures:221222- `copyValuesFrom` value equality **plus** the pointer-identity guard — after the copy,223 `dst.paramMap[id] != src.paramMap[id]`.224- Cookie routing — a cookie from `clapCookieFor` must equal `patch.paramMap.at(pid)` and225 differ from `patchMain.paramMap.at(pid)`; drive an event through it and assert `patch`226 moved while `patchMain` did not.227228## 7. Leave a port checklist229230Write `.claude/TODO_PORT.md` with the placeholders/unverified items: real icons231(+ `make_rsrc.sh` regen), GitHub repo + remote + CI secrets, missing232`doc/manual.md`, AU-subtype/clap-id collision checks, Inno GUID, gain law /233defaults / plugin-feature category decisions, any uncommitted post-init fixes,234clap-validator + standalone smoke test. Record the porting recipe + open items in235project memory.236237Do **not** list "migrate to patchMain" as an open item — a shell that needs that is not238finished. §6.5 is a gate, not a checklist entry.