# TASKS_DUMP — ParVagues / L'Armada board reconstruction

**Written 2026-08-16.** The Claude-Code Task API is not reachable from this session
(`TaskList`/`TaskCreate`/`TaskUpdate`/`TaskGet` all absent), so the live board — last seen at
112 tasks — could not be read or rewritten programmatically. This file rebuilds the tree from
durable sources instead, and is meant to be the thing you edit or re-import from.

**Provenance of every id below**

| source | trust |
|---|---|
| `armada/tasks/board-archive.md` | verbatim, ids exact (completed through 2026-07-29) |
| `armada/tasks/NNN-*.md` (001–025) + dated logs | verbatim, narrative achievement logs |
| memory store + this session's context | ids **recalled**, not verified — treat as labels |
| git log | authoritative for what actually shipped |

Ids in the #150+ range come from the lost board via session context. They are almost
certainly right but I cannot re-read them, so **treat a #17x number as a name, not a key**.

---

## EPIC A — OPAL-26 release  ⟵ *the only epic blocking a shipment*

North star: get "Sunset Forest" (opal-festival-2026, 2026-08-08) released. 14 tracks —
**#14 Desire is cut**, PLN confirmed 2026-08-16; #13 Vague de CRIME becomes the last official
track, #15 REVOLUTION stays the Encore.

### A1 · #177 — Re-cut v4 from ear-verified boundaries  ✅ **DONE — SIGNED OFF BY EAR 2026-08-16**

Ground truth: `armada/tide-table/judge_specs/opal26_boundaries_ear.json` — **do not overwrite
from a detector.** Nine boundaries verified by ear (#3 893.0 · #4 1118.4 · #5 1549.4 ·
#6 1808.1 · #7 2248.8 · #8 2726.4 (alt 2730.9) · #9 2954.4 already correct · #10 3269.0).

- [x] **#1 leading-silence measured** — 1.452 s of *literal digital silence* (all samples zero
      to sample 64038; first transient −28 dBFS). Objective, no ear needed.
- [x] **A1.1 #1 head trim recorded** — `start: 1.45` in the ear-boundary file, with the
      sample-exact measurement stored alongside for audit (`d3dbe59`). Kept as *data*, not
      baked into the rendered flac, because A1.4 regenerates the tracks from this file.
- [x] **A1.2 Settle #11, #12** — done in the boundary lab. #11 `takeover` 3557.33 (machine
      +0.13 s); #12 `mid`, then moved **1.66 s later** to 3681.76 when heard as a seam rather
      than as a cut. Two adjacent cuts, opposite verdicts on the same estimator — **there is no
      universal fraction**; offer candidates, never average.
- [x] **A1.3 Settle #13, #15** — ear-only as predicted, no candidate was ever possible.
      #13 3887.25 · #15 4547.57 · END of #13 at 4166.30 (he stops on the sound's decay,
      13.7 s before the nominal bar end).
- [x] **A1.4 DONE** — `apply_boundaries.py` → `segments_v4.json` → `render_release.py`
      (split · verify · continuous). 14 tracks × streaming+club, durations exact, 48 kHz/24-bit.
      Continuous mixes re-rendered drop-free.
- [x] **A1.5 Seam pass** — release check at `bounds.html?set=<gig>-release`; PLN's five fixes
      applied as `edits` (trims/fades), each verified by gain-ratio against the unfiltered master.
- [x] **A1.6 Close reverb** — A/B rendered (dry/subtle/more). PLN picked **subtle** from the
      isolated clip, then **revised to `more`** hearing it as the record's ending (`271e07c`).
      An effect judged on a clip ≠ the same effect judged as an ending — render the A/B
      *in place* next time. Ships as `reverb_tail_s: 5.0` + `reverb_preset: more` (650 ms top
      tap), ending on a decay with **zero** trailing silence.
- [x] **A1.7 PLN CONFIRM ✅** — 2026-08-16, on the rendered files: *"listened to 13... perfect
      end for crime. revolution starts good. listened to 99... all good perfect ending!"*
      Recorded as `release_signoff` in the ear-boundary file and in `performance_notes.md`.
      Also produced one late call: a **2 s rest** appended inside CRIME (`pad_end_s`, a third
      kind of edit — it ADDS time; a rest belongs to the outgoing track so it ships in the
      file AND in the mix). **The last gate is now CLEARED.**

**RENDERED, SIGNED OFF, READY TO UPLOAD** (2026-08-16): 14 tracks × streaming + club, durations
exact, 48 kHz/24-bit; continuous drop-free mixes `*_v4_{streaming,club}_nodrop.flac` at
**4361.35 s = 72.7 min**. Three gaps = the trims: 1.00 s (silence), 0.04 s (WAP burst),
410.17 s (Desire + CRIME/REV trims); plus one deliberate 2.00 s rest after #13.

⚠️ **The signoff is against `segments_v4.json` as rendered on 2026-08-16.** Any later re-render
invalidates it: re-run `render_release` + `build_release_joins` and get the ear again before
uploading.

### A2 · #183 — REVOLUTION mix fix  · blocked by A1

- [ ] Cut **d5 `the_revolution`**, *not* d10. PLN's first guess was wrong: measured
      band-limited (400 Hz–8 k, what a laptop reproduces) d5 leads d10 by **13.7 dB**;
      cutting d10 changes nothing he can hear. See memory `reference_small_speaker_and_rights`.
- [ ] Root-cause **d9 SUGAR CHOIR = digital silence** (−240 dB) while declared in the score.
      Two independent measurements agree. This is the finding that motivated D2's "shout until played".
- [ ] Measure the bass-"saturation" claim separately — it is a different question from loudness.

### A3 · #179 — SoundCloud uploader  ✅ **WORKING — 8 of 15 uploaded, all private**

**The 400 never needed solving.** `POST /tracks` was the wrong door: SoundCloud renders a
working upload form, and the transport's premise was always "be a real browser, don't
imitate one". `master sc form-album` (tidal-ears `66bacc5`, `src/tidal_ears/scform.py`)
fills that form. Superseded: the whole `POST /tracks` reconstruction effort, and the note
that "metadata PUT is free" — PUT also 400s, with a *different* error
("Failed to parse track update request") for flat, wrapped and array shapes alike. **The
api-v2 write path is a dead end for BOTH create and update. Do not go back to it.**

Two DOM facts that cost a session each, both now encoded in `scform.SEL`:
- the form is in a **nested same-origin iframe** `/n/upload?…v2_layout=true`, so the top
  document reports zero file inputs on a perfectly good session (`Browser.upload_frame()`);
- **sharing is a MUI Select**, portal-rendered only after its trigger is clicked. Sweeping
  radios/checkboxes/selects finds geo (worldwide/exclusiveRegions/blockedRegions) and
  licensing (all-rights-reserved/commons) and truthfully reports no privacy control.
  PLN supplied the markup: `li[role=option][data-value="Private"]`.

**STATE OF THE ACCOUNT (verified 2026-08-18 by API, not by memory):**

| up, private | missing |
|---|---|
| sunset-forest-opal-festival-2026-full-set (72:41) · wap · there-s-something-about-drums-3 · you-my-sunshine · take-five-drops · piment-bresilien · eh-ouais-je-funk · ghosts-in-the-toilets | ceci-n-est-pas-une-bombe · am-i-doing-it-right · perfect-3 · gimme-acid · mafia · vague-de-crime · revolution |

The run stopped when PLN closed the Chromium window by mistake. **It is idempotent by
permalink — just re-run**, the 8 will be skipped:

```
python3 armada/tide-table/build_release_plan.py judge_specs/opal26.json \
    --variant streaming --include-mix \
    --out /home/pln/Work/Sound/Prod/Opal26_master/release/opal26_plan.json
cd ~/Work/Sound/tidal-ears && .venv/bin/python -m tidal_ears.cli master sc form-album \
    --plan /home/pln/Work/Sound/Prod/Opal26_master/release/opal26_plan.json \
    --sharing private --state ../Prod/Opal26_master/release/sc_state.json --go
```

**TWO DEFECTS TO FIX FIRST:**
1. **`/take-five-drops` uploaded SHORT — 6:57 on SoundCloud vs 7:21 in the plan (440.7 s),
   ~24 s missing.** Every other track matches to the second. Delete it on SoundCloud and
   re-upload; do not assume a re-run fixes it (the permalink exists, so it will be
   SKIPPED). Verify with the duration check in `scratchpad/whatsup.py` pattern —
   `GET /users/{id}/tracks?access=playable,preview,blocked` and compare `duration`.
2. **`/ghosts-in-the-toilets` has one malformed tag** — `'"livecoding, festival, opal, …"'`
   as a single quoted chip, from the pre-fix run. The chip-input bug is fixed in
   `_fill_tags` for new uploads, but this track needs repair **via the track edit FORM**
   (api-v2 PUT is dead, see above).

**NOT YET DONE:** artwork (`--artwork` is wired but no cover chosen — the `output/*.jpg`
set is untracked GenAI of unknown provenance and was rejected by PLN), and no playlist/
album grouping on SoundCloud.

### A4 · Rights ledger  🟡 **29 of 60 banks answered**

`armada/tide-table/rights_audit.py` + `rights_ledger.json` (`a00066b`, `15ed166`).
Discriminator: `git ls-files` in the Dirt-Samples checkout — 217 upstream vs 509 local;
"lives in that folder" is NOT "is a stock bank". PLN cleared the five keystones
(jungle_breaks / h2o* / kick / risers / vec1*), which answered 10 banks.
**Next: `snare` (4 tracks), then 30 mostly-single-track banks.** Per-track release on
every platform waits on this; the continuous mix does not.

### A5 · Multi-platform adapters — YouTube ✅ BUILT · Bandcamp/RouteNote 🔴

**YouTube adapter shipped** (tidal-ears `30b64ea`): `master yt {render,probe,login,
upload}`, reading the SAME plan. **The Data API cannot publish** — any API project
created after 2020-07-28 has every `videos.insert` upload locked to private until
Google audits the project, so it is browser-Studio for upload and API for metadata
only. Quota is NOT the constraint (the old 1600-units/6-per-day figure became ~100
units in Dec 2025). `ytvideo.py` verified end to end on a real track. **Studio
SELECTORS ARE UNVERIFIED** — run `master yt probe` against a live session before
trusting `upload()`; needs `master yt login` first. Videos come from Slopmotion, not
a still — see EPIC G.


One manifest (`build_release_plan.py`, platform-neutral, refuses without an ear signoff),
many adapters, one state file `releases/<gig>.json` recording what landed where.
Researched 2026-08-16: **Bandcamp has NO upload API or CLI** (its OAuth API is for labels
/ merch-fulfilment, by application; every community tool is a scraper) → headful browser
reusing `scbrowser.Browser`. **RouteNote** likewise has no open API but ships a desktop
upload tool worth probing first. **YouTube** has a real API — and chapters come free from
`segments_v4.json`. PLN's priority: *"imo we need them all. youtube important, routenote
is top tier, bandcamp fine but prob clis exist, a4 pass indeed sounds important"*.
Rights rule, his words: *"cant push the single tracks when samples hit, but full mix
might well pass!"* — RouteNote/DSP is the one place a claim costs the account, not a track.

### A6 · www gig list  🔴 NOT STARTED

38 gigs published 2022–2026; 2026 has four (le-vortex 01-04, ete-surprise 03-07,
montreuil-algorave 05-22, opal-festival-2026 08-08). PLN believes some are missing —
reconcile against `backlog.md`'s month sections (`Juin Faites de la musique`, `Juillet
Douillet`, `AOUT Tout Doux`…). Also **opal-festival-2026.md has empty `audio`/`video`/
`archive`** — fill once the upload lands.

### A3-old · #179 original scope

Still **NOT BUILT**. PLN 2026-08-16: *"yes go build the uploader. im open to giving you SC
pass/user as in the .env if the cookie approach doesnt suffice. imo we need headful browser
automation."*

**Design settled by research** (see memory `reference_soundcloud_datadome`): SoundCloud is a
confirmed DataDome customer, and stealth/headless degrades to ~0 within 24 h. But that
literature is about impersonating many users; we automate PLN's own account with his own
music. So: **headful `launch_persistent_context` on `/usr/bin/chromium` with a real
user-data-dir holding the logged-in session** — be a real browser, don't defeat detection.
Browser for the FILE step only; metadata PUT is unwalled, so titles/artwork/tags/playlist go
through the plain API. `.env` credentials are the fallback only — a scripted login is the most
bot-shaped action in the flow.

- [ ] Build it. Only `/uploads/*` is DataDome-walled; **metadata PUT is free** — see
      `reference_soundcloud_write_path`, `reference_soundcloud_cli`.
- [ ] **Uploaded ≠ cleared.** Content-ID flags arrive by *email*, never through the API, so the
      tool must model those as separate states.
- [ ] Destructive calls must pass an allowlist predicate on a test-namespace permalink prefix.
      The 110 real tracks must be unreachable **by construction**.
- [ ] Delete playlist `2282681433` — sanctioned by PLN, **only once v4 is up**.

### A4 · Rights / Content ID

- [ ] **#4 "Am i Doing it Right" is flagged on SC** and it is a *true* positive — the score
      literally uses the `daft` sample bank. The durable asymmetry: the same audio **passed
      inside the 87-minute continuous mix** and flagged as a 7-minute standalone.
- [ ] Consequence to act on: static-check every track's `.tidal` sample banks **before**
      planning a per-track release. The full-set upload is the lower-risk path.

---

## EPIC B — Cut / boundary detection

### B1 · #178 — Crossfade-blind cut detection  ⚰️ **DEAD END, declared 2026-08-16**

Three attempts, all validated against the ear and all recorded in-code with their numbers:

| | approach | median err | coverage |
|---|---|---|---|
| `cut_lens.py` | midpoint reference, weighted mean, threshold | 24.0 s | — |
| `cut_lens2.py` | adjacent refs, median over orbits, CUSUM | 16.6 s | 8/14 |
| `cut_lens3.py` run 1 | span + self-calibrating fraction `u` | 3.7 s *(n=2)* | 8/14 |
| `cut_lens3.py` run 2 | medoid refs + effect-size gate | 0.4 s *(n=2)* | **4/14** ↓ |

**Do not tune this a fourth time.** The accuracy figures are not evidence — n=2, and those two
boundaries (#8, #10) already had ear answers; a metric computed on the cases that were already
easy measures nothing. The real result is the pattern: **#13 and #15 produced zero voting
orbits across all three gates and two estimators.** Those transitions have no orbit whose
timbre changes materially across the switch — the outgoing and incoming tracks share their
sounds — so a timbral lens is structurally blind there, exactly as an activity lens is blind
to a crossfade. Loosening further can only admit noise.

Two ways out, if ever revisited:
- [ ] **Change the feature** — harmonic/key change, or per-sample identity from the score's
      sample banks (which differ even when the spectrum does not).
- [ ] **Accept ear-only** for the blind cases. Cheap and honest: two timestamps, seconds each.

Standing rule: validate any new attempt against `Montreuil26_master/boundaries_ground_truth.json`
(14 user-confirmed) **first**. Note Montreuil has no per-orbit stems, so a stem-based lens
cannot use it — that gap is itself worth fixing before attempt four.

### B2 · Boundary lab  ✅ SHIPPED (`192c6e9`)

**→ http://192.168.1.3:8799/bounds.html** (LAN, phone-capable; smoke-tested against this
exact server, 11/11). `j`/`k` next-prev · space play/pause · **Enter takes the playhead** ·
drag to loop a span · candidate buttons audition from 4 s before. Export writes
`boundaries-<gig>.json` in **master** seconds — merge into `verified` in the ear file.

- [x] `build_bounds_set.py` renders a ±45 s clip per cut (not seeking the 817 MB master) and
      emits peaks + candidates. Re-encoded, never `-c copy` — FLAC stream-copy snaps to frame
      boundaries and every marker would inherit the offset.
- [x] #13/#15 show an explicit **ear-only notice** rather than being omitted; they are exactly
      the cuts that need a human.
- [ ] **PLN: make the four calls.** ~1 min each. Then A1.4.

### B3 · The set-judge  ✅ SHIPPED

`9c6a24c`, `20b9151`, `73e9d79` · log `armada/tasks/025-fifteen-intros-are-not-a-set.md` ·
memory `reference_set_judge`. Reusable per-gig via `judge_specs/<gig>.json`.

---

## EPIC C — The floor problem  ⟵ *the highest-value non-release thread*

Origin, PLN verbatim: *"I DIDNT KEEP THE KICK LONG ENOUGH. MAIN criticism / negfb I GOT:
'destructure. les gens etaient un peu perdu'."*

**Measured, and the data agrees with the floor: the kick was audible 59% of OPAL-26.**
13 of 15 tracks under 70%. Longest kickless stretch 164 s (#4). `f1798e7`.

### C1 · #180 — Pre-gig floor lens
- [ ] Static + stem analysis that flags a track's kickless stretches **before** the gig.
- [ ] **Count BARS, not seconds** — the set ran 89–166 BPM, so a seconds budget is wrong by 2×.

### C2 · #181 — HUD v2: debts + instruments  🎯 *the design is settled, see memory*
PLN: *"HUD needs evolve from 'just a lcxl repr' to proper parvagues success metrics/tooling/
state helper"* · *"things should shout at me until playd"* · *"2am-elevated-UX … like
macdonalds' screens help avoid cold burgers"*.

One mechanism — **DEBTS**, things that accrue until discharged:
- [ ] **floor debt** — bars since the kick last sounded. Must be a *budget* (≈16 bars calm →
      amber → red past 32) with an explicit "holding a breakdown" override. **A drop is
      supposed to remove the kick**; an alarm that fires on every intentional breakdown is
      ignored within one set, and then it is worse than nothing. This is the make-or-break.
- [ ] **unplayed debt** — an orbit declared in the buffer that has produced no events since
      the track loaded. Shouts until it plays. (Born from d9 SUGAR CHOIR, A2.)
- [ ] Two registers: **alarms** at the screen edge, colour **+ motion**, no numbers; **instruments**
      (kick % this track / this gig) in the topbar, never blinking.
- [ ] **Signal source**: the buffer parse structurally cannot answer any of this — it only knows
      what was *written*. Add an OSC target aimed at the HUD (`BootTidal.hs` already defines
      57120 / 1818 / 6013, so this is a one-line change avoiding contention). One stream answers
      all of it. Prior art `tidal_ears/osc_monitor.py`. **Verify the cost first** —
      `feedback_observer_perturbs`.

### C3 · #176 — `^41` standardisation across the corpus
- [ ] Verified from source: `midiOn ch pat = someCyclesBy (orDef 0 ch) pat`. So `^41` is a
      **fraction-of-cycles** control, *not* an on/off mute, and `orDef 0` means an untouched
      `^41` is safe (no mute-bomb). A mid value like 64 = the kick on ~half the cycles —
      literally structure that comes and goes, so **`^41`'s value is a first-class HUD signal**.

### C4 · HUD touched/untouched + scenes  · plan file exists, unstarted
Plan: `~/.claude/plans/zazzy-honking-waterfall.md` (touched-state tracker via `aseqdump`
read-only MIDI tap + perf-isolated scene backdrop). **The law: the HUD must never be able to
stall the editor or audio.**

---

## EPIC D — Rig & hardware

- [ ] **#154 — USB hub is a single point of failure** (drops interface + LCXL together).
- [ ] **LCXL v3 ordered** (2026-08-16). Retires: pot-pickup value jump (endless encoders send
      relative deltas — *the whole class disappears*), much of the LED-signalling cost (OLED
      renders values natively), and the USB SPOF (**DIN path is independent of USB**).
      🔍 **UNVERIFIED and it matters:** can third-party SysEx paint *arbitrary* text to the
      OLED? Documented behaviour is only "shows parameter values". C2's alarms are **derived**
      state, not control values — if the OLED is closed, the alarms stay on screen.
- [ ] CC numbering + relative-encoder mode will differ from v2. Change it in `lcxl_grid.py`
      and **regenerate** — never in a copy (`feedback_one_parser_per_concept`).

## EPIC G — Slopmotion release videos  🟡 *renderer fixed twice, one modal left*

The release videos come from **Slopmotion** (Kevin Granger / Shipow —
`../hydra-live-hexa`), NOT from a still image. PLN, rejecting the still:
*"i dont like these covers, wait where are tehse genai images from? these are not great
sources! we wanna use slopmotion for videos remember?"* The `output/*.jpg` set is
untracked, unprovenanced, and `6c3272c refactor: Remove covers from git` had deliberately
removed covers from this repo. **Rights: PLN has rights to ALL Slopmotion packs** — the
opposite of the sample-bank situation.

### G1 · Renderer fixes  🟡 two landed, one open

- ✅ **The playwright browser cache is empty now.** Returning `undefined` does not "let
  playwright pick" — it reaches for `chromium_headless_shell-1234`, never downloaded, and
  EVERY render died before a frame. Falls back to `/usr/bin/chromium` (`1fdfb85`).
- ✅ **Headless DOES reach the GPU** — `--use-gl=angle --use-angle=gl-egl`, ONE WORD from
  the `--use-gl=egl` that silently stays on SwiftShader (`32e7a00`). Measured: swiftshader
  3.5 fps → headful 11.4 → **headless+gl-egl 17.8 fps, 542 of 542 frames captured**.
  Headless is FASTER than headful (no compositor) and has no capture bottleneck.
  **Never open windows for a render** — PLN: *"stop opening chrome windows lol"*.
- 🔴 **A second modal blocks the scene: "START A SESSION".** The guard now refuses to
  capture (`8be26fb`) instead of shipping scrims. The photosensitivity modal IS dismissed
  (uncheck "Show this warning at startup" + OK; seeding localStorage does NOT work because
  `localStorageBridge.ts` re-hydrates from disk at boot and overwrites it).

### G2 · Adapt to the 50-commit `git pull`  🔴 THE NEXT THING TO DO

A pull on 2026-08-18 changed the app under us. All of this is read from source by a
subagent, NOT yet empirically confirmed (the dev server was down at the time):

- **`?studio=1` is DEAD** — it is stripped from the URL. Studio is now the DEFAULT and
  **`?live=1`** opts into the full-bleed performance shell (`app/appHelpers.ts:59-62`).
- **Studio FREEZES shader time** when `window.studioTransportActive === false`
  (`HydraCanvas.tsx:327-334`, `:459-470`) — this is the other half of "no motion", and
  probably the bigger half. **Set `window.studioTransportActive = true`.**
- The **"Minimal performance shell" button no longer exists**, so the click this renderer
  (and `scripts/capture_fx_previews.mjs`) makes is dead code.
- Seed `hexa-onboarding = {"completed":true,...}` and `hexa-onboarding-checklist =
  {"dismissed":true}` — absent from `.hydra-local/localStorage.json`, so a seed sticks.
  Fully deterministic alternative: POST them to `http://localhost:5173/api/local-storage`.
- **Dev server needs `npm install --no-save --no-package-lock`** — `npm ci` REFUSES
  (Kevin's `package.json` and lockfile are out of sync: `@emnapi/runtime` missing). Never
  plain `npm install` — it would rewrite a tracked file in someone else's repo.

### G3 · Bake-off, then the real render  🔴 blocked by G1+G2

`armada/tide-table/slop_bakeoff.py` (`c8e1aa6`) renders N presets of ONE fixed 30 s window
so looks are comparable. First run produced 19 clips that were all scrims — **delete
`visuals/slop/out/bake*.mp4` and re-render** once G1/G2 land. Window = **94 s** (the techno
opener, −10.1 dB, four-on-the-floor: the honest test for kick-synced triggers).
*Window-choice trap: scoring by spectral flux NORMALISED BY LEVEL picks near-silence — it
returned the 69:18 CRIME/REVOLUTION seam at −23 dB. Gate on level first.*

Then grade at `visuals/slop/bakeoff/bakeoff.html` (serve with
`python3 armada/serve.py --dir visuals/slop --port 8742`, root is `visuals/slop` because
the page reaches into `../out/`). Then the real thing:
`armada/tide-table/render_release_videos.py` — **2h25m of REALTIME capture** for the
landscape shape, hence its `--unit` systemd option. Resumable.

---

## EPIC H — Long-term ParVagues setup plan  ⟵ *PLN asked for this explicitly, 2026-08-18*

The rig work, split by what a gig needs BEFORE it versus what only matters AFTER. OPAL-26
(2026-08-08) is the reference gig: it exposed most of these.

### H1 · PRE-GIG — things that must be true before playing again

| # | thing | state |
|---|---|---|
| C1 | **Pre-gig floor lens** — kick was audible only **59%** of OPAL-26. Measure before, not after. | 🔴 |
| — | `tools/gig-up.sh` is THE gate; keep it cold-by-default, keep hard-vs-soft honest | ✅ exists |
| D | **LCXL v3** — ordered; endless encoders kill pot-pickup, OLED + RGB rings, DIN path | 🟡 arriving |
| — | **Orphan orbits** — 8 of 12 OPAL transitions left ghosts; `xfadeIn`'s outgoing pattern never stops | 🔴 |
| — | **Preload staleness** — generated + gitignored + never compared = the crackle that debuts at the venue | 🔴 |
| — | **SC supervision** — scsynth is sclang's CHILD; a green unit ≠ sound. Watchdog must outlive its supervisee | 🟡 |
| — | **Audio graph budget** — the J-5 dropout: unused nodes held open by a graph-wide no-suspend rule | 🟡 |
| E | **Open taste calls** — LED brightness, cut groups. Waiting on PLN's eyes/ears, not on work | ⏸ |

### H2 · POST-GIG — the release pipeline (where this session lived)

| # | thing | state |
|---|---|---|
| A1 | Ear-verified re-cut + sign-off | ✅ done 2026-08-16 |
| A3 | SoundCloud upload | 🟡 8/15 up, see A3 |
| A4 | Rights ledger — **30 of 60 banks**; `snare` gates 4 tracks | 🟡 |
| A5 | YouTube ✅ built · Bandcamp 🔴 · RouteNote 🔴 | 🟡 |
| A6 | www gig list + fill opal-festival-2026's empty `audio`/`video`/`archive` | 🔴 |
| G | Slopmotion release videos | 🟡 |

### H3 · The standing lesson from this session

Three of my confident conclusions were wrong the same way, and each cost hours:

1. *"The upload form does not render"* — it was in an iframe I never enumerated.
2. *"Headless cannot reach the GPU"* — I tested three flag combinations of five and stopped
   at the first that worked.
3. *"The uploader is public-only"* — a MUI Select's options do not exist in the DOM until
   the trigger is clicked, so "absent" and "unmounted" look identical to a sweep.

In all three the rig was fine and the **instrument** was blind, and in all three PLN's own
eyes beat the probe — one pasted `<li>` settled what four probes could not. This now joins
[[feedback_check_the_instrument_first]]: **before concluding a feature is missing, prove
the search could have found it.** An early success is not a search.

## EPIC E — Open taste calls  ⟵ *waiting on PLN's eyes/ears, not on work*
- [ ] **#11** LED brightness · **#70** cut groups. See memory `project_open_taste_calls`.

## EPIC F — Completed
Full tracker history in **`armada/tasks/board-archive.md`** (#1–#103, through 2026-07-29).
Narrative logs in **`armada/tasks/001-*.md` … `025-*.md`** + dated logs. Structured long-form
archive in **`armada/tasks/completed-archive.md`**.

---

## Recommended order  *(rewritten 2026-08-18 — the old one is done)*

The release is one re-run away from complete, so finish it before starting anything.

1. **A3 defect #1** — `/take-five-drops` is 24 s short. Delete it on SoundCloud, then
   re-run `form-album`. It will NOT self-heal: the permalink exists, so a re-run skips it.
2. **A3 re-run** — the other 6 missing tracks. Idempotent; the 8 already up are skipped.
   *(This is ~15 minutes of watching an upload. Do it first.)*
3. **A4 `snare`** — one answer unblocks 4 tracks. The single highest-leverage question on
   the whole board, and it is a question for PLN, not work.
4. **G2 → G1 → G3** — the slop renderer, in that order: adapt to the pull (`?live=1`,
   `studioTransportActive`, onboarding seeds), then the last modal, then re-bake the 19
   clips for PLN to grade. Everything downstream (YouTube, the release videos) waits here.
5. **A6** — the www gig list, and fill opal-festival-2026's `audio`/`video`/`archive` with
   what A3 produced.

**C2 (HUD v2: debts + instruments) is still the highest-value non-release thread**, and its
design is settled — it needs building, not deciding. **C1 (pre-gig floor lens) is the
highest-value thing before the NEXT gig**, since kick was audible only 59% of OPAL-26.

*Do not start EPIC B. B1 is a declared dead end.*

---

## AMENDMENT — 2026-08-21 reconciliation

### The live board WAS recoverable. It is in `.claude/RESUME.md`.

This file opens by saying the Task API was unreachable so the tree had to be rebuilt from
durable sources. That was true of the *tools*, not of the *data*: a near-limit checkpoint of
session `36236d51` wrote the full TodoWrite state to **`.claude/RESUME.md`** (2026-08-21
14:58) — **112 items: 22 done, 7 marked current, 83 open.** Read that file first; the epics
below are a curated 20% of it. (`.claude/` is untracked, and the checkpoint ref
`refs/claude/checkpoint-36236d51` expires in ~2 weeks — so the 112-item list is NOT durable.
If it matters, it must be promoted into this file.)

**Four of the 7 "current" items are stale-active** — git says they landed:
`Building the SoundCloud writer` → A3 (`66bacc5`); `Re-cutting OPAL-26 v4` → A1, ear-signed
2026-08-16; `Building the mix/master/split stack` → shipped; `Per-track acceptance test` →
`pv-at` exists. Genuinely in flight: **Slopmotion clips headlessly** (EPIC G), **gig
transitions**, **HUD REC warning**.

### EPIC I — Rig hardening & The Bridge  🆕 *9 commits the board never knew about*

Landed 2026-08-21 by a parallel session in this checkout, `2ef09af..c821bdf`:

| commit | what |
|---|---|
| `26a1e7b` | **one RIG UP button** — `gig-up --converge` and the gearbox both wrote governor/EPP with no arbiter; whichever ran last won, and ppd re-asserts on AC transitions, so a converge done on battery was undone by plugging in for the set. Converge now drives the gearbox, and deliberately does NOT touch trim (quiet-vs-cool is a room call). |
| `b893a06` | tray = the one face PLN actually looks at |
| `72937cc` | the LCXL panel could not see the daemon that lights the board |
| `7bda1cc` | gig-up: the gate that could not fail, and the two units nobody started |
| `4e0d63a` | two scripts were +x on disk and 644 in the index (`feedback_chmod_is_not_a_fix`) |
| `b503502` | absent is not broken, and a deep queue is just latency |
| `cb5a625` | **check-drift told PLN to delete his set the day before a gig** |
| `c821bdf` | stop naming a cause this check cannot see |
| `eb9221d` | **CosmicFest prep** — 24 track files + a new `live/midi/nova/techno/livecode_parade.tidal` |

This is H1 (PRE-GIG) executing. It closes H1's *"SC supervision"* and *"gig-up is THE gate"*
rows and adds the Bridge panel as the GUI half of converge.

### 🔴 CosmicFest — a gig thread with no canonical metadata

`eb9221d` is a prep pass across the whole set, and `cb5a625` says *"the day before a gig"*.
But **`content/lives/2026/` has no cosmicfest page** — only le-vortex, ete-surprise,
montreuil-algorave, opal-festival-2026. `content/lives/2025/cosmicfest.md` is the 2025
edition. Per `feedback_metadata_vs_mastering` I will not invent the date or the venue.
- [ ] **ASK PLN:** is there a CosmicFest 2026 (or another imminent gig)? A date turns every
      H1 row from "someday" into a deadline, and it decides whether the release or the rig
      comes first.

### Parked, not to be committed by anyone but PLN
`backlog.md` (+60 lines) and `armada/setlist_opal2026.txt` are modified by hand.
**`backlog.md` is the setlist SSOT — parse it, never rewrite it.**

### Merge candidates for PLN's call (flagged, not merged)
- **A3-old** and **A4 · Rights / Content ID** are superseded duplicates of A3 and A4 above.
  A3-old's *"Build it"* is done; its still-live residue is only the two safety rules
  (test-namespace allowlist, Content-ID-arrives-by-email) and the sanctioned deletion of
  playlist `2282681433` once v4 is up. Suggest folding both into A3/A4 and deleting.

---

## AMENDMENT — 2026-08-22, pre-gig close-out  *(appended; EPIC I above is the commit list)*

### ✅ CosmicFest is REAL and PLN plays it next — the open question above is half-answered

PLN, in his own words this session: *"do please do a commit `feat: cosmicfest prep`"* and
*"tomo ill setp and play"*. So the gig EXISTS and is imminent; that part needs no further
asking. What is still missing is only the **canonical metadata** — `content/lives/2026/`
has no cosmicfest page (2025's is a different edition). Per `feedback_metadata_vs_mastering`
the venue/date/lineup must come from PLN or the www content, never from a commit message.
- [ ] **A6-adjacent:** create `content/lives/2026/cosmicfest.md` + `tracks.json` from PLN's
      details AFTER the gig. Do not backfill it from `eb9221d`.

### gig-up: 4 of 5 blockers cleared, and the 5th is a DECISION not a defect

| gate | before | after |
|---|---|---|
| tools executable | FAIL | ✅ `4e0d63a` — two scripts +x on disk, 644 in the index |
| audio graph | FAIL | ✅ `b503502` — absent-UMC is now `warn`, not NO-GO |
| mute map | FAIL | ✅ `fix-mute-roles --apply`, converged to 0 |
| pvlint (setlist) | FAIL (4 err) | ✅ 0 errors — two crushbus slot collisions split |
| **surface grid intact** | FAIL | 🔴 **still red, on purpose** |

**Why the last one stays red.** 9 knobs off-column, **20 moves pending** — ghosts 10,
something_about_drums 7, perfect 2, the_revolution 1. PLN: *"migration i unfinishe dgrrr
lets not finish it before set for now."* Renumbering which physical knob does what, in four
tracks he plays imminently, is a muscle-memory change and not a safety fix; two of the moves
cannot resolve mechanically anyway (one `overflow -> gSel`, one `FIXME (no slot)`).
- [ ] **AFTER the gig:** `python3 tools/migrate-columns.py --plan` then apply, then
      `tools/check-drift.sh` must go green and `--plan` must report 0 moves.

### 🔎 RESUME: a renderer burns ~112% and it is NOT the highlighter

The open perf thread, with the guesswork already eliminated so nobody repeats it:

* **The highlighter is EXONERATED by its own instrument.** `tidalcycles:highlight-stats`
  over a 16919 s window: `RAF 5/s, work 5/s, frame avg 1.671 ms` = **0.8% of one core**.
  Not the burner, not close.
* **The marker leak is genuinely DEAD in the field:** `markers 293 live,
  creates/destroys 0/s`, `toggles 105/s` (the cheap reuse path doing its job), `errors 0`.
  Confirms `6e9f374` (2026-07-18) + `ba77c4f` (2026-08-03) over a 4.7-hour window. **Do not
  re-open the leak hunt.**
* **Already ruled out:** `editor-background` is in `disabledPackages`; sound-browser's three
  `rAF(processChunk)` loops all terminate on `currentIndex < total`; `aseqdump` flushes per
  event (proved on an isolated client — median 40 ms inter-arrival at a 25/s send rate);
  fork+exec per LED write costs 2.8 ms p50 and `--bench` shows the coalescer at p99 18 ms
  with zero overrun.
* **Caveat on the evidence:** the profile window was 4h42m while the hot renderer (pid
  799788, `--type=renderer`, 3 `Media` threads) was 3 minutes old — so the stats likely came
  from a DIFFERENT window than the burner. The 0.8% conclusion holds regardless.
* **Next step:** run `tidalcycles:highlight-stats` *in the hot window*, and get a per-thread
  breakdown inside pid `--type=renderer` (the 3 `Media` threads are unexplained with
  editor-background disabled).
- [ ] **NOT a gig blocker:** xrun delta over 25 s was **0 on every node** (ardour's 1200, SC's
      38, the sink's 11 are all historical). High CPU has not cost audio. Fix it for editor
      responsiveness, not for sound.

### Remaining known jank (editor only)

`jank: 245 frames >16ms, 91 >50ms | worst 179.82ms did 22 adds / 5 removes` — one visible
stall roughly every 3.1 minutes, ~6.5 ms per add, so stalls scale with burst size. The
highlight stylesheet is already proven to set no layout-affecting property
(`spec/highlight-class-contract-spec.cjs`), so the cost is elsewhere in the add path.
- [ ] Attribute the per-add cost in `#createPositionMarkers` / the add path (post-gig).

### ⚠️ PARKED — not mine to commit

* `~/Work/Tools/pulsar-tidalcycles` — **`3205e7f` is committed but UNPUSHED** (no SSH access
  to `github.com/PLNech/pulsar-tidalcycles` from that session). Branch
  `perf/event-highlighter`. PLN pushes it.
* Same repo: `lib/sound-browser.js` has **144 uncommitted insertions** (waveformCache, lazy
  AudioContext, currentAudio tracking) written by an earlier session. Loaded by the running
  Pulsar. Unvalidated — left alone deliberately.
* `Tidal`: `backlog.md` (+60 lines) and `armada/setlist_opal2026.txt` are PLN's own
  uncommitted edits, untouched all session (`feedback_...` — backlog is never mechanized).
* `Tidal`: `live/midi/nova/breaks/hello_fred.tidal` is a 1-line stub (`d1 $ `) — deliberately
  not committed.

---

## AMENDMENT — 2026-08-28, post-CosmicFest reconciliation  *(LCXL3 in hand)*

No gig is scheduled. Focus declared by PLN: **mastering, tooling debt, comms** — and
this session is explicitly a **probe/explore/test session** for the new controller:
*"we wanna probe, explore, test stuff. you can even write ad-hoc test soft for us to
explore the interactions, UX, possiblities, OLED displays, visual language, etc. lets
take the time."*

### ✅ EPIC D · LCXL v3 — the UNVERIFIED block is now VERIFIED. Delete the guesswork.

Source: the official [programmer's reference guide v1.0](https://fael-downloads-prod.focusrite.com/customer/prod/downloads/launch_control_xl_3_programmer_s_reference_guide-pdf_en.pdf).
Device confirmed present: USB `1235:0148`, ALSA `client 20: 'LCXL3 1'`, card 1 `L1`.

**The OLED is fully open — the board's biggest open question, answered YES.**
- Bitmap: `F0 00 20 29 02 15 09 <target> <1216 bytes> 7F`. 19 bytes/row x 64 rows,
  7 px per byte, MSB = leftmost. Target `20h` stationary, `21h` global-temporary.
- **It ACKs each frame** (`F0 00 20 29 02 15 09 7F`) *"suitable for timing fluid
  animations"* — so animation is a designed-in feature, not a hack. DESIGN.md's
  "alarms need motion" is therefore satisfiable ON THE DEVICE.
- Text path (cheaper, sharper): `F0 00 20 29 02 15 06 <target> <field> <ascii> F7`,
  ASCII 20h-7Eh plus Empty Box 1Bh / Filled Box 1Ch / Flat 1Dh / Heart 1Eh.
  Arrangements 1-4; **arrangement 3 = "1 line + 2x4: Title and 8 names"**, i.e. built
  for labelling 8 encoders.
- Limits that are real: 1-bit mono, **firmware holds ONE bitmap at a time**.

**True RGB per control:** `F0 00 20 29 02 15 01 53 <idx> <R> <G> <B> F7` (buttons and
encoder rings). Palette path: `B0 <idx> <colour>`, 128 colours.

**Endless encoders are per-ROW opt-in, not default:** `B6 45h/48h/49h 7Fh` for rows
1/2/3. Pivot `40h` = no movement, `41h` = +1 step, `3Fh` = -1 step.

**Feature CCs (DAW in port, ch 7; query on ch 8; reply always ch 7):**
`30` surface-mode select+report · `63` Shift · `69/72/73` per-row relative ·
`70` fader pickup · `71` touch events · **`111` LED brightness (non-volatile)** ·
**`112` screen brightness** · `113` temporary-display timeout · `120` Out2 thru ·
`100` global MIDI channel · `121` encoder curve (slow/med/fast).
Standalone needs `9F 0B 7F` to enable feature controls.

**Modes:** CC 30 both reports AND sets — DAW Mixer `01`, DAW Control `02`,
Custom 1-4 `06-09`, 5-8 `12h-15h`, 8-16 `16h-1Dh`. Shift + Mode opens the on-device
Custom Mode Settings menu.

### 🔴 NEW BLOCKER on the #94/#176 migration — the v3 DAW map SILENTLY OVERLAPS v2

DAW mode's CC map is **fixed**: faders `5-12`, encoders `13-20 / 21-28 / 29-36`,
buttons `37-44 / 45-52`. Compare v2 (`reference_lcxl_column_grid`):

| control | v2 CC | v3 DAW CC | verdict |
|---|---|---|---|
| knob row A | 13-20 | 13-20 | identical |
| knob row B | 29-36 | 21-28 | **v3 reuses 29-36 for row 3** |
| knob row C | 49-56 | 29-36 | shifted |
| faders | 77-84 | 5-12 | **v3 relative-encoder row 1 IS 77-84** |
| buttons | 41-44/57-60/73-76/89-92 | 37-44/45-52 | all move |

Nothing errors; controls just move one row. That is precisely the failure class
`b5ad8b6` spent 169 files fixing. **Consequence: the unmerged migration branch may
need to be re-derived for v3, or deliberately frozen at v2 numbering.** Do not merge
`b5ad8b6` until the mode question below is settled.

### ⚠️ RETRACTED board claim — DIN is NOT our USB-SPOF mitigation

EPIC D and `reference_lcxl_v3_ordered` both claim *"a DIN path is independent of USB,
which is a real gig-reliability win"*. Measured on the live system: `aconnect -l`
shows only `Midi Through` and `LCXL3 1` — **there is no MIDI DIN input on this
machine at all** (the UMC202HD has no MIDI jacks). The controller could speak DIN all
night and nothing host-side would hear it. So DAW mode's "DIN outputs disabled"
restriction **currently costs nothing**, and #154's mitigation must come from
elsewhere (a powered hub on its own port, or a second interface).

### THE KEYSTONE PROBE — decides everything above, costs ~10 minutes

The guide places *Colouring the surface* and *Controlling the screen* inside the DAW
mode chapter, prefaced *"only available once DAW mode is enabled"*. **That is a
document claim, not a tested one**, and `feedback_check_the_instrument_first` applies.
- [ ] Send `B0 <idx> <colour>` and the bitmap SysEx to the **MIDI In** port in
      standalone mode. If paint works there, we get colour + OLED for **zero corpus
      churn** and the whole renumbering problem evaporates.

### CosmicFest — closed as a mastering input

PLN: *"stage mic, and imo we have a bad rec cause the whole soundsystem had effects
and at some points ppl messed with it lol. well see, not a focus important atm."*
Verified absence, five independent probes: Ardour "Tidal Live" newest wav 2026-08-14
(`GetTogether2`); no audio under `Work/Sound/` since 08-20; 0 files in
`~/Videos ~/Music ~/Desktop` since 08-20; newest gig-log `gig-20260820-171315.jsonl`;
no `*cosmic*` dir on the (mounted) freebox.
- **No forensics, no rec-arm gate** — PLN's call, recorded here so nobody re-opens it.
- Still open, unchanged: `content/lives/2026/cosmicfest.md` + `tracks.json` from PLN's
  details. **Never backfilled from a commit message** (`feedback_metadata_vs_mastering`).
- So "mastering focus" means **the OPAL-26 release backlog**, not new CosmicFest audio.

### A4 rights ledger — real numbers, replacing "29 of 60"

`armada/tide-table/rights_ledger.json`: **103 banks** — `third_party` 43,
`unknown` 31, `dirt_samples` 19, `cleared` 10. The board's "30 of 60 answered" is
stale in both numerator and denominator.

### 🆕 Newly found this session

- **HUD BUG (mechanism known): `scene-off` is an action, not a state.**
  `pulsar-parvagues-hud/lib/scenes.js:236 off()` tears down `mediaEl` and returns; the
  re-apply path at `:284-289` runs on every buffer parse and does
  `spec = _directiveScene || cfg.defaultScene -> show(file)`. So any ctrl+save
  resurrects a scene you turned off. Fix: a `_userOff` latch set by the command,
  checked at `:284`, cleared on explicit scene-on / mode toggle / track change.
  PLN, verbatim: *"bug on scene off in hud when i ctrl+save the tidal track again
  scene restarts :facepalk:"*
- **Stale device-name match for v3.** `tools/bridge/rig.py:120` matches `"LCXL"` so it
  survives, but `tools/bridge/surface.py:51 SURFACE_NAME = "Launch Control XL"` will
  NOT match `LCXL3 1`. Worse: surface.py *"prefers the LOWEST port number"*, which on
  v3 is `MIDI In` (port 0) — DAW is port 1. In DAW mode that heuristic picks wrong.
- **Open taste call #11 is now a knob, not a debate.** LED brightness is CC 111 and
  non-volatile; screen brightness CC 112. Reframe `project_open_taste_calls`.
- **Firmware:** device inquiry replies `F0 7E 00 06 02 00 20 29 48 01 00 01 01 00 0A 54
  F7` — version field `01 00 0A 54`, which *reads* as 1.0.x (pre-v1.1), but that field
  is undocumented so it is an inference. Authoritative check is on-device: unplug, hold
  both Page buttons, plug in — the screen shows Application + Bootloader versions.
  v1.1 (Jan 2026) adds Pro Tools/Reaper/Studio One, USB-to-DIN passthrough, Shift+fader
  value on the OLED, adjustable encoder response.

### PLN's OLED priorities (answered 2026-08-28, multi-select)

1. **Floor debt + kick %** — the measured problem (59% of OPAL-26).
2. **Unplayed-orbit alarm** — the d9 SUGAR CHOIR class.
3. **Rig health** — xruns/CPU/scsynth alive/Ardour rec-armed.
4. Verbatim addition: *"unplayed orbit and also unplaued effect, or button, unless we
   display already that 'you didnt touch this since that track started' state in the
   button color per se"* — i.e. **untouched-since-track-start belongs on the CONTROL,
   not the screen.** This merges with C4 (HUD touched/untouched). Constraint that
   governs it: `feedback_dark_means_unmapped_outranks_all` — dark already means "not
   mapped", so untouched must NOT be dark. With RGB we have room: untouched =
   desaturated role hue, touched = saturated. Must stay consistent with
   `reference_lcxl_colour_settled`.

---

## AMENDMENT — 2026-08-28 (later): PLN's answers + the playability gate

### Firmware CONFIRMED from the device: **1.0.1364.0**, bootloader same, Easy Start ON

PLN read the bootloader screen (unplug → hold both Page → plug in). This also
validates the decode: the inquiry reply's version field is **7-bit packed** —
`01 00 0A 54` → `1`, `0`, `(10<<7)|84` = **1364**. Record that; it is undocumented.
- [ ] **Update to firmware v1.1.** We are behind. v1.1 adds Pro Tools/Reaper/Studio
      One, USB-to-DIN passthrough with channel filtering, **Shift+fader shows the value
      on the OLED**, and adjustable encoder response. Components is documented as
      Mac/Windows — **untested whether Chromium WebMIDI on Linux works**; that is the
      first thing to try, and if it fails the update needs a borrowed machine.
- [ ] **Turn Easy Start OFF** (bootloader mode, Record button). While on, the device
      also mounts as Mass Storage — noise we do not want in the rig's device graph.

### ❌ A4 `snare` — NOT cleared. PLN's read was a name collision. Proof:

PLN: *"snare rights its also a default tidalcycles CC0/public domain pack!"*
Checked against the actual upstream checkout
(`…/downloaded-quarks/Dirt-Samples`, a git clone of `github.com/tidalcycles/Dirt-Samples`):

    git ls-files -- 'snare/*'  ->   0 files tracked upstream
    git ls-files -- 'sn/*'     ->  52 files tracked upstream

**The default TidalCycles snare bank is `sn`. `snare` is a separate local bank** (90
files, ledger `origin: local`, `status: unknown`). Textbook
`reference_sample_rights_audit`: *"in the Dirt-Samples folder" ≠ a Dirt-Samples bank.*
The ledger entry is therefore left at `unknown` — **it still gates 4 tracks.**
Two ways forward, both cheap, PLN's call:
- **Identify it** — fingerprint the 90 files against known packs (`rights_audit.py`,
  CLAP tags), or find the download it came from.
- **Substitute it** — swap `snare` → `sn` in the 4 gated tracks. Mechanically trivial,
  but it changes the sound, so it is a taste call, not a chore.

### Priorities set by PLN (supersedes the "Recommended order" above)

> *"lets do the lcxl3 and the tracks repair/migration finish as priorities imo. but for
> this i need be able to play tracks with lxcl3 so prio is that?"*

- **PARKED tonight:** the A3 SoundCloud re-run (*"maybe not tonight"*).
- **STILL TODO, unscheduled:** CosmicFest — the www page + `tracks.json` (metadata from
  PLN only). Its audio is a stage-mic recording PLN rates as poor; not a mastering input.
- **1st:** LCXL3 **playable with the existing corpus**. 2nd: finish the track repair /
  migration. Everything else waits.

### 🔑 The gate PLN identified, and it is correct

The migration cannot be *judged* without playing it, and it cannot be played without the
LCXL3 driving the rig. So **playability precedes the numbering decision** — the reverse
of what the earlier amendment implied. It also means the numbering question must be
answered in a way that does **not** require touching 169 files before he can play.

### The design that satisfies both: a SURFACE DRIVER (adapter), not a renumber

Put an adapter between the v3 hardware and SuperDirt:

    LCXL3 (DAW mode)  ──▶  surface driver  ──▶  virtual ALSA port  ──▶  SC/SuperDirt
      v3 fixed CCs           translate + integrate         v2 CC numbers, absolute

What it buys, all at once:
- **Playable with zero corpus churn** — the 169 tracks, `BootTidal.hs` `_seed`, pvlint
  and `lcxl_grid.py` keep the v2 numbering they already agree on. `b5ad8b6` can merge on
  its own merits instead of being hostage to the hardware.
- **Paint stays available** — we run in DAW mode, so RGB + OLED are on the table.
- **Relative encoders need an integrator anyway.** Endless encoders emit deltas
  (`65` = +1, `63` = -1) and Tidal's `^NN` wants absolute 0-127. Whoever integrates them
  *owns the value* — so the driver is the natural home for it, and that also makes
  `lcxl-init.py`'s seeding meaningful again instead of obsolete.
- **It is where the paint belongs.** Holding the value is exactly what you need to paint
  a ring by value, desaturate an untouched control, or draw the OLED. C2 + C4 land here.

Cost, stated honestly: one more process in the MIDI path, and one more thing that can
break — mitigated by following the existing `midi-autoconnect` systemd-unit pattern
(`reference_rig_reconciler_units`) and by `feedback_verify_the_plumbing` (prove it with
real events, not unit tests on pure functions).

### Session sequence, approved shape

1. `tools/lcxl3-probe.py` — the bench (enumerate, raw SysEx tx/rx, decode every event).
2. **The keystone probe** — does paint work in STANDALONE, or is DAW mode required?
3. OLED sketchpad — render 128x64 frames so the visual language is judged by eye.
4. HUD `scene-off` latch (`scenes.js:236` / `:284`).
5. Then: surface driver → play a track → judge the migration.
