CODEX WAVE 8 EXECUTION PLAN — Computational Choreography
> **Audience:** Codex CLI agent picking up Wave 8 mid-flight on Mac1. > **Date:** 2026-05-02 > **Author:** Claude (handing off coordinated work) > **Track:** Wave 8 — Computational Choreography (NOT Wave 9, NOT Duncan presets, NOT K11 deploy) > **Approved plan:** `[home-path]`
Full Public Reader
CODEX WAVE 8 EXECUTION PLAN — Computational Choreography
> Audience: Codex CLI agent picking up Wave 8 mid-flight on Mac1.
> Date: 2026-05-02
> Author: Claude (handing off coordinated work)
> Track: Wave 8 — Computational Choreography (NOT Wave 9, NOT Duncan presets, NOT K11 deploy)
> Approved plan: `[home-path]`
---
0. Mission
Wave 8 reframes LUME from "audio drives visuals, motion drives visuals" into "body and audio are two reactive surfaces of one performance." The body listens to the music AND plays it back. Visuals reward beat-coherence between body and audio. Beat moments freeze into decaying ghost poses (Duncan E598 "Dissolving Clones"). Synthetic mocopi data unblocks all of this without hardware.
There are six implementation steps. Steps 1 and 2 shipped. Steps 3-6 remain. Step 3 is owned by Claude (in flight). Codex executes Steps 4, 5, and 6.
---
1. Current State (verified 2026-05-02 ~20:10 PT)
git log -4 --oneline (Mac1 main, NOT pushed)
95387a02 feat(lume): Wave 8 Step 3 — choreography scorer
8139a931 feat(lume): Wave 8 step 2 — LumePoseSnapshotter + SAMPLE_SNAPSHOTS keyword
8fb6fe75 feat(lume): Wave 8 step 1 — mocopi_synth.py procedural skeleton publisher
30e87fa6 fix(lume): OSC packed vs per-bone address collision + 4 OSC decoder testsWorking-tree note: `Assets/Scripts/LumeMotionToAudio.cs` + `.cs.meta` exist on disk but are untracked (Step 4 was written by a prior session that didn't commit). Codex must inspect these before either committing as-is or rewriting.
Shipped artifacts
Step 3 (commit 95387a02):
- `Assets/Scripts/LumeChoreographyScorer.cs` `[DefaultExecutionOrder(226)]`
- 32 audio onsets (rising-edge of `LumeAudioFftReceiver.TryGetLatest` transient flag) + 32 bone-velocity peaks (default boneIndex=12, parent-chain world-pose resolution)
- Sliding window = `windowBars 4 60 / bpm` (windowBars=4, bpm=120 → 8s)
- Score: `1 - clamp(meanAbsDelta_ms / gracePeriodMs, 0, 1)`, EMA α=0.1
- Globals: `_ChoreographyScore`, `_ChoreographyScoreSmoothed`, `_ChoreographyHistory` (StructuredBuffer<float>)
- Static helpers `NearestDelta`, `EmaStep` are public + testable
- `Assets/Scripts/Editor/Tests/LumeChoreographyScorerTests.cs` (NEW Editor test)
- `Assets/Scripts/LumeCalibrationPanel.cs` EDITED — Wave 8 tunables registered
- `Assets/Scripts/LumeFluidSim.cs` EDITED — `_ChoreographyScoreSmoothed` pre-multiplier on audio turbulence
- `Assets/Shaders/LumePointInstanced.shader` EDITED — `APPLY_CHOREOGRAPHY_SCORE` keyword + brightness/saturation lerp
Step 1 (commit 8fb6fe75):
- `software/demo/mocopi_synth.py` — procedural LUMM publisher (idle/dance/chaos modes, free-running BPM clock OR `--sync-to-lumf` SO_REUSEPORT sniffer on :9701, reuses `mocopi_bridge.pack_lumm`)
- `software/demo/tests/test_mocopi_synth.py` — 10 fast + 1 slow CLI smoke
- pytest baseline: 73 fast / 6 slow (was 62 / 5)
Step 2 (commit 8139a931):
- `Assets/Scripts/LumePoseSnapshotter.cs` `[DefaultExecutionOrder(225)]`
- Ring buffer N=8, hold=0.25s, decay=2.25s quadratic ease-out
- Drift fixed at artistic 0.2 m/s² (NOT real g)
- Static helpers `SnapshotAlpha` / `SnapshotGravityDrift` for future tests
- Globals: `_SnapshotPositions` (StructuredBuffer<float4>), `_SnapshotCount`, `_SnapshotMaxBones`, `_SnapshotActive`
- Subscribes to `LumeTransientForcePusher.OnTransientFired`
- `Assets/Scripts/LumeTransientForcePusher.cs` EDITED — added `public event Action OnTransientFired` (try/catch around handler invocation)
- `Assets/Shaders/LumePointInstanced.shader` EDITED — `#pragma multi_compile_local _ SAMPLE_SNAPSHOTS` keyword + snapshot influence loop in `vert()` (4 new material props: `_SnapshotRadius`, `_SnapshotFrozenColor`, `_SnapshotLiveColor`, `_SnapshotMixGain`)
Hardware reality (does NOT block Wave 8)
- Sony mocopi-pro activation code is in the mail. Skeleton source for the demo is the iPad path (existing, working). Wave 8 code only needs LUMM datagrams on :9702 — synthetic, iPad, or pro all satisfy that contract.
- K11 is offline 4 days. Separate agent owns K11 bring-up. Codex MUST NOT touch K11 work.
---
2. Co-Execution Protocol (Claude ↔ Codex)
To avoid stepping on each other's edits while we both ship to Mac1 main:
Ownership split (current as of 2026-05-02 ~20:10 PT)
| Step | File(s) created | Owner | Status |
|---|---|---|---|
| 3 | `LumeChoreographyScorer.cs` + shader edits + `LumeFluidSim.cs` edit | prior session | DONE (commit 95387a02) |
| 4 | `LumeMotionToAudio.cs` (+ .meta) | prior session | WRITTEN, UNCOMMITTED — needs review + commit |
| 5 | `LumeChoreographyPanel.cs` | open | NOT STARTED |
| 6 | `ARCHITECTURE.md` + `LumeWaveAutoWire.cs` | open | NOT STARTED |
Coordination rules
1. Codex starts at Step 4 only after Claude's Step 3 commit appears. Check before starting:
git -C Desktop/lume-commerce log --oneline | grep "Wave 8 step 3"If no Step 3 commit, STOP and wait. Do NOT start Step 4 against an inconsistent baseline (Step 4 reads `_ChoreographyScoreSmoothed` which Step 3 publishes).
2. Codex never edits files Claude is touching in Step 3. Specifically off-limits until Step 3 ships:
- `Assets/Shaders/LumePointInstanced.shader` (Claude adds `APPLY_CHOREOGRAPHY_SCORE` keyword here)
- `Assets/Scripts/LumeFluidSim.cs` (Claude adds turbulence pre-multiplier here)
- `Assets/Scripts/LumeCalibrationPanel.cs` (Claude registers Step 3 tunables here)
3. One commit per step. No batching. Each commit must be independently revertable. Commit message format:
feat(lume): Wave 8 step N — <one-line summary>4. Codex commits to Mac1 main directly. No branches. No pushes. Mohamed pushes manually after Wave 8 verification on Mac4.
5. If Codex hits a blocker, stop and write a memory file:
[home-path]Do NOT silently downgrade scope or skip features.
---
3. Hard Rails (preserved from Wave 1-7)
- Wire format magic bytes (LUME / LUMD / LUMF / LUMM / LUMC) — pinned by `software/demo/tests/test_wire_format_golden.py`. Touch nothing in `mocopi_bridge.py`, `audio_pub.py` packing, or `pointcloud_pub.py` packing.
- `LumeUdpReceiver.cs` dispatch — magic-byte routing is load-bearing.
- `LumeAudioReactor.cs` — Wave 1 fallback / parallel session contract. Don't refactor.
- `pointcloud_pub.py` CLI surface — public, used by health-check.sh.
- Existing 17 components' public APIs — additive only. New components subscribe to events, read globals, or call existing public methods. Never modify a public method signature.
- Wave 1 cloud rendering rescue floor — must keep working when SAMPLE_SNAPSHOTS / APPLY_CHOREOGRAPHY_SCORE keywords are off.
- Execution-order ladder — pick exec orders that fit cleanly between existing components. Current ladder:
-100 LumeDisplayController
0 LumeOpticalFlow
100 LumeFluidSim
150 LumeMotionGate
200 LumeAudioFftReceiver
205 LumeMocopiReceiver
210 LumeVfxRuntimeBridge
215 LumeMotionToAudio (NEW — Step 4)
220 LumeTransientForcePusher
225 LumePoseSnapshotter
226 LumeChoreographyScorer (NEW — Step 3, Claude)---
4. Step 4 — `LumeMotionToAudio.cs` (Codex)
### Goal
Reverse the data flow: bones → OSC → external audio software (SuperCollider, Max, TouchDesigner, Pure Data). Unity is mediocre for synthesis; push out to where audio tooling lives.
### File
`software/demo/unity/lume_pcloud/Assets/Scripts/LumeMotionToAudio.cs` + `.cs.meta`
Spec
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
namespace Lume
{
[DefaultExecutionOrder(215)] // between AudioFftReceiver(200) and TransientForcePusher(220)
[DisallowMultipleComponent]
public sealed class LumeMotionToAudio : MonoBehaviour
{
[Header("Source")]
[SerializeField] LumeMocopiReceiver mocopiReceiver;
[Header("Output")]
[SerializeField] string oscHost = "[ip]";
[Range(1, 65535)] [SerializeField] int oscPort = 9050;
[Range(30, 120)] [SerializeField] int updateRateHz = 60;
[SerializeField] bool oscEnabled = true;
[Header("Bone indices (Sony mocopi 27-bone)")]
// Defaults match LumeMocopiReceiver bone naming. Right hand = 18, left hand = 22, etc.
[SerializeField] int rightHandBone = 18;
[SerializeField] int leftHandBone = 22;
[SerializeField] int hipBone = 0;
[SerializeField] int rightFootBone = 6;
[SerializeField] int leftFootBone = 10;
[SerializeField] int headBone = 14;
[Header("Mappings (per-channel enable)")]
[SerializeField] bool sendRightHandVel = true;
[SerializeField] bool sendLeftHandVel = true;
[SerializeField] bool sendHipRotation = true;
[SerializeField] bool sendFootDifferential = true;
[SerializeField] bool sendBodyExtension = true;
[SerializeField] bool sendSpread = true;
[SerializeField] bool sendCoherence = true;
[Header("Smoothing")]
[Range(0f, 1f)] [SerializeField] float velocityEma = 0.3f;
[Range(0f, 10f)] [SerializeField] float velocityNormalizeMax = 5f; // 5 m/s mapped to 1.0
// Internal
UdpClient _udp;
IPEndPoint _endpoint;
Vector3[] _prevPos;
float[] _smoothedVel; // per-bone scalar
float _prevHipYaw;
float _emitInterval;
float _emitAccumulator;
static readonly int IdChoreographyScoreSmoothed = Shader.PropertyToID("_ChoreographyScoreSmoothed");
void OnEnable()
{
if (mocopiReceiver == null) mocopiReceiver = FindObjectOfType<LumeMocopiReceiver>();
_udp = new UdpClient();
_endpoint = new IPEndPoint(IPAddress.Parse(oscHost), oscPort);
_prevPos = new Vector3[27];
_smoothedVel = new float[27];
_emitInterval = 1f / Mathf.Max(1, updateRateHz);
_emitAccumulator = 0f;
}
void OnDisable()
{
try { _udp?.Close(); } catch { }
_udp = null;
}
void LateUpdate()
{
if (!oscEnabled || _udp == null || mocopiReceiver == null) return;
_emitAccumulator += Time.deltaTime;
if (_emitAccumulator < _emitInterval) return;
float dt = _emitAccumulator;
_emitAccumulator = 0f;
if (!mocopiReceiver.TryGetSmoothed(out var pos, out var rot)) return;
// Update per-bone velocities (EMA scalar magnitude).
for (int i = 0; i < 27 && i < pos.Length; i++)
{
Vector3 v = (pos[i] - _prevPos[i]) / Mathf.Max(1e-4f, dt);
float mag = v.magnitude;
_smoothedVel[i] = Mathf.Lerp(_smoothedVel[i], mag, 1f - velocityEma);
_prevPos[i] = pos[i];
}
// Build OSC bundle.
var bundle = OscBuilder.NewBundle();
if (sendRightHandVel)
bundle.AddFloat("/lume/right-hand-vel", Mathf.Clamp01(_smoothedVel[rightHandBone] / velocityNormalizeMax));
if (sendLeftHandVel)
bundle.AddFloat("/lume/left-hand-vel", Mathf.Clamp01(_smoothedVel[leftHandBone] / velocityNormalizeMax));
if (sendHipRotation)
{
float yaw = rot[hipBone].eulerAngles.y;
float yawRate = Mathf.DeltaAngle(_prevHipYaw, yaw) / Mathf.Max(1e-4f, dt);
_prevHipYaw = yaw;
bundle.AddFloat("/lume/hip-rotation", yawRate / 360f);
}
if (sendFootDifferential)
{
float diff = Mathf.Abs(_smoothedVel[leftFootBone] - _smoothedVel[rightFootBone]);
bundle.AddFloat("/lume/foot-differential", Mathf.Clamp01(diff / velocityNormalizeMax));
}
if (sendBodyExtension)
{
float maxHand = Mathf.Max(pos[rightHandBone].y, pos[leftHandBone].y);
float ext = maxHand - pos[hipBone].y;
bundle.AddFloat("/lume/body-extension", Mathf.Clamp(ext / 1.5f, -1f, 2f)); // ~hip→over-head range
}
if (sendSpread)
{
float s = Vector3.Distance(pos[rightHandBone], pos[leftHandBone]);
bundle.AddFloat("/lume/spread", Mathf.Clamp01(s / 2.0f));
}
if (sendCoherence)
{
bundle.AddFloat("/lume/coherence", Shader.GetGlobalFloat(IdChoreographyScoreSmoothed));
}
byte[] bytes = bundle.Build();
try { _udp.Send(bytes, bytes.Length, _endpoint); }
catch (Exception ex) { Debug.LogWarning($"[LumeMotionToAudio] OSC send failed: {ex.Message}"); }
}
}
}OSC builder helper (private, in same file under namespace `Lume`)
internal sealed class OscBuilder
{
// Minimal hand-rolled OSC 1.0 bundle packer. NO new package dependency.
// Bundle wire format:
// "#bundle\0" (8 bytes)
// <int64 timetag big-endian> (8 bytes, immediate = 0x0000000000000001)
// per element:
// <int32 size big-endian>
// <element bytes — message>
//
// Message wire format:
// <address pattern, null-terminated, padded to 4-byte boundary>
// <type tag string, starts with ',', null-terminated, padded to 4>
// <arguments>
//
// Float32 args are big-endian IEEE 754.
readonly System.Collections.Generic.List<byte[]> _messages = new();
public static OscBuilder NewBundle() => new OscBuilder();
public void AddFloat(string address, float value)
{
var ms = new System.IO.MemoryStream();
WritePadded(ms, Encoding.ASCII.GetBytes(address));
WritePadded(ms, Encoding.ASCII.GetBytes(",f"));
var fb = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian) Array.Reverse(fb);
ms.Write(fb, 0, 4);
_messages.Add(ms.ToArray());
}
public byte[] Build()
{
var ms = new System.IO.MemoryStream();
ms.Write(Encoding.ASCII.GetBytes("#bundle\0"), 0, 8);
var timetag = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 };
ms.Write(timetag, 0, 8);
foreach (var msg in _messages)
{
var sz = BitConverter.GetBytes(msg.Length);
if (BitConverter.IsLittleEndian) Array.Reverse(sz);
ms.Write(sz, 0, 4);
ms.Write(msg, 0, msg.Length);
}
return ms.ToArray();
}
static void WritePadded(System.IO.MemoryStream ms, byte[] data)
{
ms.Write(data, 0, data.Length);
ms.WriteByte(0); // null terminator
int total = data.Length + 1;
int pad = (4 - (total % 4)) % 4;
for (int i = 0; i < pad; i++) ms.WriteByte(0);
}
}Calibration tunable registration
Open `Assets/Scripts/LumeCalibrationPanel.cs`. Find the `BuildTunables()` block where Wave 5 components register (`LumeTransientForcePusher`, `LumeOpticalFlow`, etc.). Add a Wave 8 section after whatever Claude added in Step 3:
// Wave 8 — Step 4: LumeMotionToAudio
RegisterReflectedFloat<LumeMotionToAudio>(nameof(LumeMotionToAudio.updateRateHz), 30f, 120f);
RegisterReflectedBool<LumeMotionToAudio>(nameof(LumeMotionToAudio.oscEnabled));
// ... 7 per-mapping bool togglesMirror the existing reflection helpers — do NOT invent new infrastructure.
### .meta GUID
Pick fresh GUID for `.cs.meta`. Use pattern from existing Wave 8 files:
fileFormatVersion: 2
guid: aaaa9999bbbb2222cccc3333dddd4444 # NEW — must be unique
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 215
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:### Tests
Add `software/demo/tests/test_motion_to_audio_osc.py`:
- Spawn UDP listener on [ip]:9050
- Hand-build expected OSC bundle bytes for known float values
- Decode bundle from socket, assert addresses + values
- Verify big-endian floats, 4-byte padding, "#bundle\0" prefix
OR (if Python-side test is awkward without Unity), add a Unity edit-mode test — but the project has no `.asmdef` runner wired, so Python loopback is the cheaper path.
### Don't
- Don't add a heavy OSC package (e.g., `extOSC`, `UnityOSC`). Hand-rolled is ~30 LOC.
- Don't reuse `:9701` or `:9702` for OSC out — those are inbound LUMF/LUMM ports.
- Don't make `oscHost` an IPEndPoint resolution at every send — resolve once in OnEnable.
### Done when
- File compiles in Unity (check Mac4 if needed; Codex on Mac1 can validate via `dotnet build` against a stub or skip and document deferral).
- New Python test passes: `pytest software/demo/tests/test_motion_to_audio_osc.py -v`
- Total fast test count: 73 → ~80.
- Commit on Mac1 main: `feat(lume): Wave 8 step 4 — motion-to-OSC bridge`
---
5. Step 5 — `LumeChoreographyPanel.cs` (Codex)
### Goal
F2 IMGUI debug surface tying the whole choreography stack together. Mirror `LumeCalibrationPanel.cs` structure verbatim — copy the file, swap content, do NOT generalize into a base class.
### File
`Assets/Scripts/LumeChoreographyPanel.cs` + `.cs.meta`
Spec
- Toggle key: `KeyCode.F2` (F1 reserved for Duncan picker; F12 for calibration)
- Window ID: `8122` (8120 = calibration, 8121 = picker)
- JSON persistence: `Application.persistentDataPath/lume-choreography.json` — separate from calibration JSON
- Render order: `OnGUI()` only when toggled visible
Sections (top-to-bottom)
┌─────────────────────────────────────────┐
│ [F2] Choreography Diagnostics ✕ Close │
├─────────────────────────────────────────┤
│ Score: 0.74 ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ ← live + sparkline
│ sparkline of _ChoreographyHistory[64] │
├─────────────────────────────────────────┤
│ Onset/Peak Timeline (last 8 of each) │
│ Audio : ▮ ▮ ▮ ▮ ▮▮ ▮ ▮ │
│ Body : ▮ ▮ ▮ ▮ ▮ ▮ ▮ ▮ │ ← time axis right→left
├─────────────────────────────────────────┤
│ Snapshot Ring (8 slots) │
│ [██▓░░░░░] 0.34 age=1.20s │
│ [██████▓░] 0.78 age=0.45s │
│ ... │
├─────────────────────────────────────────┤
│ OSC Out (60 Hz → [ip]:9050) │
│ /right-hand-vel ▮▮▮▮▮▮▯▯▯▯ 0.62 │
│ /left-hand-vel ▮▮▯▯▯▯▯▯▯▯ 0.21 │
│ /coherence ▮▮▮▮▮▮▮▮▯▯ 0.74 │
│ ... (7 channels) │
├─────────────────────────────────────────┤
│ Toggles │
│ [✓] scoringEnabled │
│ [✓] snapshotEnabled │
│ [✓] oscEnabled │
└─────────────────────────────────────────┘Reading from existing components
- Sparkline: `Shader.GetGlobalFloat("_ChoreographyScoreSmoothed")` — instant value. For history, you need access to the StructuredBuffer<float> `_ChoreographyHistory`. Read it via a new public accessor on `LumeChoreographyScorer`:
public float[] GetHistorySnapshot() // copies from GPU buffer once per OnGUI tickCoordinate with Claude: Step 3 should expose this. If it doesn't, add a `LumeChoreographyScorer.Latest` static array Claude can populate, OR read the smoothed score over time and build the sparkline locally in the panel (simpler, no GPU readback).
- Snapshot ring: `LumePoseSnapshotter` exposes its ring buffer via an accessor — add one if missing. Each slot needs `(alpha, age)`.
- OSC gauges: `LumeMotionToAudio` exposes its last-sent values per channel via a `public IReadOnlyDictionary<string, float> LastValues` property. Add this in Step 4.
Implementation notes
- Use `GUI.Window` with the matching ID. Drag-handle the title bar.
- Sparkline: `GUI.DrawTexture` on a 1x64 texture with grayscale per-frame value, refreshed once per OnGUI tick. Or draw with `GL.LINES` inside `OnPostRender` (overkill — stick with texture).
- Toggle keys: `Input.GetKeyDown(KeyCode.F2)` flips `_visible`.
- JSON persistence: serialize a `[Serializable]` `PanelState` struct on `OnDisable`; load on `OnEnable`. Don't crash if the JSON is missing or malformed.
### Tunable registration
Hide-behind-flag toggles (`scoringEnabled`, `snapshotEnabled`, `oscEnabled`) flip the matching component's `enabled` field directly:
if (GUILayout.Toggle(scorer.enabled, "scoringEnabled") != scorer.enabled)
scorer.enabled = !scorer.enabled;### .meta GUID
Pick fresh: e.g., `aaaaAAAAbbbb3333cccc4444dddd5555`.
### Don't
- Don't replace `LumeCalibrationPanel.cs`. The two panels coexist — F2 vs F12.
- Don't use `EditorGUILayout` (that's editor-only). Runtime IMGUI uses `GUILayout`.
- Don't pull in any UI Toolkit / UGUI dependency. IMGUI only, mirrors calibration.
### Done when
- F2 in Play mode opens the panel; F2 again closes it.
- Sparkline animates within 1s of motion changes.
- JSON saved/restored across Play sessions.
- Commit on Mac1 main: `feat(lume): Wave 8 step 5 — F2 choreography panel`
---
6. Step 6 — Architecture wrap-up (Codex)
6a — `software/demo/unity/lume_pcloud/ARCHITECTURE.md`
Update the doc to match reality.
Changes:
1. Component count in the header summary: 17 → 21 (LumePoseSnapshotter, LumeChoreographyScorer, LumeMotionToAudio, LumeChoreographyPanel are the +4).
2. Execution-order ladder table — add four rows in order:
| 215 | LumeMotionToAudio | bones → OSC out | Wave 8 |
| 220 | LumeTransientForcePusher| (existing) | Wave 5 |
| 225 | LumePoseSnapshotter | beat-locked pose ghosts | Wave 8 |
| 226 | LumeChoreographyScorer | body↔audio coherence score | Wave 8 |`LumeChoreographyPanel` is `OnGUI()`-only; doesn't need an exec-order row but mention it under the panels section.
3. Globals table — add rows:
| Name | Type | Producer | Consumer |
|------|------|----------|----------|
| `_ChoreographyScore` | float | LumeChoreographyScorer | LumeChoreographyPanel |
| `_ChoreographyScoreSmoothed` | float | LumeChoreographyScorer | shader brightness, LumeFluidSim turbulence, LumeMotionToAudio /lume/coherence |
| `_ChoreographyHistory` | StructuredBuffer<float> | LumeChoreographyScorer | LumeChoreographyPanel sparkline |
| `_SnapshotPositions` | StructuredBuffer<float4> | LumePoseSnapshotter | LumePointInstanced.shader (SAMPLE_SNAPSHOTS) |
| `_SnapshotCount` | float | LumePoseSnapshotter | shader |
| `_SnapshotMaxBones` | float | LumePoseSnapshotter | shader |
| `_SnapshotActive` | float | LumePoseSnapshotter | shader |
4. Wave ladder section — add Wave 8 entry with the 4 commits:
Wave 8 — Computational Choreography
8fb6fe75 step 1 mocopi_synth.py + 11 tests
8139a931 step 2 LumePoseSnapshotter + SAMPLE_SNAPSHOTS keyword
<SHA> step 3 LumeChoreographyScorer + APPLY_CHOREOGRAPHY_SCORE
<SHA> step 4 LumeMotionToAudio (OSC :9050)
<SHA> step 5 LumeChoreographyPanel (F2)
<SHA> step 6 Architecture + Auto-Wire5. F-key panel table — add F2:
F1 Duncan preset picker window 8121
F2 Choreography panel window 8122 (Wave 8)
F12 Calibration panel window 81206. Wire formats table — already lists LUME/LUMD/LUMF/LUMM/LUMC. Confirm LUMM is described as Wave 8-active (synthetic + iPad + future Sony pro).
7. Don't-touch list — append:
- Wave 8 wire format magic 0x4C554D4D (LUMM) — pinned by mocopi_synth golden test
- LumeTransientForcePusher.OnTransientFired event — load-bearing for snapshotter
6b — `Assets/Editor/LumeWaveAutoWire.cs`
Add Wave 8 component attach + serialized-slot fill. Pattern: read existing Wave 1+2+3+5 entries, add four parallel cases.
// Wave 8 — Computational Choreography
EnsureComponent<LumeMotionToAudio>(target, c => {
SetSerializedRef(c, "mocopiReceiver", FindOrAttach<LumeMocopiReceiver>(target));
});
EnsureComponent<LumePoseSnapshotter>(target, c => {
SetSerializedRef(c, "transientForcePusher", FindOrAttach<LumeTransientForcePusher>(target));
SetSerializedRef(c, "mocopiReceiver", FindOrAttach<LumeMocopiReceiver>(target));
});
EnsureComponent<LumeChoreographyScorer>(target, c => {
SetSerializedRef(c, "audioFftReceiver", FindOrAttach<LumeAudioFftReceiver>(target));
SetSerializedRef(c, "mocopiReceiver", FindOrAttach<LumeMocopiReceiver>(target));
});
EnsureComponent<LumeChoreographyPanel>(target);Idempotent: re-running the menu must not duplicate components. Use `target.GetComponent<T>() ?? target.AddComponent<T>()` pattern.
Add a verify entry:
LogTickbox("Wave 8: choreography stack",
target.GetComponent<LumeChoreographyScorer>() != null &&
target.GetComponent<LumePoseSnapshotter>() != null &&
target.GetComponent<LumeMotionToAudio>() != null);### .meta files
ARCHITECTURE.md doesn't need a .meta change (Unity ignores .md outside Assets/). LumeWaveAutoWire.cs already has a .meta — leave it alone.
### Done when
- ARCHITECTURE.md component count, ladder, globals, Wave 8 section all match the four shipped commits.
- Auto-Wire menu attaches all 21 components without errors on a fresh GameObject.
- Verify menu prints `[x]` for the new Wave 8 tickbox.
- Commit on Mac1 main: `docs(lume): Wave 8 architecture + auto-wire`
---
7. Verification Checklist (Codex runs after each step)
# 1. Test suite still green (after Steps 4 only — Steps 5/6 don't add Python tests)
cd Desktop/lume-commerce/software/demo
pytest tests/ -q
# Expect: 73 fast green at start. After step 4: ~80 fast green.
# 2. Git state clean
git -C Desktop/lume-commerce status
# Expect: only the new files for the current step.
# 3. Commit landed
git -C Desktop/lume-commerce log -1 --oneline
# Expect: matches "Wave 8 step N" format.
# 4. No accidental edits to don't-touch files
git -C Desktop/lume-commerce diff HEAD~1 HEAD --stat | grep -E "(audio_pub|mocopi_bridge|pointcloud_pub|LumeUdpReceiver|LumeAudioReactor)"
# Expect: empty output (none of these files changed).If any of these fail, DO NOT commit. Investigate and either fix or write a blocker memory file.
---
8. Reading Order for Codex (paste this into your bootstrap)
1. This file: `software/demo/unity/lume_pcloud/CODEX-WAVE8-PLAN.md`
2. Plan source: `[home-path]`
3. Memory state: `[home-path]`
4. Architecture mental model: `software/demo/unity/lume_pcloud/ARCHITECTURE.md`
5. API contracts (read-only):
- `Assets/Scripts/LumeMocopiReceiver.cs` → `TryGetSmoothed`
- `Assets/Scripts/LumeAudioFftReceiver.cs` → `TryGetLatest`
- `Assets/Scripts/LumeTransientForcePusher.cs` → `OnTransientFired` event
- `Assets/Scripts/LumeCalibrationPanel.cs` → IMGUI + reflection-tunable + JSON pattern
6. Step 3 baseline (when Claude commits it): `Assets/Scripts/LumeChoreographyScorer.cs`
---
9. Out of Scope (don't pick up)
- K11 deploy / NSSM / pyorbbecsdk install — separate K11 agent owns this.
- Mac4 verification — separate Mac4 agent owns this after all four commits ship.
- Sony mocopi-pro pairing — blocked on activation code in mail.
- iPad skeleton publisher — already exists and works; Mohamed will use it as-is.
- Wave 9 (marching cubes, Human Kaleidoscope, frozen velocity buffers, pinscreen mesh).
- Pushing to remote — Mohamed pushes manually after Mac4 verification.
- Authoring `Assets/VFX/LumeFlowParticles.vfx` — GUI-only, Mohamed's job.
---
10. Stop / Recovery Conditions
Stop and write a blocker memory if:
- Step 3 commit isn't on main when you start (Codex must wait).
- A test that was previously green starts failing — bisect first, don't paper over.
- A `.meta` GUID collision (Unity will complain on reimport).
- `LumeCalibrationPanel.RegisterReflectedFloat<T>` doesn't exist (Claude may have used a different helper name in Step 3 — read the file before assuming).
- Any Wave 1-7 don't-touch file shows up in your diff.
Blocker memory format:
---
name: LUME Wave 8 Codex Blocker <DATE>
description: Codex stopped on Wave 8 step N — <one-line reason>
type: project
---
# What I tried
...
# What broke
... (file:line, exact error)
# What I need
... (decision Mohamed must make, or unblock from Claude)
# State of the working tree
... (git status output)---
End of plan. Codex: read top to bottom, then start at Section 4 only after Section 1's "Step 3 commit" appears in `git log`. If you must wait, run `pytest software/demo/tests/ -q` once to confirm baseline, then idle.
Promotion Decision
Attach run IDs, datasets, metrics, and reproduction commands.
Source Anchor
lume-commerce/viz/lume-pcloud/CODEX-WAVE8-PLAN.md
Detected Structure
Method · Evaluation · References · Code Anchors · Architecture