# Sidequest Setup

> 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.

- Skill: `baconpaul/sidequest-setup` (Agent Skill)
- Install (CLI): `npx skillmds@latest add baconpaul/sidequest-setup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/baconpaul/sidequest-setup/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: baconpaul (https://skillmd.com/u/baconpaul)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/baconpaul/sidequest-setup

---


# 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:

  ```bash
  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`):

```bash
#!/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.

```bash
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.

