diff --git a/crates/engine/src/cmd/setup_more.rs b/crates/engine/src/cmd/setup_more.rs
--- a/crates/engine/src/cmd/setup_more.rs
+++ b/crates/engine/src/cmd/setup_more.rs
@@ -42,13 +42,24 @@ pub fn specs() -> Vec<CommandSpec> {
             always,
             prepare_dpe
         ),
-        cmd!(noundo "setup.hardware", "Hardware...", ["Setup"], None, "{device?, buffer_size?: 16..8192} — audio interface settings (sample rate is the session's)", always, |e, p| engine_settings(e, p)),
-        cmd!(noundo "setup.playback_engine", "Playback Engine...", ["Setup"], None, "{device?, buffer_size?: 16..8192, delay_compensation?: bool}", always, |e, p| {
-            if let Some(v) = p.get("delay_compensation").and_then(Value::as_bool) {
-                e.session_mut().edit.delay_compensation = v;
-            }
-            engine_settings(e, p)
-        }),
+        cmd!(
+            noundo "setup.hardware",
+            "Hardware...",
+            ["Setup"],
+            None,
+            "{device?: output name, buffer_size?: 16..=8192 or null} — output device and buffer. Applied only in the desktop app (Playback Engine window / ui.audio_device), which returns the open device. Headless stores the request and returns applied: false; buffer_size is null when unset. Any integer in range, not rounded to the window's power-of-two sizes. Sample rate is the session's",
+            always,
+            |e, p| engine_settings(e, p, "setup.hardware")
+        ),
+        cmd!(
+            noundo "setup.playback_engine",
+            "Playback Engine...",
+            ["Setup"],
+            None,
+            "{device?: output name, buffer_size?: 16..=8192 or null, delay_compensation?: bool} — device and buffer as setup.hardware (applied only in the desktop app, or via ui.audio_device); delay_compensation is applied here too. Headless returns applied: false and does not invent a buffer",
+            always,
+            |e, p| engine_settings(e, p, "setup.playback_engine")
+        ),
         cmd!(noundo "setup.disk_allocation", "Disk Allocation...", ["Setup"], None, "{path?, track?} — the record folder for the session, or for one track", always, disk_allocation),
         cmd!(noundo "setup.peripherals", "Peripherals...", ["Setup"], None, "{settings?: {name: number|bool|string}} — control surface / sync device settings", always, peripherals),
         cmd!(
@@ -156,18 +167,69 @@ fn prepare_dpe(e: &mut Engine, p: &Value) -> Result<Value> {
     Ok(json!({"prepared": n}))
 }
 
-fn engine_settings(e: &mut Engine, p: &Value) -> Result<Value> {
+/// A `buffer_size` argument: absent, cleared (`null`), or an integer in 16..=8192.
+enum BufferParam {
+    Skip,
+    Clear,
+    Set(u64),
+}
+
+/// Same range as `ui.audio_device`. Out of range and non-numbers error; there is no clamp and no
+/// power-of-two rounding (the Playback Engine window lists those sizes, the command keeps the number).
+fn buffer_param(id: &str, p: &Value) -> Result<BufferParam> {
+    match p.get("buffer_size") {
+        None => Ok(BufferParam::Skip),
+        Some(Value::Null) => Ok(BufferParam::Clear),
+        Some(v) => {
+            let n = v.as_u64().filter(|n| (16..=8192).contains(n)).ok_or_else(|| bad(id, "`buffer_size` must be a number in 16..=8192 or null"))?;
+            Ok(BufferParam::Set(n))
+        }
+    }
+}
+
+/// `device` as `ui.audio_device`'s `output`: a name, or `null`/empty to clear. Absent leaves it.
+fn device_param<'a>(id: &str, p: &'a Value) -> Result<Option<Option<&'a str>>> {
+    match p.get("device") {
+        None => Ok(None),
+        Some(Value::Null) => Ok(Some(None)),
+        Some(Value::String(name)) => Ok(Some((!name.is_empty()).then_some(name.as_str()))),
+        Some(_) => Err(bad(id, "`device` must be a string or null")),
+    }
+}
+
+/// Headless `setup.hardware` / `setup.playback_engine`. Playback never reads `engine.device` or
+/// `engine.buffer_size` (only this command does); they are kept so a later call can report the
+/// request. The result says `applied: false` and leaves `buffer_size` null until one was stored.
+fn engine_settings(e: &mut Engine, p: &Value, id: &str) -> Result<Value> {
+    let buffer = buffer_param(id, p)?;
+    let device = device_param(id, p)?;
+    if id == "setup.playback_engine"
+        && let Some(on) = p.get("delay_compensation").and_then(Value::as_bool)
+    {
+        e.session_mut().edit.delay_compensation = on;
+    }
     let s = e.session_mut();
-    if let Some(b) = p.get("buffer_size").and_then(Value::as_u64) {
-        let b = b.clamp(16, 8192).next_power_of_two().min(8192);
-        s.edit.values.insert("engine.buffer_size".into(), b as f64);
+    match buffer {
+        BufferParam::Skip => {}
+        BufferParam::Clear => {
+            s.edit.values.remove("engine.buffer_size");
+        }
+        BufferParam::Set(n) => {
+            s.edit.values.insert("engine.buffer_size".into(), n as f64);
+        }
     }
-    if let Some(d) = str_param(p, "device") {
-        set_text(&mut s.edit, "engine.device", Some(d));
+    if let Some(name) = device {
+        set_text(&mut s.edit, "engine.device", name);
     }
+    // Exact integers only: a hand-edited fraction is not reported as a buffer size.
+    let buffer_size = s.edit.values.get("engine.buffer_size").copied().and_then(|v| {
+        let n = v as u64;
+        (v.is_finite() && (n as f64) == v).then_some(n)
+    });
     Ok(json!({
+        "applied": false,
         "device": text(&s.edit, "engine.device"),
-        "buffer_size": s.edit.value("engine.buffer_size", 1024.0),
+        "buffer_size": buffer_size,
         "sample_rate": s.sample_rate.hz(),
         "bit_depth": s.bit_depth,
         "delay_compensation": s.edit.delay_compensation,
@@ -587,7 +649,8 @@ mod tests {
     fn engine_and_midi_settings() {
         let mut e = Engine::default();
         let r = e.execute("setup.playback_engine", &json!({"buffer_size": 300, "device": "Interface"})).unwrap();
-        assert_eq!(r["buffer_size"], 512.0);
+        assert_eq!(r["applied"], false);
+        assert_eq!(r["buffer_size"], 300);
         assert_eq!(r["device"], "Interface");
         let r = e.execute("setup.midi_input_filter", &json!({"filter": ["sysex", "bogus"]})).unwrap();
         assert_eq!(r["filtered"], json!(["sysex"]));
@@ -597,6 +660,45 @@ mod tests {
         assert_eq!(r["samples"], 3200.0);
     }
 
+    #[test]
+    fn playback_engine_does_not_pretend_the_device_changed() {
+        let mut e = Engine::default();
+        let r = e.execute("setup.playback_engine", &json!({})).unwrap();
+        assert_eq!(r["applied"], false);
+        assert!(r["buffer_size"].is_null(), "no invented default buffer: {r}");
+        assert!(r["device"].is_null());
+        assert_eq!(r["delay_compensation"], true, "the session default, not a device claim");
+
+        let r = e.execute("setup.hardware", &json!({"buffer_size": 300, "device": "Interface"})).unwrap();
+        assert_eq!(r["applied"], false);
+        assert_eq!(r["buffer_size"], 300, "kept, not rounded to a power of two");
+        assert_eq!(r["device"], "Interface");
+        assert_eq!(e.session().edit.value("engine.buffer_size", -1.0), 300.0);
+
+        let r = e.execute("setup.playback_engine", &json!({"delay_compensation": false})).unwrap();
+        assert_eq!(r["applied"], false);
+        assert_eq!(r["delay_compensation"], false);
+        assert_eq!(r["buffer_size"], 300, "delay compensation does not clear a stored buffer");
+        assert!(!e.session().edit.delay_compensation);
+
+        let err = e.execute("setup.playback_engine", &json!({"buffer_size": 8, "delay_compensation": true, "device": "Other"})).unwrap_err();
+        assert!(err.to_string().contains("16..=8192"), "{err}");
+        assert!(!e.session().edit.delay_compensation, "an invalid buffer changes nothing");
+        assert_eq!(e.session().edit.value("engine.buffer_size", -1.0), 300.0);
+        assert_eq!(text(&e.session().edit, "engine.device").as_deref(), Some("Interface"));
+        assert!(e.execute("setup.hardware", &json!({"buffer_size": "big"})).is_err());
+
+        let err = e.execute("setup.playback_engine", &json!({"device": 42, "delay_compensation": true})).unwrap_err();
+        assert!(err.to_string().contains("`device` must be a string or null"), "{err}");
+        assert!(!e.session().edit.delay_compensation, "an invalid device changes nothing");
+        assert_eq!(text(&e.session().edit, "engine.device").as_deref(), Some("Interface"));
+
+        let r = e.execute("setup.hardware", &json!({"buffer_size": null, "device": ""})).unwrap();
+        assert_eq!(r["applied"], false);
+        assert!(r["buffer_size"].is_null());
+        assert!(r["device"].is_null());
+    }
+
     #[test]
     fn import_session_data_brings_tracks_and_audio() {
         let dir = std::env::temp_dir().join(format!("sc-import-{}", std::process::id()));
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -518,6 +518,11 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
     if id == "ui.audio_device" {
         return Some(crate::windows::audio_device_command(app, p));
     }
+    // Menu clicks never get here: Setup › Playback Engine / Hardware open the window
+    // (`MENU_WINDOWS`). A programmatic call applies the open device instead.
+    if matches!(id, "setup.playback_engine" | "setup.hardware") {
+        return Some(crate::windows::setup_audio_command(app, id, p));
+    }
     let toggle = |v: &mut bool| {
         *v = p.get("value").and_then(Value::as_bool).unwrap_or(!*v);
         json!({"value": *v})
diff --git a/crates/ui-egui/src/windows.rs b/crates/ui-egui/src/windows.rs
--- a/crates/ui-egui/src/windows.rs
+++ b/crates/ui-egui/src/windows.rs
@@ -516,6 +516,8 @@ pub fn playback_engine(app: &mut SoundApp, ctx: &egui::Context) {
             number_combo(ui, "pe-rate", "Sample rate", &mut draft.sample_rate, out.map_or(&[][..], |d| &d.sample_rates), "Hz");
             ui.end_row();
             ui.label("Buffer size");
+            // The combo lists standard power-of-two sizes. `setup.playback_engine` does not round
+            // onto this list: it keeps any integer 16..=8192, the same as `ui.audio_device`.
             let buffers = out.map(DeviceInfo::buffer_sizes).unwrap_or_else(|| soundcraft_playback::devices::STANDARD_BUFFERS.to_vec());
             number_combo(ui, "pe-buffer", "Buffer size", &mut draft.buffer_size, &buffers, "samples");
             ui.end_row();
@@ -622,6 +624,53 @@ pub fn audio_device_command(app: &mut SoundApp, p: &serde_json::Value) -> Result
     Ok(out)
 }
 
+/// Programmatic `setup.playback_engine` / `setup.hardware`. `device` is the output name and
+/// `buffer_size` is any integer 16..=8192 or null (not rounded to the window's power-of-two list);
+/// both go through [`audio_device_command`]. Delay compensation still goes through the engine
+/// command, and only after the device change has been accepted. The result is the open device:
+/// its name, the buffer actually in use (`device_buffer`, null for the device default), its sample
+/// rate, and delay compensation. `applied` is true only when an audio device is open.
+pub fn setup_audio_command(app: &mut SoundApp, id: &str, p: &serde_json::Value) -> Result<serde_json::Value, String> {
+    use serde_json::Value;
+    let mut audio = serde_json::Map::new();
+    if let Some(device) = p.get("device") {
+        if !device.is_null() && !device.is_string() {
+            return Err("`device` must be a string or null".into());
+        }
+        audio.insert("output".to_string(), device.clone());
+    }
+    if let Some(buffer) = p.get("buffer_size") {
+        audio.insert("buffer_size".to_string(), buffer.clone());
+    }
+    if !audio.is_empty() {
+        audio_device_command(app, &Value::Object(audio))?;
+    }
+    if id == "setup.playback_engine"
+        && let Some(on) = p.get("delay_compensation").and_then(Value::as_bool)
+    {
+        app.engine.execute("setup.playback_engine", &json!({"delay_compensation": on})).map_err(|e| e.to_string())?;
+    }
+    let applied = app.player.as_ref().is_some_and(|player| !player.silent);
+    let (device, buffer_size, sample_rate) = match app.player.as_ref() {
+        Some(player) => {
+            let buffer = match player.device_buffer {
+                Some(n) => json!(n),
+                None => Value::Null,
+            };
+            (json!(player.device_name), buffer, json!(player.device_rate))
+        }
+        None => (Value::Null, Value::Null, json!(app.engine.session().sample_rate.hz())),
+    };
+    Ok(json!({
+        "applied": applied,
+        "device": device,
+        "buffer_size": buffer_size,
+        "sample_rate": sample_rate,
+        "bit_depth": app.engine.session().bit_depth,
+        "delay_compensation": app.engine.session().edit.delay_compensation,
+    }))
+}
+
 /// Setup › I/O: busses and output paths.
 pub fn io_setup(app: &mut SoundApp, ctx: &egui::Context) {
     let mut open = app.ui.show_io_setup;
diff --git a/crates/ui-egui/tests/it/audio_device.rs b/crates/ui-egui/tests/it/audio_device.rs
--- a/crates/ui-egui/tests/it/audio_device.rs
+++ b/crates/ui-egui/tests/it/audio_device.rs
@@ -25,3 +25,64 @@ fn the_audio_device_choice_is_kept_only_when_it_is_applied() {
     assert_eq!(r["config"]["buffer_size"], 256);
     assert!(r["device"].is_object(), "{r}");
 }
+
+#[test]
+fn playback_engine_applies_the_same_device_choice_as_the_audio_command() {
+    let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
+    let before = app.ui.audio.clone();
+    let bit_depth = app.engine.session().bit_depth;
+    assert!(!app.ui.show_playback_engine);
+
+    let r = app.run("setup.hardware", json!({})).unwrap();
+    assert_eq!(r["applied"], false, "without a player there is no open device");
+    assert_eq!(r["bit_depth"], json!(bit_depth));
+
+    assert!(app.engine.session().edit.delay_compensation, "delay compensation starts on");
+    let err = app.run("setup.playback_engine", json!({"buffer_size": 8, "delay_compensation": false})).unwrap_err();
+    assert!(err.contains("16..=8192"), "{err}");
+    assert_eq!(app.ui.audio, before, "an invalid buffer changes nothing");
+    assert!(app.engine.session().edit.delay_compensation, "an invalid buffer does not apply delay compensation");
+
+    app.engine.transport.recording = true;
+    let err = app.run("setup.playback_engine", json!({"buffer_size": 256, "delay_compensation": false})).unwrap_err();
+    assert!(err.contains("stop recording"), "{err}");
+    assert_eq!(app.ui.audio, before);
+    assert!(app.engine.session().edit.delay_compensation, "a refused device change does not apply delay compensation");
+    app.engine.transport.recording = false;
+
+    let err = app.run("setup.playback_engine", json!({"device": 42, "delay_compensation": false})).unwrap_err();
+    assert!(err.contains("`device` must be a string or null"), "{err}");
+    assert_eq!(app.ui.audio, before, "an invalid device changes nothing");
+    assert!(app.engine.session().edit.delay_compensation, "an invalid device does not apply delay compensation");
+
+    let r = app.run("setup.playback_engine", json!({"buffer_size": 256, "delay_compensation": false})).unwrap();
+    assert_eq!(app.ui.audio.buffer_size, Some(256));
+    assert!(!app.engine.session().edit.delay_compensation);
+    assert!(!app.ui.show_playback_engine, "a programmatic call does not open the window");
+    assert_eq!(r["delay_compensation"], false);
+    let player = app.player.as_ref().unwrap();
+    assert_eq!(r["applied"], !player.silent);
+    assert_eq!(r["device"], json!(player.device_name));
+    assert_eq!(r["buffer_size"], json!(player.device_buffer));
+    assert_eq!(r["sample_rate"], player.device_rate);
+    assert_eq!(r["bit_depth"], json!(bit_depth));
+
+    // 300 is in range and is not rounded onto the window's power-of-two list.
+    let mut via_window_command = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
+    via_window_command.run("ui.audio_device", json!({"buffer_size": 300, "output": "Elsewhere"})).unwrap();
+    app.run("setup.hardware", json!({"buffer_size": 300, "device": "Elsewhere"})).unwrap();
+    assert_eq!(app.ui.audio.buffer_size, Some(300));
+    assert_eq!(app.ui.audio.buffer_size, via_window_command.ui.audio.buffer_size);
+    assert_eq!(app.ui.audio.output, via_window_command.ui.audio.output);
+    assert!(!app.ui.show_playback_engine);
+
+    app.run("setup.playback_engine", json!({"buffer_size": null, "device": null})).unwrap();
+    assert_eq!(app.ui.audio.buffer_size, None);
+    assert_eq!(app.ui.audio.output, None);
+    assert!(!app.engine.session().edit.delay_compensation, "clearing the device leaves delay compensation");
+
+    let cfg = app.ui.audio.clone();
+    soundcraft_ui_egui::menus::invoke_menu(&mut app, "setup.playback_engine", "Setup > Playback Engine...");
+    assert!(app.ui.show_playback_engine, "the menu still opens the Playback Engine window");
+    assert_eq!(app.ui.audio, cfg, "opening the window does not change the device");
+}
diff --git a/crates/ui-egui/tests/it/command_reachability.rs b/crates/ui-egui/tests/it/command_reachability.rs
--- a/crates/ui-egui/tests/it/command_reachability.rs
+++ b/crates/ui-egui/tests/it/command_reachability.rs
@@ -25,8 +25,6 @@ const KNOWN_UNREACHED: &[&str] = &[
     "mix.solo_safe",
     "options.post_roll",
     "options.pre_roll",
-    "setup.hardware",        // menu opens the Playback Engine window, which doesn't run it
-    "setup.playback_engine", // same
     "setup.reset_edit_state",
     "track.load_preset",
     "track.pin",
@@ -93,6 +91,13 @@ fn every_command_has_a_ui_path() {
     assert!(stale.is_empty(), "now reachable, remove from KNOWN_UNREACHED: {stale:?}");
 }
 
+#[test]
+fn playback_engine_menu_opens_the_window_instead_of_running_the_command() {
+    let extra = menus::ui_aliases();
+    assert_eq!(catalog::implemented_by("Setup > Playback Engine...", &extra).as_deref(), Some("window.playback_engine"));
+    assert_eq!(catalog::implemented_by("Setup > Hardware...", &extra).as_deref(), Some("window.playback_engine"));
+}
+
 #[test]
 fn new_midi_clip_is_in_the_clip_menu() {
     let extra = menus::ui_aliases();
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
 # Where SoundCraft falls short of Pro Tools
 
-> **Last reviewed:** 2026-10-19 · **Last updated:** 2026-10-25 · **Change:** trivial (G-04: Clip Looping crossfade and loop count; previously trivial: G-08 and G-20: built-in plugin count, the sampler shipped) · **Target:** Avid Pro Tools Ultimate 2026.4.1
+> **Last reviewed:** 2026-10-25 · **Last updated:** 2026-10-25 · **Change:** trivial (G-01: `setup.playback_engine` applies the device buffer in the desktop app; G-04: Clip Looping crossfade and loop count) · **Target:** Avid Pro Tools Ultimate 2026.4.1
 
 Every known shortfall, one entry each, ranked by how much it stops a Pro Tools user from doing
 real work in SoundCraft. This is the work list: pick from the top. Rows marked **alpha blocker**
@@ -46,11 +46,16 @@ When you close a gap, delete its entry (or shrink it) in the same PR, and bump t
   device, sample rate and device buffer size (kept with the preferences, per machine; `ui.audio_device`
   for agents); f32 and 16/32-bit integer devices; ASIO behind the opt-in `asio` build feature
   (verified on Windows with an SSL 2+ at 128 samples).
+- **Done (2026-10-25):** `setup.playback_engine` and `setup.hardware` apply `device` (the output
+  name) and `buffer_size` in the desktop app on the same path as the Playback Engine window
+  (`ui.audio_device`) and return the open device. `buffer_size` is any integer 16..=8192 and is
+  not rounded onto the window's power-of-two list. Headless (CLI, no app) still stores the request
+  and returns `applied: false`, with `buffer_size` null when unset; playback does not read those
+  session values.
 - **Missing:** routing to physical channels (I/O Setup), one duplex stream on one clock (input and
   output are still separate cpal streams), latency reporting and record-offset compensation, ASIO
   in official builds (owner decision: Steinberg's SDK is proprietary or GPLv3), JACK, and
   verification on macOS and Linux. The mix still runs in fixed 512-sample blocks.
-  `setup.playback_engine` still stores `device`/`buffer_size` in the session, unused.
 - **Evidence:** code above; Playback Engine window says "Mix block size: 512 samples"; issue #42
   (no input device selection, forced monitoring feedback with virtual routing).
 - **Impact:** blocks tracking with any multichannel interface; latency makes overdubs hard.
@@ -226,6 +231,7 @@ missing. Out of scope unless the owner decides otherwise.
 | Date | Change | Summary |
 |---|---|---|
 | 2026-10-25 | trivial | G-04: Clip Looping (#1030) lacks the loop crossfade option, and Number of Loops stops at 512 while length and fill make up to 1000 loops |
+| 2026-10-25 | trivial | G-01: `setup.playback_engine` and `setup.hardware` apply the device and buffer in the desktop app; headless returns `applied: false` |
 | 2026-10-24 | trivial | G-08: 27 processors + 3 instruments, as the plugin registry holds; G-20: the sampler shipped (#108), so it is no longer listed as missing |
 | 2026-10-19 | trivial | G-04: Float32 encoding retains finite headroom (#791); integer clipping and non-finite handling retained |
 | 2026-10-18 | trivial | G-04: mono device stereo/surround fold-down fixed and regression-tested; estimates unchanged |
diff --git a/docs/roadmap.md b/docs/roadmap.md
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
 # SoundCraft roadmap: milestones and what's next
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** trivial (built-in plugin count; the sampler shipped) · **Target:** Avid Pro Tools Ultimate 2026.4.1
+> **Last reviewed:** 2026-10-25 · **Last updated:** 2026-10-25 · **Change:** trivial (`setup.playback_engine` applies the device buffer in the desktop app) · **Target:** Avid Pro Tools Ultimate 2026.4.1
 
 Forward-looking plan. Status and numbers: [`../ROADMAP.md`](../ROADMAP.md) and
 [`target-app-parity.md`](target-app-parity.md); the ranked work list: [`gaps.md`](gaps.md).
@@ -23,7 +23,7 @@ main platform). Any **no** keeps SoundCraft **pre-alpha**, and those rows are th
 
 | Workflow | Works end to end? | Evidence | Hours to pass |
 |---|---|---|---:|
-| Record audio from a chosen interface at a chosen buffer size, monitor, punch, keep the takes | **no** | Default output and input devices only. Fixed 512-sample block. Separate input and output streams. `setup.playback_engine` params ignored by playback. Takes held in RAM until stop (PR #84). No input device choice (#42). | 35–55 |
+| Record audio from a chosen interface at a chosen buffer size, monitor, punch, keep the takes | **no** | Device, rate and buffer are chosen in the Playback Engine window and, in the desktop app, by `setup.playback_engine` (headless returns `applied: false`). The mix still runs in fixed 512-sample blocks. Separate input and output streams. Takes held in RAM until stop (PR #84). | 35–55 |
 | Record MIDI from a controller and play it back through an instrument | **no** | Playback of drawn and imported MIDI works. No MIDI device layer at all, so nothing can be recorded and instruments can't be played live (G-02). | 20–35 |
 | Edit: select, trim, fade, comp playlists, Slip/Shuffle/Grid | yes (rough) | Every Edit menu command; property-tested with undo; open bugs #3, #5, #140 don't block the workflow | — |
 | Mix with built-in and third-party plugins, sends, busses, automation | partial (passes on CLAP/AU) | 27 built-in processors; CLAP and AU host on the signed macOS build, but its missing entitlement blocks third-party VST3 (#153). No AU plugin windows yet; VST3 windows on macOS only | 1–2 (entitlement) |
@@ -89,6 +89,7 @@ There is no `cargo xtask scorecard` in this repo; the measured number is `cargo
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-25 | trivial | Record-workflow evidence: `setup.playback_engine` applies the device and buffer in the desktop app; headless returns `applied: false` |
 | 2026-10-24 | trivial | 27 built-in processors, as the plugin registry holds; the sampler (#108) is no longer listed under plugin essentials |
 | 2026-10-14 | trivial | M8: reject oversized MCP batches before execution (#460) |
 | 2026-10-10 | minor | Added the alpha gate (3 of 6 core workflows fail → pre-alpha) |
