Skip to content

TASK-1.6: Implement FlaUI-Driven Measurement Capture

Objective

Build a UI-Automation-driven capture rig that drives the real WPF main window through actual button clicks, runs dotnet-counters against the running process for metrics, produces 18-metric measurement rows in the established format, and closes the deferred slice-1-2-real-frame-payloads row. This replaces the retired SLICE-1.5 / SLICE-1.5.1 automation rig.

Scope

  • new test project tests/InspectionPrototype.AcceptanceTests (Windows-only, net10.0-windows) with FlaUI.Core + FlaUI.UIA3 package references via Directory.Packages.props
  • IUiDriver / FlaUiDriver / FakeUiDriver abstraction split — drivers live in a new shared library so unit tests in InspectionPrototype.Tests can use the fake without pulling FlaUI into the unit-test project
  • AutomationProperties.AutomationId audit on every operator-touched control in MainWindow.xaml; a build-time regression test asserts the canonical id set is present
  • a JSON-binding integration test for SimulatorProfilesOptions (folding in the SLICE-1.2 review finding — verify HighFrameRate actually loads from appsettings.json)
  • two scenarios as xUnit [Fact]s with [Trait("Category", "Capture")]: DemoBaselineFlaUi, MultiTagSoakFlaUi
  • new (smaller) tools/Capture-Measurements.ps1 wrapping build → app launch → dotnet-counters → dotnet test (filtered) → row extraction
  • runbook §3a refresh: FlaUI prerequisites + new orchestrator invocation; preserves the sleep-disable powercfg recipe; preserves the SLICE-1.5 retirement pointer
  • capture the deferred slice-1-2-real-frame-payloads row (10-min HighFrameRate scenario via the new rig)
  • update SLICE-1.5 / SLICE-1.5.1 spec/task banners from "planned SLICE-1.6" to "SLICE-1.6 (this slice)"
  • update CLAUDE.md current-position block + roadmap-progress.md table + session-log entry

Non-Scope

  • CI integration (FlaUI requires foreground window; out of scope until CI runners get RDP/VM display)
  • accessibility audit, screenshot-diff regression testing, multi-monitor support, or any FlaUI usage beyond what the two scenarios need
  • re-capturing rows 0, 0a, 0b, or slice-1-1-multi-tag-telemetry — they remain valid historical evidence under the retired rig
  • replacing any of the existing 313 unit tests in InspectionPrototype.Tests with FlaUI-based equivalents
  • migrating Get-GcPauseP95 / Get-LohAllocRateAvg extraction helpers (already correct from SLICE-1.2 Pass 2; reused as-is)
  • new measurement metrics beyond the 18 already produced by ConvertTo-MeasurementRow

Touched Projects

  • tests/InspectionPrototype.AcceptanceTests/ — new project, scenarios, FakeUiDriver tests
  • src/InspectionPrototype.UiDriver/ (or similar) — new shared library, IUiDriver, FakeUiDriver. FlaUiDriver may live in the AcceptanceTests project to keep FlaUI deps isolated, OR in this library if implementation finds it cleaner — either is acceptable
  • src/InspectionPrototype.App/MainWindow.xaml — add AutomationProperties.AutomationId to every operator-touched control
  • tests/InspectionPrototype.Tests/ — JSON-binding integration test for SimulatorProfilesOptions; AutomationId regression test (parses MainWindow.xaml as XML, asserts the canonical id set is present)
  • tools/Capture-Measurements.ps1 — re-introduced as a small wrapper (~80 lines)
  • docs/runbook/capturing-measurements.md — §3a refresh
  • docs/specs/SLICE-1.5-automated-measurement-capture.md and docs/specs/SLICE-1.5.1-automated-capture-followups.md — banner update
  • docs/tasks/TASK-1.5-implement-automated-measurement-capture.md and docs/tasks/TASK-1.5.1-implement-automated-capture-followups.md — banner update
  • docs/reviews/phase-1-measurements.md — slice-1-2-real-frame-payloads row block
  • docs/captures/ — new CSV evidence
  • Directory.Packages.props — FlaUI version pin
  • CLAUDE.md, docs/reviews/roadmap-progress.md

AI Tool Guidance

Three Copilot passes; one-pass-per-session protocol as in TASK-1.5.

  1. Foundations — new AcceptanceTests project + FlaUI deps, IUiDriver / FlaUiDriver / FakeUiDriver, AutomationId audit on MainWindow.xaml + regression test, JSON-binding integration test. NO scenarios, NO orchestrator, NO captures.
  2. Scenarios + orchestratorDemoBaselineFlaUi, MultiTagSoakFlaUi, scenario unit tests via FakeUiDriver, new tools/Capture-Measurements.ps1. NO captures.
  3. Capture + runbook + banner updates — run the 10-min HighFrameRate capture with sleep disabled, append row block, refresh runbook §3a, update SLICE-1.5/1.5.1 banners, update CLAUDE.md and roadmap-progress.md. NO code changes.

Acceptance Criteria Mapping

The implementation must satisfy all acceptance criteria from SLICE-1.6:

  • Pass 1 covers criteria 1, 2, 3, 8 (existing tests still pass), plus the JSON-binding finding from the SLICE-1.2 review
  • Pass 2 covers criteria 4, 5 (smoke run portion), 9 (Trait), and the FakeUiDriver portion of 8
  • Pass 3 covers criteria 6, 7, 10

Copilot Agent Prompts

Pass 1 — Foundations

You are implementing Pass 1 of TASK-1.6: stand up the FlaUI acceptance-test
project, the IUiDriver abstraction, the AutomationId audit on MainWindow.xaml,
and a JSON-binding integration test for SimulatorProfilesOptions.
NO scenarios, NO orchestrator script, NO captures.

## Authoritative references

Read these before making changes:
- docs/specs/SLICE-1.6-flaui-capture.md     (the requirements)
- docs/tasks/TASK-1.6-implement-flaui-capture.md (this task)
- src/InspectionPrototype.App/MainWindow.xaml
- src/InspectionPrototype.Presentation/ViewModels/MainViewModel.cs   (find every IRelayCommand and the controls bound to them)
- src/InspectionPrototype.Infrastructure/InfrastructureServiceCollectionExtensions.cs
- src/InspectionPrototype.Infrastructure/Simulator/SimulatorProfilesOptions.cs
- src/InspectionPrototype.App/appsettings.json
- Directory.Packages.props
- tests/InspectionPrototype.Tests/InspectionPrototype.Tests.csproj   (model for the new AcceptanceTests project)

## Scope of this pass

New project + FlaUI deps, driver abstraction, MainWindow.xaml ID audit + regression
test, SimulatorProfilesOptions JSON-binding integration test. NO scenarios, NO
orchestrator. NO captures.

## Deliverables

1. New shared library project src/InspectionPrototype.UiDriver/:
   - InspectionPrototype.UiDriver.csproj with TargetFramework net10.0 (NOT
     net10.0-windows — this library has zero WPF dependencies; consumers
     in WPF projects bring their own)
   - Reference: nothing project-internal; this library is standalone
   - Files:
     - IUiDriver.cs:
         Task ClickByAutomationIdAsync(string id, TimeSpan? timeout = null);
         Task<string> ReadTextByAutomationIdAsync(string id);
         Task<bool> WaitForTextAsync(string id, string expected, TimeSpan timeout);
         Task SelectComboBoxItemAsync(string id, string item);
         Task<byte[]> CaptureScreenshotPngAsync();
       (return byte[] for the screenshot to keep the interface free of System.Drawing types)
     - FakeUiDriver.cs:
         An in-process implementation backed by:
           ConcurrentDictionary<string, FakeElement> _elements
         where FakeElement holds CurrentText and an Action OnClick that
         tests can configure. Methods record calls into a ClickLog list
         that tests can assert against.
         WaitForTextAsync polls _elements at 10 ms cadence (fast for tests).
         SelectComboBoxItemAsync sets the element's CurrentText.
         CaptureScreenshotPngAsync returns Array.Empty<byte>() (tests don't
         care about pixel content; just that the method is callable).

2. Add InspectionPrototype.UiDriver to Directory.Packages.props centrally:
   - No new package deps (FakeUiDriver uses BCL only).
   - Verify the new project compiles standalone with no warnings.

3. New test project tests/InspectionPrototype.AcceptanceTests/:
   - InspectionPrototype.AcceptanceTests.csproj with:
       <TargetFramework>net10.0-windows</TargetFramework>
       <UseWPF>false</UseWPF>   (we drive the WPF app from outside; this project itself isn't WPF)
       Reference InspectionPrototype.UiDriver
       Package references (centralize versions in Directory.Packages.props):
         FlaUI.Core (latest stable; record the chosen version in commit message)
         FlaUI.UIA3 (matching version)
         xunit, xunit.runner.visualstudio, Microsoft.NET.Test.Sdk (match InspectionPrototype.Tests)
   - Files:
     - FlaUiDriver.cs — implements IUiDriver using FlaUI.UIA3.UIA3Automation:
         Constructor takes a System.Diagnostics.Process and uses
         FlaUI.Core.Application.Attach(process) to bind.
         ClickByAutomationIdAsync polls the AutomationElement tree at 100 ms
         intervals until an element with matching AutomationId is found and
         IsEnabled, then invokes InvokePattern.Invoke().
         WaitForTextAsync polls at 100 ms; reads element.Properties.Name (or
         element.AsTextBlock().Text for TextBlock).
         SelectComboBoxItemAsync uses ExpandCollapsePattern then selects the
         child whose Name matches.
         All time-bounded methods throw TimeoutException with a message
         naming the element id and timeout.
     - SmokeTest.cs ([Trait("Category","Smoke")]):
         A trivial xUnit [Fact] that constructs FakeUiDriver and verifies
         ClickByAutomationIdAsync records the click in the log. This proves
         the project builds and references resolve. NO scenarios yet.

4. AutomationProperties.AutomationId audit in src/InspectionPrototype.App/MainWindow.xaml:
   - Add namespace if missing:
       xmlns:auto="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       (AutomationProperties is in the default WPF presentation namespace)
   - Required automation IDs (these are the ones SLICE-1.6 scenarios depend on):
       Buttons: ConnectButton, DisconnectButton, RefreshCatalogButton,
                LoadRecipeButton, HomeButton, StartRunButton, StopButton,
                ApplySimulatorProfileButton
       ComboBoxes: RecipeSelectorComboBox, SimulatorProfileSelectorComboBox
       TextBlocks: WorkflowStateText, ConnectionStateText, ActiveSimulatorProfileText
   - Find each in MainWindow.xaml; add AutomationProperties.AutomationId="<Id>".
     If a control name doesn't match expected (e.g. it's not the right
     ContentPresenter wrapping a Button), audit the binding: the IRelayCommand
     in MainViewModel that maps to the operator action determines which
     button gets which id.

5. Tests under tests/InspectionPrototype.Tests/Presentation/:
   - MainWindowAutomationIdRegressionTest.cs:
       * Read MainWindow.xaml as an XML document at test time
       * Use XPath or LINQ to XML to find every element with an
         AutomationProperties.AutomationId attribute
       * Assert each id in the canonical set above is present exactly once
       * Print actionable failure: "Missing AutomationId 'StartRunButton' in
         MainWindow.xaml. SLICE-1.6 scenarios depend on this id."
       * The test must NOT instantiate the WPF app (no [STAThread], no
         Application.Current); just parse XAML as XML.

6. Tests under tests/InspectionPrototype.Tests/Infrastructure/:
   - SimulatorProfilesOptionsBindingTests.cs (NEW):
       * Build an IConfiguration with InMemoryCollection containing a
         "Simulator:Profiles:0:Name" = "TestProfile" key (and the rest of
         the required SimulatorProfile fields)
       * Register options via services.AddOptions<SimulatorProfilesOptions>()
         .BindConfiguration(SimulatorProfilesOptions.SectionName)
         (mirror the production registration in
         InfrastructureServiceCollectionExtensions)
       * Build the service provider, resolve IOptions<SimulatorProfilesOptions>
       * Assert options.Profiles contains "TestProfile" with the bound fields
       * This is the regression test that catches the silent-ignore bug from
         the SLICE-1.2 review (where PostConfigure-only registration meant
         JSON profiles were ignored).

## Constraints

- Do NOT add scenarios (Pass 2).
- Do NOT add the orchestrator script (Pass 2).
- Do NOT capture anything (Pass 3).
- Do NOT add any production-code dependency on FlaUI. FlaUI lives ONLY in
  InspectionPrototype.AcceptanceTests; the rest of the codebase doesn't know
  it exists.
- Do NOT change MainViewModel, IRelayCommand, or any binding plumbing.
  AutomationId is purely a XAML addition.
- The MainWindowAutomationIdRegressionTest must be in InspectionPrototype.Tests
  (the existing pure-domain test project), NOT in AcceptanceTests. The
  regression check has no FlaUI dependency.

## Verification before you report done

  dotnet build --configuration Release
  dotnet test --configuration Release   (filtered to exclude Trait="Capture" if any exist)

Manual smoke tests:
  - launch the app interactively
  - in the open window, mentally verify each AutomationId is present by
    using Inspect.exe (Windows SDK) or AccessibilityInsights.
    Confirm: clicking on each named control finds an element whose
    AutomationId matches the expected name.
  - if Inspect.exe is unavailable, the regression test in deliverable 5 is
    the necessary-and-sufficient signal.

## Report format when finished

- files created and modified
- the chosen FlaUI package version (recorded in commit message)
- confirmation that all 313+ existing tests still pass plus the new
  AutomationId regression and JSON-binding integration tests
- a single commit hash
- commit message: "feat(test): add InspectionPrototype.AcceptanceTests + FlaUI driver, MainWindow AutomationIds, JSON-binding regression test (pass 1/3 of TASK-1.6)"

Pass 2 — Scenarios + orchestrator

You are implementing Pass 2 of TASK-1.6. Pass 1 (AcceptanceTests project, IUiDriver
abstraction, MainWindow AutomationId audit, JSON-binding test) is already merged.
This pass adds the two scenario [Fact]s and the new tools/Capture-Measurements.ps1
wrapper. NO captures.

## Authoritative references

Read these before making changes:
- docs/specs/SLICE-1.6-flaui-capture.md (criteria 4, 5, 9; FakeUiDriver portion of 8)
- src/InspectionPrototype.UiDriver/IUiDriver.cs            (Pass 1 output)
- src/InspectionPrototype.UiDriver/FakeUiDriver.cs         (Pass 1 output)
- tests/InspectionPrototype.AcceptanceTests/FlaUiDriver.cs (Pass 1 output)
- tests/InspectionPrototype.AcceptanceTests/SmokeTest.cs   (Pass 1 output, will be replaced)
- tools/MeasurementExtraction.psm1                         (existing — reused unchanged)
- docs/runbook/capturing-measurements.md   (§4.1, §4.2 step lists with inter-run Home — scenarios mirror these)

Pass 1 must be merged. Confirm by checking that the AcceptanceTests project
builds and the AutomationId regression test passes.

## Scope of this pass

Two scenario [Fact]s with [Trait("Category","Capture")], scenario unit tests
via FakeUiDriver, new tools/Capture-Measurements.ps1. NO captures, NO new
abstractions.

## Deliverables

1. tests/InspectionPrototype.AcceptanceTests/Scenarios/DemoBaselineFlaUi.cs:
   - public class DemoBaselineFlaUi
   - [Fact, Trait("Category", "Capture")]
   - public async Task RunAsync()
       reads DURATION_SECONDS env var (parses int; default 600)
       reads APP_PROCESS_ID env var (parses int; throws if missing)
       attaches FlaUiDriver to the named process
       runs the scenario flow:
         await driver.ClickByAutomationIdAsync("ConnectButton");
         await driver.WaitForTextAsync("ConnectionStateText", "Connected", 10s);
         await driver.ClickByAutomationIdAsync("RefreshCatalogButton");
         await driver.WaitForTextAsync("WorkflowStateText", "Idle", 10s);   // catalog populated; recipe selectable
         await driver.SelectComboBoxItemAsync("RecipeSelectorComboBox", "Standard 5-Point Wafer Scan");
         await driver.ClickByAutomationIdAsync("LoadRecipeButton");
         await driver.WaitForTextAsync("WorkflowStateText", "Idle", 10s);
         await driver.ClickByAutomationIdAsync("HomeButton");
         await driver.WaitForTextAsync("WorkflowStateText", "Ready", 30s);
         using var durationCts = new CancellationTokenSource(TimeSpan.FromSeconds(durationSeconds));
         while (!durationCts.IsCancellationRequested) {
             await driver.ClickByAutomationIdAsync("StartRunButton");
             await driver.WaitForTextAsync("WorkflowStateText", "Running", 10s);
             // wait for terminal state — Completed, Stopped, Faulted, Aborted
             // either parameterize WaitForTextAsync to take a predicate, or
             // poll Read for any-of-set; pick the cleaner option
             await WaitForTerminalStateAsync(driver, durationCts.Token);
             if (durationCts.IsCancellationRequested) break;
             await driver.ClickByAutomationIdAsync("HomeButton");
             await driver.WaitForTextAsync("WorkflowStateText", "Ready", 30s);
         }
         // tear-down
         var ws = await driver.ReadTextByAutomationIdAsync("WorkflowStateText");
         if (ws == "Running") {
             await driver.ClickByAutomationIdAsync("StopButton");
             // best-effort; don't fail the test if Stop hangs
         }
         await driver.ClickByAutomationIdAsync("DisconnectButton");
   - factor the run loop into private methods so the unit test (deliverable 3)
     can drive a single iteration against FakeUiDriver

2. tests/InspectionPrototype.AcceptanceTests/Scenarios/MultiTagSoakFlaUi.cs:
   - same shape as DemoBaselineFlaUi, with one extra step before Connect:
       await driver.SelectComboBoxItemAsync("SimulatorProfileSelectorComboBox", "MultiTag");
       await driver.ClickByAutomationIdAsync("ApplySimulatorProfileButton");
       await driver.WaitForTextAsync("ActiveSimulatorProfileText", "MultiTag", 5s);
   - reuses the same private run-loop method as DemoBaselineFlaUi if practical;
     if not, accept a small amount of duplication

3. tests/InspectionPrototype.Tests/Scenarios/ScenarioFlowTests.cs (NEW, in
   the existing pure-xUnit test project, NOT AcceptanceTests):
   - Reference InspectionPrototype.UiDriver (FakeUiDriver only)
   - Tests:
       FakeUiDriver_DrivesDemoBaselineFlow_RecordsExpectedClickSequence:
         construct FakeUiDriver
         pre-seed _elements with the canonical AutomationIds and "Connected"/"Ready"
         text values
         invoke a single iteration of the run loop (extract from
         DemoBaselineFlaUi if needed, or duplicate as a test fixture)
         assert ClickLog contains:
           ["ConnectButton", "RefreshCatalogButton", "LoadRecipeButton",
            "HomeButton", "StartRunButton", "HomeButton", "DisconnectButton"]
       Scenario_HandlesProfileSelection_BeforeConnect:
         (MultiTagSoakFlaUi-equivalent)
         assert SelectComboBoxItemAsync("SimulatorProfileSelectorComboBox",...)
         was called BEFORE ClickByAutomationIdAsync("ConnectButton")
   - These tests do NOT require a window; they verify scenario logic in
     isolation.

4. New tools/Capture-Measurements.ps1 (~80 lines):
   - parameters:
       [Parameter(Mandatory)][string]$Scenario      # "DemoBaseline" or "MultiTagSoak"
       [Parameter(Mandatory)][int]$DurationSeconds
       [Parameter(Mandatory)][string]$OutputCsv
       [string]$CommitHash = ''
       [string]$Profile = ''                       # optional; if set, uses --start-with-profile fallback in app launch
       [string]$SliceTag = ''
       [switch]$AllowDirty
       [switch]$AppendToTable
   - sequence:
       0. Refuse if working tree dirty (unless -AllowDirty).
          Default $CommitHash to (git rev-parse --short HEAD).
       1. Build Release. Fail script if build fails.
       2. Launch the app VISIBLY (no -WindowStyle Hidden):
          $appProc = Start-Process -FilePath $AppExe -PassThru
          (no --scenario flag; the app starts in normal interactive mode)
       3. Poll `dotnet-counters ps` for the new PID (timeout 30 s).
       4. Start `dotnet-counters collect` against the PID with
          --counters InspectionPrototype,System.Runtime, --refresh-interval 1,
          --format csv, --output $OutputCsv.
       5. Wait for the app's window to be ready (poll FlaUI from PowerShell
          via `Add-Type -Path` to load FlaUI assemblies, OR — simpler —
          just sleep 3 seconds; the test itself will retry-poll and so a
          fixed sleep is fine).
       6. Set environment vars:
          $env:APP_PROCESS_ID = $appProc.Id
          $env:DURATION_SECONDS = $DurationSeconds
       7. Run the test:
          dotnet test tests/InspectionPrototype.AcceptanceTests --configuration Release `
            --filter "FullyQualifiedName~$Scenario" --no-build --logger "console;verbosity=detailed"
          Capture exit code.
       8. Stop dotnet-counters (Wait-Process with 5 s timeout, then Stop-Process -Force).
       9. Close the app: try `$appProc.CloseMainWindow()` then Wait-Process 5 s,
          then `Stop-Process -Force` if still running.
      10. Verify $OutputCsv exists and is non-empty.
      11. Import-Module .\tools\MeasurementExtraction.psm1 -Force.
      12. Run ConvertTo-MeasurementRow with the captured args and print the
          18-metric markdown row block. If $AppendToTable, append to
          docs/reviews/phase-1-measurements.md under "## Phase 1 rows".
      13. If the dotnet-test exit code was non-zero: rename the CSV to
          ${OutputCsv}.partial, exit 1.
   - Document the new flow in the script's comment block at the top.

## Constraints

- Do NOT capture row blocks in this pass — Pass 3 owns the actual SLICE-1.2
  row capture.
- Do NOT add new automation IDs to MainWindow.xaml. If a scenario needs
  an id that doesn't exist, that's a Pass 1 gap; STOP and add it via a
  small Pass 1 fixup commit before continuing.
- Do NOT add a `--start-with-profile` CLI flag to the app yet. The spec
  authorizes one as a fallback if FlaUI's ComboBox driving is flaky, but
  determine whether it's needed first by running a smoke scenario manually.
- Scenario [Fact]s must NOT run as part of a default `dotnet test` invocation.
  The Trait("Category","Capture") is the gate; the orchestrator's
  --filter "FullyQualifiedName~$Scenario" picks the specific test.
- Do NOT touch CLAUDE.md, roadmap-progress.md, or runbook §3a in this
  pass — Pass 3 owns those.

## Verification before you report done

  dotnet build --configuration Release
  dotnet test --configuration Release   (default — should NOT include the Capture-trait scenarios)

Manual smoke test (60 s scenario, not the full 10 minutes):
  - tools/Capture-Measurements.ps1 -Scenario DemoBaseline -DurationSeconds 60 `
      -OutputCsv docs/captures/_smoke.csv -AllowDirty
  - Confirm:
      app window appears, FlaUI clicks ARE visible (cursor doesn't move,
      but buttons appear pressed; status text changes)
      runs.completed > 0 in the printed row block
      script exits 0
      CSV exists at the named path
  - Delete the smoke CSV before commit.
  - If the scenario hits a flaky FlaUI step (especially the ComboBox), file
    the symptom in the report and add the `--start-with-profile` fallback to
    the app NOW (within this pass) per the SLICE-1.6 spec's verification note.

## Report format when finished

- files created and modified
- the smoke-test stdout (the 18-metric markdown row block) — for evidence
  that the new rig produces the same format as the retired one
- the chosen approach for waiting on terminal state in the run loop
  (predicate-based WaitForText, or poll-and-check) — note in commit message
- whether `--start-with-profile` fallback was needed, and why
- a single commit hash
- commit message: "feat(test,tools): add FlaUI scenario [Fact]s, fake-driver scenario tests, capture orchestrator wrapper (pass 2/3 of TASK-1.6)"

Pass 3 — Capture + runbook + banner updates

You are implementing Pass 3 of TASK-1.6, the final pass. Passes 1 and 2 are
merged. This pass runs the deferred slice-1-2-real-frame-payloads capture
under the new rig, refreshes runbook §3a, updates the SLICE-1.5 / SLICE-1.5.1
banners, and updates session-handoff documents. NO code changes.

## Authoritative references

Read these before making changes:
- docs/specs/SLICE-1.6-flaui-capture.md   (criteria 6, 7, 10)
- docs/specs/SLICE-1.2-real-frame-payloads.md   (criterion 6 of that slice — frames.dropped=0, frames.ingested>=17500)
- docs/runbook/capturing-measurements.md  (current §3a — sleep-disable + retirement pointer)
- docs/reviews/phase-1-measurements.md    (slice-1-1-multi-tag-telemetry row to mirror)
- docs/specs/SLICE-1.5-automated-measurement-capture.md       (banner to update)
- docs/specs/SLICE-1.5.1-automated-capture-followups.md       (banner to update)
- docs/tasks/TASK-1.5-implement-automated-measurement-capture.md  (banner to update)
- docs/tasks/TASK-1.5.1-implement-automated-capture-followups.md  (banner to update)
- docs/tasks/TASK-1.2-implement-real-frame-payloads.md         (status block — SLICE-1.2 closes when this pass lands)
- CLAUDE.md, docs/reviews/roadmap-progress.md
- tools/Capture-Measurements.ps1   (Pass 2 output)

## Scope of this pass

Capture, table edit, runbook §3a refresh, banner updates, session-handoff
updates. NO code changes.

## Deliverables

1. Disable system sleep BEFORE the capture. Note the previous values for
   restoration:
       powercfg /change standby-timeout-ac 0
       powercfg /change monitor-timeout-ac 0

2. Run the SLICE-1.2 capture under the new rig:
       $date = Get-Date -Format 'yyyy-MM-dd'
       tools/Capture-Measurements.ps1 -Scenario MultiTagSoak `
         -DurationSeconds 600 `
         -OutputCsv "docs/captures/slice-1-2-high-fps-$date.csv" `
         -CommitHash $(git rev-parse --short HEAD) `
         -Profile HighFrameRate `
         -SliceTag slice-1-2-real-frame-payloads `
         -AppendToTable
   Confirm:
       * exit code 0
       * frames.dropped == 0 (criterion 6 of SLICE-1.2)
       * frames.ingested >= 17500 (criterion 6 of SLICE-1.2)
       * 18 metrics in the printed row block, including non-em-dash values
         for gc-pause-p95 and LOH-alloc-rate avg
       * tags.active == 50 in the CSV (regression check)
       * the row block was appended to phase-1-measurements.md

   If any of these fails, STOP. Capture the failure mode in the report; do
   NOT proceed to the table edit, runbook refresh, or banner updates. Hand off.

3. Refresh runbook §3a in docs/runbook/capturing-measurements.md:
   - Keep the "Disable system sleep" subsection as-is.
   - Replace the "the retired automated capture rig" paragraph with the
     new SLICE-1.6 rig description:
       - one-paragraph rationale
       - new orchestrator invocation:
           tools/Capture-Measurements.ps1 -Scenario <name> -DurationSeconds <secs> `
             -OutputCsv docs/captures/<name>-<date>.csv `
             -CommitHash $(git rev-parse --short HEAD)
       - subsection "FlaUI prerequisites":
           foreground window required (no headless or RDP-disconnected)
           no screen lock during capture (locks the workstation, dispatcher pauses)
           Windows display scaling = 100% recommended (FlaUI element coords
           use logical pixels; non-100% can shift hit-test under some controls)
       - subsection "Registered scenarios" (mirror the structure from before):
           DemoBaseline    | DemoBaselineFlaUi
           MultiTagSoak    | MultiTagSoakFlaUi
       - subsection "When to fall back to manual §3":
           fast exploratory captures (manual is faster than the rig for <60s runs)
           machines without FlaUI installed (partial dev environments)
   - Remove the "retired rig" Note block — the rig is no longer retired
     in the abstract; SLICE-1.6 IS the rig. Add one short historical line:
       "An earlier headless rig (SLICE-1.5) drove ICommand directly without
        rendering the UI; that rig was retired 2026-04-27 in favor of this
        UI-Automation approach."

4. Update SLICE-1.5 / SLICE-1.5.1 banners:
   - In docs/specs/SLICE-1.5-automated-measurement-capture.md, replace
     "Status: Superseded by SLICE-1.6 (FlaUI capture, planned)" with
     "Status: Superseded by SLICE-1.6 (FlaUI capture, shipped)" and update
     the banner prose to reference SLICE-1.6 as a real, landed slice.
   - Same edit in docs/specs/SLICE-1.5.1-automated-capture-followups.md.
   - Same edit in docs/tasks/TASK-1.5-implement-automated-measurement-capture.md.
   - Same edit in docs/tasks/TASK-1.5.1-implement-automated-capture-followups.md.

5. Update TASK-1.2 status block:
   - "Status: Passes 1 & 2 complete; Pass 3 deferred" becomes
     "Status: Passes 1 & 2 complete; Pass 3 captured under SLICE-1.6"
   - Add a one-line note at the bottom referencing the slice-1-2-real-frame-payloads
     row block in phase-1-measurements.md and the CSV path under docs/captures/.

6. Update CLAUDE.md "Current position" block:
   - Phase: 1 (Simulator to scale) — SLICE-1.6 complete; SLICE-1.2 closed
     (Pass 3 row captured under the FlaUI rig)
   - Last completed: TASK-1.6 Pass 3 — captured slice-1-2-real-frame-payloads
     row (10-min HighFrameRate, 18 metrics, frames.dropped=0,
     frames.ingested=<count>); refreshed runbook §3a; updated SLICE-1.5/1.5.1
     banners to "shipped"; commit <hash>
   - Next action: SLICE-1.3 (Encoder-rate motion); spec needs to be written
   - Blocked on: nothing
   - Last updated: <today's date>

7. Append session-log entry to docs/reviews/roadmap-progress.md naming the
   row block, frames.dropped, frames.ingested, gc-pause-p95, LOH-alloc-rate-avg,
   and the commit hash. Mark SLICE-1.6 as Completed in the progress table;
   move SLICE-1.2 from "In Progress (Pass 3 deferred)" to "Completed".

8. Restore powercfg settings:
       powercfg /change standby-timeout-ac <previous_value_in_minutes>
       powercfg /change monitor-timeout-ac <previous_value_in_minutes>

## Constraints

- Do NOT make any code or test changes.
- Do NOT capture without disabling system sleep (deliverable 1).
- Do NOT move on to deliverables 3+ if the capture in deliverable 2 fails.
- Do NOT modify SLICE-1.5 or SLICE-1.5.1 spec/task BODIES — only the banner
  blocks at the top.
- Do NOT re-capture rows 0, 0a, 0b, or slice-1-1-multi-tag-telemetry.

## Verification before you report done

  dotnet build --configuration Release
  dotnet test --configuration Release   (default; Capture-trait scenarios still excluded)

Plus:
  - the docs/captures/slice-1-2-high-fps-<date>.csv file exists and is committed
  - the row block is in docs/reviews/phase-1-measurements.md with all 18
    metrics filled, frames.dropped = 0, frames.ingested >= 17500
  - runbook §3a renders correctly (no broken markdown, no stale links)
  - SLICE-1.5 / SLICE-1.5.1 banners reference the now-shipped SLICE-1.6
  - CLAUDE.md current-position block reflects SLICE-1.6 closure

## Report format when finished

- files created and modified
- the captured row block (the 18-metric markdown table) included in the report
- gc-pause-p95 and LOH-alloc-rate-avg values, with one-sentence interpretation
  (e.g. "p95 GC pause was X ms; LOH alloc rate Y MB/s — consistent with
  2 MB × 30 fps = 60 MB/s")
- a single commit hash
- commit message: "feat(measurements): close SLICE-1.6 with slice-1-2-real-frame-payloads row; runbook §3a refreshed (pass 3/3 of TASK-1.6)"

Operator notes

  • One pass per Copilot session. Same protocol as TASK-1.5.
  • Pass 1's IUiDriver lives in a standalone library, not in the AcceptanceTests project. This is non-obvious; the agent may suggest collapsing it. Refuse — the FakeUiDriver needs to be reachable from the existing InspectionPrototype.Tests project for scenario unit tests, and pulling the FlaUI dep transitively into that project would break the "FlaUI is isolated" rule.
  • Pass 1's MainWindow AutomationId audit is non-cosmetic. A missing id silently breaks Pass 2's scenarios; the regression test in deliverable 5 is the load-bearing canary.
  • Pass 2's smoke test is the load-bearing verification. A 60-second scenario run that produces a non-zero runs.completed proves the FlaUI driver actually drives the UI. If this fails, do not proceed to Pass 3.
  • Pass 3 must run with sleep disabled. Same rule as TASK-1.2's deferred Pass 3 — the LOH-alloc-rate metric the slice exists to measure is diluted by any sleep duration.
  • The FlaUI ComboBox quirk is real. If selecting the simulator profile via FlaUI proves unreliable in Pass 2, the spec authorizes adding a --start-with-profile <name> CLI flag to the app within this slice, not as a follow-up. Pass 2 owns that decision.
  • Update the index files only at the end of the phase, not per-slice. Same rationale as earlier tasks.

Docs-first project memory for AI-assisted implementation.