---
phase: 07
plan: 05
type: execute
wave: 4
depends_on: [07-04]
files_modified:
  - driver/src/http_server.hpp
  - driver/src/http_server.cpp
  - driver/src/device_provider.cpp
  - src/steamvr/include/micmap/steamvr/vr_input.hpp
  - src/steamvr/src/vr_input.cpp
  - apps/micmap/main.cpp
autonomous: true
requirements: [MIG-02]
must_haves:
  truths:
    - "HttpServer ctor gains a 4th param `std::function<bool()> driverDetectionActiveGetter = nullptr` (nullable so test code can construct without DeviceProvider) and stores it as a private member (D-09, RESEARCH Open Question 1 recommendation a)"
    - "HttpServer's `GET /health` route returns nlohmann::json body with `status` (string \"healthy\") AND `driver_detection_active` (boolean) fields (D-09)"
    - "DeviceProvider constructs HttpServer with a getter lambda `[this]{ return driverDetectionEnabled_ && audioWorker_ && detectionRunner_ && detectionRunner_->IsRunning(); }` so /health reflects the actual live state (D-09)"
    - "IDriverClient gains a new method `bool isDriverDetectionActive()` that polls `GET /health` and parses the field; result cached for 1 second to keep onTrigger latency bounded (D-10)"
    - "MicMapApp::onTrigger checks `driverClient->isDriverDetectionActive()` BEFORE calling `driverClient->tap()`; when true, emits debug log `onTrigger: driver_detection_active=true, suppressing` and returns without tapping (D-10)"
    - "State machine cooldown is the belt-and-suspenders backstop for transient double-tap during /health-poll lag — already enforced in v1.5 client + 07-03 driver state machine (D-11)"
    - "When `driver_detection_active=false` is observed (driver flag off, driver crashed/restarted, or not yet polled since startup), client resumes its own trigger path — no restart required (D-10)"
    - "`curl http://127.0.0.1:27015/health` returns JSON with both fields parseable by `jq`"
  artifacts:
    - path: "driver/src/http_server.hpp"
      provides: "HttpServer ctor 4th param driverDetectionActiveGetter; private member to hold it"
      contains: "driverDetectionActiveGetter"
    - path: "driver/src/http_server.cpp"
      provides: "/health route emits nlohmann::json with status + driver_detection_active fields"
      contains: "driver_detection_active"
    - path: "driver/src/device_provider.cpp"
      provides: "HttpServer constructed with getter lambda capturing detectionRunner_ + audioWorker_ + driverDetectionEnabled_"
      contains: "driverDetectionActiveGetter"
    - path: "src/steamvr/include/micmap/steamvr/vr_input.hpp"
      provides: "IDriverClient::isDriverDetectionActive declaration"
      contains: "isDriverDetectionActive"
    - path: "src/steamvr/src/vr_input.cpp"
      provides: "DriverClient::isDriverDetectionActive impl — polls /health, parses field, caches 1s"
      contains: "driver_detection_active"
    - path: "apps/micmap/main.cpp"
      provides: "onTrigger gates IDriverClient::tap() on isDriverDetectionActive()"
      contains: "isDriverDetectionActive"
  key_links:
    - from: "driver/src/http_server.cpp /health handler"
      to: "DeviceProvider's lambda capturing detectionRunner_ state"
      via: "driverDetectionActiveGetter_() invocation per request"
      pattern: "driverDetectionActiveGetter_\\(\\)"
    - from: "apps/micmap/main.cpp MicMapApp::onTrigger"
      to: "src/steamvr/src/vr_input.cpp DriverClient::isDriverDetectionActive"
      via: "driverClient->isDriverDetectionActive() before tap()"
      pattern: "isDriverDetectionActive"
---

<objective>
Wave 4 — ship the migration handshake. This plan extends the driver's `GET /health` JSON to include a `driver_detection_active` boolean field (D-09), wires DeviceProvider to pass a getter lambda at HttpServer construction (RESEARCH Open Question 1 recommendation a), adds an `isDriverDetectionActive()` method to `IDriverClient` (with a 1-second cache to keep onTrigger's latency budget bounded), and gates `MicMapApp::onTrigger`'s `IDriverClient::tap()` call on the field (D-10). State machine cooldown remains the belt-and-suspenders backstop for any /health-poll-lag double-tap window (D-11).

Purpose: this is the migration handshake that allows v1.5's client-side trigger pipeline to coexist with v1.6's in-process driver-side trigger pipeline during the P7→P10 phased migration. With both flags ON, the client polls /health and stands down its own trigger; with either flag OFF (or driver crashed), the client resumes its own trigger automatically — no restart required. P10 deletes this entire scaffolding (POST /button, IDriverClient::tap, the field, and the suppression dance) per D-12.

Output: HttpServer + DeviceProvider extensions on the driver side; IDriverClient + DriverClient extensions on the client SDK side; MicMapApp::onTrigger guard on the client app side. Six files, all surgical.
</objective>

<execution_context>
@C:/Users/decid/.ccs/instances/bigscreen/get-shit-done/workflows/execute-plan.md
@C:/Users/decid/.ccs/instances/bigscreen/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/STATE.md
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/phases/07-driver-side-detection-thread/07-CONTEXT.md
@.planning/phases/07-driver-side-detection-thread/07-RESEARCH.md
@.planning/phases/07-driver-side-detection-thread/07-PATTERNS.md
@.planning/phases/07-driver-side-detection-thread/07-04-PLAN.md
@.planning/research/PITFALLS.md
@./CLAUDE.md
@driver/src/http_server.hpp
@driver/src/http_server.cpp
@driver/src/device_provider.hpp
@driver/src/device_provider.cpp
@driver/src/detection_runner.hpp
@src/steamvr/include/micmap/steamvr/vr_input.hpp
@src/steamvr/src/vr_input.cpp
@apps/micmap/main.cpp

<interfaces>
<!-- All snippets MUST be applied as-is. From 07-PATTERNS.md and 07-RESEARCH.md. -->

# 1. http_server.hpp — append 4th ctor param + private member.

```cpp
// Add near the top of http_server.hpp (replacing/extending current includes):
#include <functional>

// Modify the constructor signature at line 44-46:
explicit HttpServer(CommandQueue& queue,
                    int port = 27015,
                    const std::string& host = "127.0.0.1",
                    std::function<bool()> driverDetectionActiveGetter = nullptr);

// Add private member after the existing `running_` atomic:
std::function<bool()> driverDetectionActiveGetter_;
```

# 2. http_server.cpp — store the getter; extend /health route.

```cpp
// Update the constructor impl (lines 26-32) to take + store the getter:
HttpServer::HttpServer(CommandQueue& queue, int port, const std::string& host,
                       std::function<bool()> driverDetectionActiveGetter)
    : queue_(queue)
    , port_(port)
    , host_(host)
    , driverDetectionActiveGetter_(std::move(driverDetectionActiveGetter))
{
    DriverLog("HttpServer created (host: %s, port: %d)\n", host_.c_str(), port_);
}
```

```cpp
// Update SetupRoutes() — replace the existing /health handler at lines 154-157:
// GET /health — liveness + port-probe endpoint (used by DriverClient).
// P7 D-09: emit driver_detection_active so the client can suppress its own
// trigger when the driver owns the detection path. Field is true iff the
// getter (set by DeviceProvider) reports {flag enabled AND audio worker alive
// AND detection runner alive AND running}.
server_->Get("/health", [this](const httplib::Request&, httplib::Response& res) {
    nlohmann::json body;
    body["status"] = "healthy";
    body["driver_detection_active"] =
        driverDetectionActiveGetter_ ? driverDetectionActiveGetter_() : false;
    res.set_content(body.dump(), "application/json");
});
```

# Note: nlohmann/json is ALREADY included in http_server.cpp (used by POST /button).
# No new include needed.

# 3. device_provider.cpp — pass the getter at HttpServer construction.
# Find the existing HttpServer construction (search for `make_unique<HttpServer>`).
# Wrap the new lambda capture into the ctor call.

```cpp
// Existing P5/v1.5 HttpServer construction probably looks like:
//   httpServer_ = std::make_unique<HttpServer>(*commandQueue_, port, host);
// Replace with:
httpServer_ = std::make_unique<HttpServer>(
    *commandQueue_,
    port,                                    // existing port arg if any
    host,                                    // existing host arg if any
    /*driverDetectionActiveGetter=*/[this]() {
        // P7 D-09: true iff driver actually owns the detection path.
        return driverDetectionEnabled_
            && audioWorker_
            && detectionRunner_
            && detectionRunner_->IsRunning();
    });
```

# IMPORTANT: HttpServer is constructed BEFORE the VRSettings reads in current Init flow
# (per device_provider.cpp:80 the order is: VR_INIT, PatchGenericHmdBindings, commandQueue
# construction, httpServer_->Start(), THEN settings reads + audio + detection).
# So at HttpServer construction time, driverDetectionEnabled_ / audioWorker_ /
# detectionRunner_ ARE ALL FALSE/NULL. The lambda captures `this` and reads the members
# AT REQUEST TIME — so the field will be false until detection actually constructs and
# starts (which happens later in Init), then flip to true on the next /health poll.
# This is correct behavior — clients see false during the brief Init window, then true.

# 4. src/steamvr/include/micmap/steamvr/vr_input.hpp — add isDriverDetectionActive method.
# Locate IDriverClient interface (lines 170-186 per PATTERNS.md):

```cpp
class IDriverClient {
public:
    virtual ~IDriverClient() = default;
    // ... existing methods (connect, tap, isConnected, getStatus, getLastError) ...

    /// P7 D-10: returns true iff the driver reports `driver_detection_active=true`
    /// in its GET /health response. Cached for ~1 second to keep onTrigger latency
    /// bounded (Pitfall 10 mitigation — client suppresses its own tap when driver
    /// owns the detection path).
    /// Deleted in P10 (D-12) along with tap() and the entire trigger-coexistence
    /// scaffolding.
    virtual bool isDriverDetectionActive() = 0;
};
```

# 5. src/steamvr/src/vr_input.cpp — DriverClient::isDriverDetectionActive impl.
# Mirror the existing `connect()` /health probe at vr_input.cpp:142-148.

```cpp
// Add a private cache member to DriverClient (in vr_input.hpp, the concrete impl):
//   std::chrono::steady_clock::time_point lastDetectionPoll_{};
//   bool                                   cachedDetectionActive_{false};
//   constexpr static std::chrono::milliseconds kDetectionCacheTtl_{1000};

// Impl in vr_input.cpp:
bool DriverClient::isDriverDetectionActive() {
    using clock = std::chrono::steady_clock;
    const auto now = clock::now();
    if (now - lastDetectionPoll_ < std::chrono::milliseconds(1000)) {
        return cachedDetectionActive_;
    }
    if (!connected_ || port_ == 0) return false;

    httplib::Client client(host_, port_);
    client.set_connection_timeout(2);
    client.set_read_timeout(2);
    auto res = client.Get("/health");
    if (!res || res->status != 200) {
        cachedDetectionActive_ = false;
        lastDetectionPoll_ = now;
        return false;
    }
    try {
        auto body = nlohmann::json::parse(res->body);
        cachedDetectionActive_ = body.value("driver_detection_active", false);
    } catch (const nlohmann::json::exception&) {
        cachedDetectionActive_ = false;
    }
    lastDetectionPoll_ = now;
    return cachedDetectionActive_;
}
```

# 6. apps/micmap/main.cpp — gate onTrigger on isDriverDetectionActive (D-10).
# Existing onTrigger at lines 515-523 per PATTERNS.md.

```cpp
void MicMapApp::onTrigger() {
    if (!driverClient || !driverClient->isConnected()) {
        MICMAP_LOG_DEBUG("onTrigger: driver not connected, skipping");
        return;
    }
    // P7 D-10: suppress local trigger when driver owns the detection path.
    // State machine cooldown is the belt-and-suspenders backstop per D-11.
    if (driverClient->isDriverDetectionActive()) {
        MICMAP_LOG_DEBUG("onTrigger: driver_detection_active=true, suppressing");
        return;
    }
    if (!driverClient->tap()) {
        MICMAP_LOG_WARNING("onTrigger failed: ", driverClient->getLastError());
    }
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Extend HttpServer + DeviceProvider — /health JSON gains driver_detection_active field (D-09)</name>
  <files>
    driver/src/http_server.hpp,
    driver/src/http_server.cpp,
    driver/src/device_provider.cpp
  </files>
  <read_first>
    - driver/src/http_server.hpp (full file — current ctor signature at lines 44-46; private members)
    - driver/src/http_server.cpp (full file — ctor at lines 26-32; SetupRoutes /health at 154-157; existing nlohmann::json usage at lines 130-150 for POST /button proves the include is already in place)
    - driver/src/device_provider.cpp (full file — locate HttpServer construction site; pre-07-04 it was constructed as `std::make_unique<HttpServer>(*commandQueue_, port, host)` somewhere in Init step 4 area)
    - driver/src/detection_runner.hpp (07-03 — IsRunning() accessor used in the getter lambda)
    - .planning/phases/07-driver-side-detection-thread/07-PATTERNS.md §"`driver/src/http_server.cpp` MODIFIED — `/health` JSON extension (D-09)" — exact rewrite of the route handler
    - .planning/phases/07-driver-side-detection-thread/07-RESEARCH.md §"`/health` JSON extension (D-09)" + Open Question 1 recommendation
    - .planning/phases/07-driver-side-detection-thread/07-CONTEXT.md (D-09 — field semantics; D-10 — client side; D-12 — P10 deletion)
  </read_first>
  <behavior>
    - HttpServer ctor accepts a 4th optional `std::function<bool()>` arg; default `nullptr` keeps existing call sites compiling (P6 carryover preserved).
    - GET /health response is now valid nlohmann::json with two top-level keys: `status` (string) and `driver_detection_active` (boolean).
    - When DeviceProvider constructs HttpServer with the lambda getter, /health responses reflect live driver state: false during Init before detection starts; true once detection is running; false again after Cleanup.
    - When HttpServer is constructed without the getter (test code, or legacy v1.5 callers), the field is hardcoded to false (defensive null-check on the std::function).
    - `curl http://127.0.0.1:27015/health | jq .driver_detection_active` returns `true` or `false` boolean (not string).
    - Existing `POST /button` route remains byte-functionally identical — the v1.5 trigger path is unaffected.
  </behavior>
  <action>
**Step A — `driver/src/http_server.hpp`.**
1. Add `#include <functional>` near the existing includes.
2. Modify the constructor signature at lines 44-46 to add the 4th param `std::function<bool()> driverDetectionActiveGetter = nullptr`. Default value of `nullptr` keeps existing call sites compiling.
3. Add a private member after the existing `std::atomic<bool> running_{false};` member:
   ```cpp
   std::function<bool()> driverDetectionActiveGetter_;
   ```
4. Update the doxygen comment block above the class (lines 27-35) to mention the new `driver_detection_active` field on /health.

**Step B — `driver/src/http_server.cpp`.**
1. Update the constructor impl (lines 26-32) to take + store the new param via `std::move`. Existing log line preserved verbatim.
2. Replace the `/health` route handler at lines 154-157 with the body from `<interfaces>` block §2:
   - Captures `[this]` (so the getter is reachable).
   - Constructs `nlohmann::json body;`, sets `body["status"] = "healthy";` and `body["driver_detection_active"] = driverDetectionActiveGetter_ ? driverDetectionActiveGetter_() : false;`.
   - `res.set_content(body.dump(), "application/json");`
3. NO new includes needed — `nlohmann/json.hpp` is already included for the POST /button handler.

**Step C — `driver/src/device_provider.cpp`.**
1. Locate the existing HttpServer construction site in DeviceProvider::Init. Pre-07 it looks like `httpServer_ = std::make_unique<HttpServer>(*commandQueue_, /*port=*/..., /*host=*/...);`.
2. Add a 4th argument: a lambda `[this]() { return driverDetectionEnabled_ && audioWorker_ && detectionRunner_ && detectionRunner_->IsRunning(); }`.
3. RECOMMEND: extract the getter into a named local `auto detectionGetter = [this]() { ... };` for readability if the construction line gets long.
4. The lambda captures `this` and reads members AT REQUEST TIME — so it correctly reflects the live state even though HttpServer is constructed BEFORE the VRSettings reads + DetectionRunner construction (D-19 ordering). During the brief Init window between HttpServer::Start and DetectionRunner::Start, the field will report false; once DetectionRunner is alive + running, the next /health poll flips it to true.

CRITICAL constraints:
1. The HttpServer ctor's 4th param MUST default to `nullptr` so any test code (07-01's tests if they ever construct HttpServer directly, though they don't currently) and any other call site continues to compile without changes.
2. The getter lambda MUST capture `this` and read the live members — DO NOT cache a snapshot at HttpServer construction time. The whole point is that the field flips true → false → true as the driver transitions through its lifecycle.
3. The getter lambda MUST be safe to call from the HTTP server thread (not just RunFrame). Reading the members `driverDetectionEnabled_` (bool), `audioWorker_` (unique_ptr), `detectionRunner_` (unique_ptr), and `detectionRunner_->IsRunning()` (atomic load) is safe IF Cleanup runs the unique_ptr resets ATOMICALLY (which they are — unique_ptr::reset is safe at the destructor level, but the read-then-call sequence is NOT atomic). For P7 we accept the small window where HttpServer thread might read `detectionRunner_` non-null AFTER `detectionRunner_.reset()` has flipped it to nullptr but BEFORE the destructor body completes — this is the Pitfall 4 / D-20 reverse-order Cleanup window. The HttpServer is stopped FIRST in P6/v1.5 Cleanup before detectionRunner_.reset()? Actually NO — per D-20, the order is: detectionRunner_.reset() FIRST, audioWorker_.reset() SECOND, then httpServer_->Stop(). So during the detectionRunner reset, the HttpServer is still running and could fire the lambda. The unique_ptr `detectionRunner_` itself is the variable being read — since Cleanup runs on the vrserver thread and HttpServer's lambda runs on its own httplib thread, there IS a race window. Mitigation: the lambda reads `detectionRunner_.get()` (operator-> + IsRunning()) — if `.get()` returns non-null and we call `IsRunning()` on a partially-destructed object, we'd UAF. **Practical mitigation for P7:** reverse the Cleanup order for HTTP — actually no, D-20 is locked. Instead, accept the small window per the F-07-04 threat acceptance below. SC4 stress test does NOT exercise concurrent /health polling during Cleanup, so the test won't fire the race. Document in commit message.

   **Better practical mitigation:** `detectionRunner_` is a `std::unique_ptr`; the `.get()` call returns a `DetectionRunner*` pointer. The `unique_ptr::reset()` operation is NOT atomic (no acquire/release). **However:** in practice the HTTP server thread is bound to localhost and only fires on incoming requests — during Cleanup, no client is making /health requests because vrserver is shutting down. The race window is theoretical, not practical. P10's deletion of the entire field + scaffolding closes the window permanently.

   For this plan: just write the simple lambda. Document the race in the threat model. Accept it.

After authoring, run:
- `cmake --build build --config Release --target driver_micmap`.
- `cmake --build build --config Release --target micmap` (client unaffected by HttpServer changes).
- Manual smoke test: start vrserver with both flags ON, run `curl http://127.0.0.1:27015/health` and confirm the JSON contains `"driver_detection_active": true` (deferred to 07-06 UAT D-25(1)).
- Acceptance via static checks below.
  </action>
  <verify>
    <automated>cmake --build C:/Users/decid/Documents/projects/mic-map/build --config Release --target driver_micmap</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "#include <functional>" driver/src/http_server.hpp` >= 1.
    - `grep -c "std::function<bool()>" driver/src/http_server.hpp` >= 2 (param + member).
    - `grep -c "driverDetectionActiveGetter" driver/src/http_server.hpp` >= 2 (ctor param name + member name).
    - `grep -c "driverDetectionActiveGetter_" driver/src/http_server.cpp` >= 2 (ctor init + use in /health handler).
    - `grep -c "driver_detection_active" driver/src/http_server.cpp` >= 1 (the JSON field name).
    - `grep -c "nlohmann::json body" driver/src/http_server.cpp` >= 1 (the /health body construction).
    - `grep -c "body\\[.status.\\]\\s*=\\s*.healthy." driver/src/http_server.cpp` >= 1 (or whitespace-tolerant variant of `body["status"] = "healthy"`).
    - `grep -c "driverDetectionActiveGetter\\b" driver/src/device_provider.cpp` >= 1 (lambda passed at construction).
    - `grep -c "detectionRunner_->IsRunning()" driver/src/device_provider.cpp` >= 1 (lambda body).
    - `grep -c "driverDetectionEnabled_ && audioWorker_ && detectionRunner_" driver/src/device_provider.cpp` >= 1 OR whitespace-tolerant variant (the AND-chain in the lambda body).
    - cmake configure exits 0; `cmake --build build --config Release --target driver_micmap` exits 0.
    - `cmake --build build --config Release --target micmap` exits 0 (client unaffected by HttpServer changes — no recompile cascade beyond this driver TU).
    - All P5/P6/07-01..07-04 ctest carryovers stay green:
      - AssertNoOpenVRInCore, lint_no_openvr_in_core, lint_no_driver_macro, AssertAudioWorkerNoVrApi, AssertDetectionRunnerNoVrApi, AudioWorkerLifecycleHeadless, DetectionSettingsPropagation, DeviceProviderLifecycleStress, test_command_queue.
    - dumpbin /exports of `driver_micmap.dll` shows ONLY `HmdDriverFactory` (no new exported symbols from /health changes).
  </acceptance_criteria>
  <done>HttpServer constructor takes a getter lambda that DeviceProvider passes capturing the live detection state. /health responses now include `driver_detection_active` boolean. Existing POST /button path unaffected. v1.5 trigger path stays alive.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Extend IDriverClient + MicMapApp::onTrigger — /health poll suppression (D-10, D-11)</name>
  <files>
    src/steamvr/include/micmap/steamvr/vr_input.hpp,
    src/steamvr/src/vr_input.cpp,
    apps/micmap/main.cpp
  </files>
  <read_first>
    - src/steamvr/include/micmap/steamvr/vr_input.hpp full file (IDriverClient interface, DriverClient concrete impl)
    - src/steamvr/src/vr_input.cpp lines 120-210 (DriverClient impl — connect, tap, /health probe pattern at lines 142-148)
    - apps/micmap/main.cpp lines 510-525 (MicMapApp::onTrigger — current implementation)
    - .planning/phases/07-driver-side-detection-thread/07-PATTERNS.md §"`apps/micmap/main.cpp` MODIFIED — `/health` poll suppression (D-10)" — exact onTrigger diff
    - .planning/phases/07-driver-side-detection-thread/07-RESEARCH.md §"Open Questions" #1 (cache TTL recommendation; getter shape)
    - .planning/phases/07-driver-side-detection-thread/07-CONTEXT.md (D-10 — client suppression; D-11 — cooldown backstop; D-12 — P10 deletion)
    - .planning/research/PITFALLS.md §"Pitfall 10" (double-trigger during phased migration)
  </read_first>
  <behavior>
    - IDriverClient gains pure-virtual `bool isDriverDetectionActive() = 0;`.
    - DriverClient (concrete impl) implements isDriverDetectionActive: polls `GET /health`, parses `driver_detection_active` field via nlohmann::json, caches result for 1 second (`std::chrono::steady_clock` based TTL).
    - The first call to isDriverDetectionActive after construction polls /health; subsequent calls within 1s return the cached value.
    - When the driver is not connected (`connected_=false` or `port_=0`), isDriverDetectionActive returns false without polling (no trigger suppression on disconnect — defensive default).
    - When /health returns non-200 status or the JSON doesn't contain `driver_detection_active`, the cached value flips to false and the cache TTL refreshes — so a temporary driver hiccup is treated as "driver doesn't own detection".
    - MicMapApp::onTrigger checks isDriverDetectionActive BEFORE tap(); when true, emits debug log `onTrigger: driver_detection_active=true, suppressing` and returns without calling tap().
    - State machine cooldown (already enforced in v1.5 client + 07-03 driver) handles the transient double-tap window in the /health-poll lag (D-11).
    - When the driver flag is mid-session disabled (or driver crashes/restarts), the next /health poll within 1 second flips cachedDetectionActive_=false; client resumes its own trigger automatically — no restart required (D-10 behavioral guarantee).
    - Client builds (`cmake --build build --config Release --target micmap`); existing client tests stay green.
  </behavior>
  <action>
**Step A — `src/steamvr/include/micmap/steamvr/vr_input.hpp`.**
1. In the IDriverClient interface (located by reading the file — likely lines 170-186), add a new pure-virtual method `virtual bool isDriverDetectionActive() = 0;` per `<interfaces>` block §4. Add a doc-comment block per the interfaces block (P7 D-10 reference + P10 deletion note D-12).
2. In the DriverClient concrete impl class (likely in the same header — read it to confirm location), add the override declaration `bool isDriverDetectionActive() override;` AND new private members:
   - `std::chrono::steady_clock::time_point lastDetectionPoll_{};`
   - `bool cachedDetectionActive_{false};`
3. Add `#include <chrono>` near the existing includes if not already there.

**Step B — `src/steamvr/src/vr_input.cpp`.**
1. Add `#include <chrono>` if not already there.
2. Implement `bool DriverClient::isDriverDetectionActive()` per `<interfaces>` block §5:
   - Cache TTL = 1000 ms via `std::chrono::steady_clock`.
   - Early-return cached value if within TTL.
   - Early-return false if `!connected_ || port_ == 0`.
   - `httplib::Client client(host_, port_); client.set_connection_timeout(2); client.set_read_timeout(2);`.
   - `auto res = client.Get("/health");`; on non-200 or null, set cached=false + refresh timestamp + return false.
   - Try-parse `nlohmann::json::parse(res->body)`; `cachedDetectionActive_ = body.value("driver_detection_active", false);` — defensive default if field missing.
   - On parse exception, cached=false.
   - Refresh timestamp; return cachedDetectionActive_.
3. Place the implementation NEAR the existing connect() impl (around vr_input.cpp:120-210) for source-locality. Match the existing httplib usage pattern at lines 142-148.

**Step C — `apps/micmap/main.cpp`.**
1. Locate `MicMapApp::onTrigger()` around lines 510-525.
2. INSERT the suppression check per `<interfaces>` block §6 — between the existing `if (!driverClient || !driverClient->isConnected()) { ... return; }` block AND the `driverClient->tap()` call.
3. NEW block (add it):
   ```cpp
   // P7 D-10: suppress local trigger when driver owns the detection path.
   // State machine cooldown is the belt-and-suspenders backstop per D-11.
   if (driverClient->isDriverDetectionActive()) {
       MICMAP_LOG_DEBUG("onTrigger: driver_detection_active=true, suppressing");
       return;
   }
   ```
4. The existing `if (!driverClient->tap()) { MICMAP_LOG_WARNING("onTrigger failed: ", driverClient->getLastError()); }` line stays UNCHANGED.

CRITICAL constraints:
1. The cache TTL MUST be 1 second (`std::chrono::milliseconds(1000)`). Shorter wastes CPU on repeated polls; longer extends the double-tap window past acceptable.
2. `isDriverDetectionActive` MUST return `false` defensively when not connected, when /health is unreachable, when the field is missing, or when JSON parse fails. The principle: if we can't determine that the driver actively owns detection, we DON'T suppress — the client falls back to its own trigger path. Pitfall 10 / D-11 cooldown backstop handles any double-tap.
3. The new method MUST be PURE VIRTUAL on IDriverClient (so any future stub IDriverClient impl in tests must provide it). Existing test stubs (if any) MUST be updated — search for `class .*IDriverClient` derived classes.
4. The lifecycle of `lastDetectionPoll_` and `cachedDetectionActive_`: both are member variables of DriverClient (concrete impl), default-initialized in the class definition. NO explicit init required.
5. The implementation MUST NOT introduce a new third-party dep — `httplib` and `nlohmann::json` are already linked by the client (they exist in vr_input.cpp for connect()).
6. The MICMAP_LOG_DEBUG line text MUST be EXACTLY `"onTrigger: driver_detection_active=true, suppressing"` (used by 07-09 UAT D-25(6) coexistence verification log-grep).

After authoring, run:
- `cmake --build build --config Release --target micmap` (client builds).
- `cmake --build build --config Release` (full build — no regressions).
- Acceptance via static checks below.
- Defer real-driver `/health` integration test to 07-06 UAT D-25(1)/(6).
  </action>
  <verify>
    <automated>cmake --build C:/Users/decid/Documents/projects/mic-map/build --config Release --target micmap</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "isDriverDetectionActive" src/steamvr/include/micmap/steamvr/vr_input.hpp` >= 2 (interface + concrete decl).
    - `grep -c "virtual bool isDriverDetectionActive" src/steamvr/include/micmap/steamvr/vr_input.hpp` >= 1 (pure-virtual on interface).
    - `grep -c "= 0;" src/steamvr/include/micmap/steamvr/vr_input.hpp` >= 1 (pure-virtual marker on the new method — count may match other pure-virtuals; the new line specifically must end in `= 0;`).
    - `grep -c "lastDetectionPoll_" src/steamvr/include/micmap/steamvr/vr_input.hpp` >= 1 (cache timestamp member).
    - `grep -c "cachedDetectionActive_" src/steamvr/include/micmap/steamvr/vr_input.hpp` >= 1 (cache value member).
    - `grep -c "DriverClient::isDriverDetectionActive" src/steamvr/src/vr_input.cpp` >= 1 (impl).
    - `grep -c "driver_detection_active" src/steamvr/src/vr_input.cpp` >= 1 (the JSON field name parsed).
    - `grep -c "std::chrono::milliseconds(1000)" src/steamvr/src/vr_input.cpp` >= 1 (the cache TTL).
    - `grep -c "nlohmann::json::parse" src/steamvr/src/vr_input.cpp` >= 1 (parsing /health body).
    - `grep -c "client.Get(\"/health\")" src/steamvr/src/vr_input.cpp` >= 2 (existing connect probe + new isDriverDetectionActive probe — matches Patterns analog at vr_input.cpp:142).
    - `grep -c "isDriverDetectionActive" apps/micmap/main.cpp` >= 1.
    - `grep -c "driver_detection_active=true, suppressing" apps/micmap/main.cpp` >= 1 (literal log line for 07-09 D-25(6) verification).
    - cmake configure exits 0; `cmake --build build --config Release --target micmap` exits 0.
    - Full build exits 0: `cmake --build build --config Release`.
    - All P5/P6/07-01..07-04 ctest carryovers stay green:
      - AssertNoOpenVRInCore, lint_no_openvr_in_core, lint_no_driver_macro, AssertAudioWorkerNoVrApi, AssertDetectionRunnerNoVrApi, AudioWorkerLifecycleHeadless, DetectionSettingsPropagation, DeviceProviderLifecycleStress, test_command_queue.
    - Existing client-side tests still pass (e.g., test_config_manager, test_cli_flags_parse, test_manifest_registrar, test_vr_input_quit_ordering, test_tray_balloon_once, test_vrmanifest_schema, test_bindings_patcher).
    - If any test stub implements IDriverClient (search via Grep), it has been updated to override isDriverDetectionActive (returning false) — otherwise those test exes will fail to build.
  </acceptance_criteria>
  <done>The full migration handshake is in place. Driver /health emits `driver_detection_active`; client polls and caches for 1s; onTrigger gates tap() on the field. Pitfall 10 mitigation complete (D-09 + D-10 + D-11 backstop). 07-09 UAT D-25(6) is the final coexistence verification on Bigscreen Beyond.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| HTTP server thread → DeviceProvider members via lambda capture | Cross-thread read of detectionRunner_ / audioWorker_ / driverDetectionEnabled_ at /health request time |
| Client process → driver process via /health JSON | New field crosses process boundary as JSON; existing localhost-only binding (P5+v1.5 SVR-05) unchanged |
| MicMapApp::onTrigger thread → DriverClient::isDriverDetectionActive cache members | Cache read may race with concurrent calls; DriverClient is single-threaded per usage convention but this is worth noting |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-07-05-01 | I (Information disclosure — race window in /health getter lambda during Cleanup) | HttpServer::SetupRoutes /health handler | accept | Per the Cleanup-order discussion in Task 1's action: D-20 reverses the order to detectionRunner_.reset() FIRST, audioWorker_.reset() SECOND, then httpServer_->Stop(). During detectionRunner reset, an HTTP /health poll could theoretically read detectionRunner_.get() and dereference IsRunning() on a partially-destructed object. In practice, vrserver is shutting down during Cleanup and no client is polling. P10 deletes the entire scaffolding; for P7 we accept the theoretical race. The 50-cycle stress test (DeviceProviderLifecycleStress, 07-01) does NOT poll /health during Cleanup, so the test won't surface the race. |
| T-07-05-02 | S (Spoofing — non-driver process responds on port 27015 with `driver_detection_active=true`) | DriverClient::isDriverDetectionActive | accept | Existing v1.5 SVR-05 mitigation: HttpServer binds to 127.0.0.1 only (Pitfall 7 — already mitigated in P5). Localhost-only ensures no remote spoof. A local malicious process could in theory bind 27015 first and serve a fake /health, but that requires local code-execution which is already game-over for the user. Risk accepted as out-of-scope for P7. |
| T-07-05-03 | D (DoS — DriverClient::isDriverDetectionActive blocks onTrigger for 2 seconds when driver is unreachable) | client httplib timeouts | mitigate | `client.set_connection_timeout(2); client.set_read_timeout(2);` — worst-case 4s before falling back to false. The 1-second cache means subsequent calls within 1s return cached=false without re-polling. NOT ideal but bounded. Future improvement (deferred): async pre-fetch of /health on a background thread; not in P7 scope. |
| T-07-05-04 | T (Tampering — JSON field renamed/typo'd, suppression silently fails) | server emit + client parse | mitigate | Both sides hardcode the literal `"driver_detection_active"` string. Acceptance: `grep -c "driver_detection_active" driver/src/http_server.cpp` >= 1 AND `grep -c "driver_detection_active" src/steamvr/src/vr_input.cpp` >= 1. The 07-09 UAT D-25(6) coexistence test verifies single-tap behavior end-to-end on real hardware, which catches any typo. |
| T-07-05-05 | I (Information disclosure — /health response leaks detection-active state to LAN scanners) | port-discovery scan | mitigate | Existing v1.5 P5+P6 mitigation: HttpServer binds 127.0.0.1 only. Acceptance: `grep -c "127.0.0.1" driver/resources/settings/default.vrsettings` >= 1 (existing http_host setting); netstat verification deferred to 07-09 UAT general regression. |
| T-07-05-06 | E (Elevation — client gates tap() on field but field is forced false for an attacker who controls a downstream proxy) | network path | accept | Localhost-only binding eliminates the meaningful network path. A local attacker with privileges to MITM localhost has already won. Out-of-scope for P7. |
| T-07-05-07 | T (Tampering — `body.value("driver_detection_active", false)` returns false on type mismatch (e.g., field present as string "true" instead of boolean)) | DriverClient parse | mitigate | nlohmann::json::value()'s default-on-type-mismatch behavior catches this defensively — falls back to false (no suppression). Acceptance: source-grep on the `value(..., false)` call. The driver only emits boolean per Task 1 (acceptance: `grep -c "driver_detection_active.*= " driver/src/http_server.cpp` shows assignment from getter call which is `bool`, not string). |
</threat_model>

<verification>
**After both tasks land:**

```bash
# Driver target compiles
cmake --build build --config Release --target driver_micmap

# Client target compiles
cmake --build build --config Release --target micmap

# Full build
cmake --build build --config Release

# All carryovers stay green
ctest --test-dir build --output-on-failure -E "DetectionSettingsPropagation|DeviceProviderLifecycleStress"  # exclude RED scaffolds if still RED
ctest --test-dir build -R "DetectionSettingsPropagation|DeviceProviderLifecycleStress" --output-on-failure  # confirm GREEN

# Manual smoke (deferred to 07-06 UAT for full verification)
# Start vrserver with both flags ON; in another terminal:
# curl http://127.0.0.1:27015/health | jq .
# Expected: {"status": "healthy", "driver_detection_active": true}
```

The driver_detection_active field is end-to-end verifiable via curl when the driver is running — that is the autonomous-task substitute for full UAT. 07-06 D-25(6) is the manual coexistence sign-off on Bigscreen Beyond.
</verification>

<success_criteria>
- HttpServer ctor takes a 4th `std::function<bool()>` param (default nullptr); /health JSON gains `driver_detection_active` boolean field reflecting live driver state (D-09).
- DeviceProvider passes a getter lambda capturing detectionRunner_/audioWorker_/driverDetectionEnabled_ at HttpServer construction.
- IDriverClient gains `isDriverDetectionActive` pure-virtual; DriverClient implements it with /health poll + 1s cache + defensive false-on-error.
- MicMapApp::onTrigger gates tap() on isDriverDetectionActive; emits literal log `onTrigger: driver_detection_active=true, suppressing` when suppressing.
- State machine cooldown remains the backstop (D-11) — already enforced.
- Client + driver build cleanly; all P5/P6/07-01..07-04 carryover invariants stay green.
- 07-06 UAT D-25(6) coexistence on Bigscreen Beyond is the final manual sign-off.
</success_criteria>

<output>
After completion, create `.planning/phases/07-driver-side-detection-thread/07-05-SUMMARY.md`
</output>
