Skip to content

TASK-1.5.1: Implement Automated Capture Follow-ups

Status: Superseded as of 2026-04-27, along with TASK-1.5. Items 1, 2, 4 of the Copilot prompts below targeted the retired automated rig and are obsolete. Item 3's _disposed Interlocked guard on SimulatedTagSource (and the matching guard on SimulatedCamera) survives in the codebase as the fix for a real DI double-disposal bug. The replacement FlaUI rig (SLICE-1.6) shipped 2026-04-27. Read for context; do not run the prompts.

Objective

Land the four findings from the SLICE-1.5 review: replace the state.Update(... WorkflowState = Ready) hack in scenarios with the real ops.Home step the manual operator already performs, fix the runbook §4.1/§4.2 step lists that omit that Home, enforce the partial-CSV protection in Capture-Measurements.ps1, fix the ObjectDisposedException that fires at scenario-mode shutdown, add the UI-binding-regression caveat to runbook §3a, and capture row 0b under the corrected scenario so SLICE-1.2 onward has a clean automated reference.

Scope

  • replace the direct AppState mutation in DemoBaselineScenario and MultiTagSoakScenario with ops.Home.Execute(null) plus the existing WaitForStateAsync helper
  • correct the §4.1 and §4.2 step lists in docs/runbook/capturing-measurements.md to make the inter-run Home explicit
  • update tests/InspectionPrototype.Tests/Scenarios/DemoBaselineScenarioTests.cs (and the multi-tag tests if present) to drive Home transitions through the fake state store between runs
  • add a $KnownBenignExitCodes allowlist to tools/Capture-Measurements.ps1 and rename the CSV to .partial plus exit non-zero on any non-allowed non-zero code
  • add a small Test-PartialRenameOnFailure Pester test (or inline verification) for the rename path
  • investigate the ObjectDisposedException (exit code -532462766) at scenario-mode shutdown by attaching a debugger to --scenario Fake --duration 5, find the offending Dispose call, fix at the smallest reasonable scope (most likely a _disposed flag in SimulatedTagSource or a host shutdown ordering change in App.xaml.cs)
  • add the UI-binding-regression caveat to runbook §3a's "When to fall back to the manual procedure" subsection
  • capture row 0b under the corrected DemoBaselineScenario and append a row block to docs/reviews/phase-1-measurements.md; add a one-line note on row 0a marking row 0b as the new automated reference for SLICE-1.2 onward

Non-Scope

  • changes to CommandGuards.CanStart or any other workflow guard
  • a new operator command (Reset, Acknowledge, etc.)
  • broader refactor of the WPF host shutdown sequence beyond the minimum needed to fix the ObjectDisposedException
  • replacing dotnet-counters with an in-process exporter
  • new measurement metrics or new scenarios
  • back-revising row 0a (it stays as historical evidence; row 0b is the new reference)
  • SLICE-1.1 Pass 3 work (per-tag metrics, multi-tag-soak measurement row) — that lives in TASK-1.1 Pass 3

Touched Projects

  • src/InspectionPrototype.Application/Scenarios/DemoBaselineScenario.cs — replace the state.Update block with ops.Home
  • src/InspectionPrototype.Application/Scenarios/MultiTagSoakScenario.cs — same correction
  • src/InspectionPrototype.Infrastructure/Simulator/SimulatedTagSource.cs_disposed guard or equivalent (Pass 3 picks the right scope after debugging)
  • src/InspectionPrototype.App/App.xaml.cs — only if the fix turns out to need host shutdown reordering
  • tests/InspectionPrototype.Tests/Scenarios/DemoBaselineScenarioTests.cs — update fake transitions to expect Home between runs
  • tools/Capture-Measurements.ps1$KnownBenignExitCodes, partial-rename branch
  • tools/MeasurementExtraction.psm1 — extract a small Move-PartialCsvOnFailure function for testability (Pass 2)
  • docs/runbook/capturing-measurements.md — §4.1/§4.2 step list corrections (Pass 1), §3a caveat (Pass 2), §3a "App exit code" subsection cleanup (Pass 3)
  • docs/reviews/phase-1-measurements.md — row 0b block, row 0a footnote (Pass 3)
  • docs/captures/demo-baseline-automated-row-0b-<date>.csv — evidence (Pass 3)

AI Tool Guidance

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

  1. Scenario Home fix and runbook step-list corrections — items 1 and 2 of the spec. Replace the state.Update hack in both scenarios with ops.Home, update the existing scenario tests, fix the §4.1 and §4.2 step lists. No tooling work, no captures.
  2. Orchestrator partial-CSV protection and §3a UI-binding caveat — items 2 and 4 of the spec. $KnownBenignExitCodes allowlist with -532462766 as the only initial entry, partial-rename branch with a Pester smoke test, §3a paragraph addition. No code changes outside tools/.
  3. ObjectDisposedException fix and row 0b capture — items 3 and 5 of the spec. Debug, fix at the smallest scope, empty the $KnownBenignExitCodes allowlist, remove the §3a "App exit code" subsection, capture row 0b, append to the measurements table, update CLAUDE.md and the session log.

Each pass ends with its own commit. Run dotnet test after Passes 1 and 3 (Pass 2 is tooling-only); Pester for Pass 2's smoke test if available, otherwise document the manual verification.

Acceptance Criteria Mapping

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

  • Pass 1 covers criteria 1, 2, and the scenario-test portion of 7
  • Pass 2 covers criteria 3 and 5
  • Pass 3 covers criteria 4, 6, and the remainder of 7

Copilot Agent Prompts

Pass 1 — Scenario Home fix and runbook step-list corrections

You are implementing Pass 1 of TASK-1.5.1 in this repository: replace the
state.Update hack in DemoBaselineScenario and MultiTagSoakScenario with
ops.Home, then correct the runbook §4.1 and §4.2 step lists to match.
NO tooling work, NO captures.

## Authoritative references

Read these before making changes:
- docs/specs/SLICE-1.5.1-automated-capture-followups.md          (the requirements; item 1 is what this pass implements)
- docs/tasks/TASK-1.5.1-implement-automated-capture-followups.md (this task)
- src/InspectionPrototype.Application/Scenarios/DemoBaselineScenario.cs   (the broken block)
- src/InspectionPrototype.Application/Scenarios/MultiTagSoakScenario.cs   (same fix)
- src/InspectionPrototype.Application/Scenarios/ScenarioStepHelper.cs     (the WaitForStateAsync helper)
- src/InspectionPrototype.Application/Guards/CommandGuards.cs              (CanStart requires Idle | Ready; CanHome accepts Completed)
- docs/runbook/capturing-measurements.md                                   (§4.1 step list and §4.2 step list)
- tests/InspectionPrototype.Tests/Scenarios/DemoBaselineScenarioTests.cs   (existing fake transitions)

Spec acceptance criteria 1, 2, and the scenario-test portion of 7 are the
definition of done for this pass.

## Scope of this pass

Two scenario classes, two runbook subsections, the existing scenario test
file. NO changes to ScenarioRunner, IOperatorCommands, CommandGuards,
WorkflowService, or any tooling.

## Deliverables

1. In src/InspectionPrototype.Application/Scenarios/DemoBaselineScenario.cs,
   replace the block currently at lines 119-123:
       if (terminal is WorkflowState.Completed or WorkflowState.Stopped)
       {
           // Reset to startable state — WorkflowService has no auto-transition from Completed/Stopped → Ready.
           state.Update(s => s with { WorkflowState = WorkflowState.Ready });
       }
       else
       {
           // Faulted or Aborted — stop the loop.
           _logger.LogWarning(...);
           break;
       }

   with:
       if (terminal is WorkflowState.Completed or WorkflowState.Stopped)
       {
           // Re-home to return to a startable state. CanStart requires Idle|Ready;
           // after a run terminates the workflow sits in Completed/Stopped, and the
           // operator UI requires a Home click between runs. We follow the same path.
           ops.Home.Execute(null);
           await ScenarioStepHelper.WaitForStateAsync(
               state,
               s => s.WorkflowState == WorkflowState.Ready && s.IsMotionHomed,
               DefaultStepTimeout,
               "post-run WorkflowState == Ready && IsMotionHomed",
               ct);
       }
       else
       {
           // Faulted or Aborted — stop the loop.
           _logger.LogWarning(...);
           break;
       }

   Keep the existing operator-delay handling immediately after this block
   (the `if (context.OperatorDelay > TimeSpan.Zero) await Task.Delay(...)`)
   exactly as today. The Home step is BEFORE the operator-delay sleep, not
   after — operator-delay simulates human think-time AFTER homing finishes.

2. Apply the identical correction in
   src/InspectionPrototype.Application/Scenarios/MultiTagSoakScenario.cs.
   The block to replace and its surrounding context are identical to
   DemoBaselineScenario. The only difference between the two files in
   this region is the Name property; the run loop is shared in shape.

3. Update tests/InspectionPrototype.Tests/Scenarios/DemoBaselineScenarioTests.cs
   so the existing "scenario advances through Connect → ... → first StartRun
   → Idle, then test cancels" test publishes an additional sequence on the
   FakeAppStateStore between StartRun iterations:
       - after WorkflowState == Completed, expect ops.Home to fire
       - test pushes WorkflowState transitions Homing → Ready (with IsMotionHomed = true)
       - then expect the next StartRun
   Add a new test specifically for the inter-run Home:
       Test name: "RunLoop_AfterCompleted_CallsHomeBeforeNextStart"
       - drive the scenario through one complete run
       - assert ops.Home.Execute was called exactly once *between* the
         first run reaching Completed and the second StartRun firing
       - assert no calls to state.Update flipping WorkflowState (a regression
         guard: if a future change reintroduces the hack the test fails)

4. Repeat the equivalent test addition in
   tests/InspectionPrototype.Tests/Scenarios/MultiTagSoakScenarioTests.cs
   if that file exists. If it does not, add a small file mirroring the
   demo-baseline tests, scoped to the inter-run Home assertion.

5. Add a regression-guard test under
   tests/InspectionPrototype.Tests/Scenarios/ScenarioLayeringTests.cs:
       - Use reflection or a source-text grep to assert no call site under
         InspectionPrototype.Application.Scenarios mutates AppState.WorkflowState
         via state.Update. Acceptable forms include scanning the compiled
         IL for a string literal "WorkflowState =" inside the Scenarios
         namespace, or running a Roslyn analyzer in the test, or reading
         the .cs source files at test time and grepping. Pick whichever
         is simplest given the existing test infrastructure.
       - The test passes today (after deliverables 1-2) and exists to
         prevent the next agent from re-introducing the hack.

6. Update docs/runbook/capturing-measurements.md:
   - In §4.1 "Demo baseline (row 0)", replace the current step list:
         6.  Click Start Run.                      Wait until the run completes.
         7.  Repeat step 6 continuously until the 10:00 mark on the stopwatch.
         8.  If a run is in progress at 10:00, click Stop; otherwise skip.
         9.  Click Disconnect.
     with:
         6.  Click Start Run.                      Wait until the run completes.
         7.  Click Home.                           Wait until homing completes.
         8.  Repeat steps 6-7 continuously until the 10:00 mark on the stopwatch.
         9.  If a run is in progress at 10:00, click Stop; otherwise skip.
        10.  Click Disconnect.
     Renumber any references to step 6/7/8 elsewhere in the same section.
     Add a one-sentence note immediately above the step list:
       "Note: Start Run is disabled after a run reaches Completed
        (CommandGuards.CanStart requires Idle | Ready). Home returns the
        workflow to Ready."

   - In §4.2 "Multi-tag soak", apply the equivalent step-list correction.
     The §4.2 list has a similar shape (Connect → Refresh → Load → Home →
     Start → Repeat); insert the inter-run Home and renumber. Apply the
     same one-sentence note.

   - Do NOT touch §3a, §4.3+, §5, or any other section. Pass 2 owns §3a.

## Constraints

- Do NOT add a Reset command or relax CanStart. The point of this pass is
  that the existing manual flow already has the right path; the scenario
  was just skipping it.
- Do NOT change ScenarioStepHelper, ScenarioContext, or the IOperatorCommands
  interface.
- Do NOT touch the orchestrator script or any tooling.
- Do NOT capture row 0b. That lands in Pass 3 after the
  ObjectDisposedException fix.
- The DefaultStepTimeout (30s) for the inter-run Home wait is correct and
  does NOT need to change. Homing completes well under 30s in the simulator.
- The scenarios still receive ScenarioContext via RunAsync — do not change
  that signature.

## Verification before you report done

  dotnet build --configuration Release
  dotnet test --configuration Release

Manual smoke test:
  - tools/Capture-Measurements.ps1 -Scenario DemoBaseline -Duration 60 `
       -OutputCsv docs/captures/_smoke.csv `
       -CommitHash $(git rev-parse --short HEAD)
    Confirm:
      * the app drives one or more complete cycles
      * no exception fires from the scenario
      * runs.completed in the printed row block is at least 2 (proves the
        loop iterated past the first run via the new Home step)
    Delete docs/captures/_smoke.csv before commit.
  - The app will still exit with code -532462766 in scenario mode after
    completion. That is item 3, fixed in Pass 3, and is allowed to remain
    after this pass.

## Report format when finished

- files modified
- confirmation that all tests pass plus new inter-run Home tests and the
  ScenarioLayeringTests regression guard
- the smoke-test stdout (the markdown row block; NOT for committing — for
  proving the scenario advances past run #1)
- a single commit hash
- commit message: "fix(scenarios): drive inter-run reset via ops.Home, correct runbook §4.1/§4.2 step lists (pass 1/3 of TASK-1.5.1)"

Pass 2 — Orchestrator partial-CSV protection and §3a UI-binding caveat

You are implementing Pass 2 of TASK-1.5.1. Pass 1 (scenario Home fix +
runbook step-list corrections) is already merged. This pass tightens
tools/Capture-Measurements.ps1 to enforce partial-CSV protection on
non-zero app exit codes, and adds the UI-binding-regression caveat to
runbook §3a. NO code changes outside tools/. NO captures.

## Authoritative references

Read these before making changes:
- docs/specs/SLICE-1.5.1-automated-capture-followups.md   (items 2 and 4)
- tools/Capture-Measurements.ps1                          (the script being tightened)
- tools/MeasurementExtraction.psm1                        (where the rename helper will live)
- docs/runbook/capturing-measurements.md                  (§3a "When to fall back to the manual procedure")

Pass 1 must be merged. Confirm by running:
  tools/Capture-Measurements.ps1 -Scenario DemoBaseline -Duration 60 `
    -OutputCsv docs/captures/_smoke.csv `
    -CommitHash $(git rev-parse --short HEAD)
and observing that runs.completed >= 2 in the printed row block. Delete the
smoke CSV before starting Pass 2 work.

## Scope of this pass

Orchestrator script behavior on non-zero exit codes, a small extracted
helper for testability, runbook §3a one-paragraph addition. Items 2 and
4 of the spec.

## Deliverables

1. In tools/MeasurementExtraction.psm1, add and export a function:

       function Move-PartialCsvOnFailure {
           [CmdletBinding()]
           param(
               [Parameter(Mandatory)][string] $CsvPath,
               [Parameter(Mandatory)][int]    $ExitCode,
               [Parameter(Mandatory)][int[]]  $KnownBenignExitCodes
           )
           # Returns $true if the CSV was renamed to .partial (caller should
           # exit non-zero); returns $false if the exit code is 0 or in the
           # benign list (caller may proceed with extraction).
           if ($ExitCode -eq 0) { return $false }
           if ($KnownBenignExitCodes -contains $ExitCode) {
               Write-Warning "App exited with KNOWN benign code $ExitCode — proceeding."
               return $false
           }
           if (Test-Path $CsvPath) {
               $partial = "${CsvPath}.partial"
               if (Test-Path $partial) { Remove-Item $partial -Force }
               Rename-Item -Path $CsvPath -NewName (Split-Path $partial -Leaf)
               Write-Error "App exited with code $ExitCode — CSV moved to $partial."
           } else {
               Write-Error "App exited with code $ExitCode — no CSV to preserve."
           }
           return $true
       }

   Update the existing Export-ModuleMember to include this function.

2. In tools/Capture-Measurements.ps1, near the parameter block, add a script
   variable:

       $KnownBenignExitCodes = @(-532462766)   # ObjectDisposedException at scenario-mode shutdown — to be removed in Pass 3

   Replace the existing block at lines 129-131:

       if ($appExitCode -ne 0) {
           Write-Warning "App exited with code $appExitCode — results may be incomplete."
       }

   with:

       $shouldFail = Move-PartialCsvOnFailure `
           -CsvPath $OutputCsv `
           -ExitCode $appExitCode `
           -KnownBenignExitCodes $KnownBenignExitCodes
       if ($shouldFail) {
           exit 1
       }

3. Add a Pester test under tests/Tools/MeasurementExtraction.Tests.ps1
   (create the directory if absent). The test exercises Move-PartialCsvOnFailure
   without launching the real app:
       - Setup: create a temp CSV with one line of fake content.
       - Test "ExitCodeZero_ReturnsFalse_NoRename": passes ExitCode 0 with
         empty allowlist; asserts the function returned $false and the
         original CSV still exists with the same content.
       - Test "BenignExitCode_ReturnsFalse_NoRename": passes ExitCode -532462766
         with allowlist @(-532462766); asserts $false and original still exists.
       - Test "UnknownExitCode_ReturnsTrue_RenamesToPartial": passes ExitCode 1
         with empty allowlist; asserts $true, original CSV no longer exists,
         a .partial file exists with the same content.
       - Test "UnknownExitCode_OverwritesExistingPartial": pre-creates a stale
         .partial file with different content; passes ExitCode 1 with empty
         allowlist; asserts the new .partial replaced the stale one.
       - Test "MissingCsv_ReturnsTrue_LogsError": deletes the CSV before the
         call; passes ExitCode 1 with empty allowlist; asserts $true, no
         .partial created (because there was nothing to preserve).
   If Pester is not installed in the dev environment, document the manual
   equivalent steps in a comment block at the top of the test file and
   skip wiring it into a CI runner — the test exists for documentation
   purposes until Pester is added project-wide.

4. In docs/runbook/capturing-measurements.md, in §3a's "When to fall back
   to the manual procedure (§3)" subsection, append a new bullet (or a
   short paragraph immediately after the existing bullets):

       - **Verifying a UI-binding regression.** The automated scenarios
         call `ICommand` instances on `MainViewModel` directly through the
         `IOperatorCommands` adapter. A binding that broke between XAML
         and the view-model — a typo in `{Binding}`, a mistyped
         `CommandParameter`, a `CanExecute` no longer raising
         `CanExecuteChanged` — will not surface in an automated capture
         even though the scenario reports success. For UI-affecting
         changes, run the manual §3 procedure as a spot-check before
         trusting an automated row.

   Place the bullet at the END of the existing list, after "observe the
   UI live while collecting." Do NOT touch any other §3a content; the
   "App exit code" subsection that documents -532462766 stays for now
   (Pass 3 removes it once the bug is fixed).

5. Smoke test the orchestrator end-to-end:
       tools/Capture-Measurements.ps1 -Scenario DemoBaseline -Duration 60 `
         -OutputCsv docs/captures/_smoke.csv `
         -CommitHash $(git rev-parse --short HEAD)
   Confirm:
       * the app exits with -532462766 (still pre-Pass-3)
       * the orchestrator emits Write-Warning naming the benign code
       * the script does NOT exit non-zero
       * the row block prints normally
   Delete docs/captures/_smoke.csv and any .partial file before commit.

## Constraints

- Do NOT empty $KnownBenignExitCodes in this pass. The known code is the
  ObjectDisposedException; it is fixed in Pass 3, and emptying the
  allowlist before that fix would make every smoke run hard-fail.
- Do NOT modify any C# code or any Application/Infrastructure file.
- Do NOT alter the rate-check section of the orchestrator (lines 148-207
  in the current file). Item 2 is about the failure-handling path, not
  the per-tag verification.
- Do NOT add a new section to the runbook. The UI-binding caveat is one
  bullet inside §3a's existing "When to fall back" list.
- Pester tests are nice-to-have if Pester is already in use; if not,
  document the manual verification in the test file's header comment
  rather than introducing a new tool dependency in this pass.

## Verification before you report done

  dotnet build --configuration Release
  dotnet test --configuration Release

Plus:
  - the smoke run in deliverable 5 succeeds
  - if Pester is available: Invoke-Pester tests/Tools/MeasurementExtraction.Tests.ps1
    passes all five tests
  - the §3a runbook addition renders correctly (no broken markdown)

## Report format when finished

- files created and modified
- confirmation that all C# tests pass (still 311+ depending on Pass 1)
- the smoke-test transcript showing the benign-code warning path
- the Pester test results if Pester ran, or a note that it was deferred
- a single commit hash
- commit message: "feat(tools): enforce partial-CSV protection on app failure; add UI-binding caveat to §3a (pass 2/3 of TASK-1.5.1)"

Pass 3 — ObjectDisposedException fix and row 0b capture

You are implementing Pass 3 of TASK-1.5.1, the final pass. Passes 1 and 2
are already merged. This pass debugs and fixes the ObjectDisposedException
at scenario-mode shutdown, empties the orchestrator's $KnownBenignExitCodes
allowlist, removes the §3a "App exit code" subsection that documents the
known-benign code, captures row 0b under the now-clean DemoBaselineScenario,
appends the row block, and updates session-handoff documents.

## Authoritative references

Read these before making changes:
- docs/specs/SLICE-1.5.1-automated-capture-followups.md   (items 3 and 5)
- src/InspectionPrototype.Infrastructure/Simulator/SimulatedTagSource.cs   (the suspected origin of Dispose-after-Dispose)
- src/InspectionPrototype.App/App.xaml.cs                                    (host shutdown sequence)
- src/InspectionPrototype.Application/Services/TagStreamPipelineService.cs   (consumer that may hold references through the writer)
- tools/Capture-Measurements.ps1                                             (allowlist to empty)
- docs/runbook/capturing-measurements.md                                     (§3a "App exit code" subsection to remove)
- docs/reviews/phase-1-measurements.md                                       (row 0a header to mirror for row 0b)
- CLAUDE.md, docs/reviews/roadmap-progress.md                                (session-handoff updates)

## Scope of this pass

Bug investigation, fix at the smallest reasonable scope, allowlist cleanup,
runbook subsection removal, row 0b capture, session-handoff updates. NO
new features, NO scenario changes (Pass 1 owned those).

## Deliverables

1. Reproduce and capture the failing stack:
   - dotnet build --configuration Release
   - launch with --scenario Fake --duration 5 under a debugger (Visual
     Studio "Debug -> Attach to Process" with managed exception breakpoint
     enabled, OR `dotnet run` with `set DOTNET_DbgEnableMiniDump=1` and
     read the resulting dump in WinDbg/dotnet-dump)
   - capture the full ObjectDisposedException stack and identify:
       a. the type whose Dispose is being called twice (or whose member
          is being touched after Dispose)
       b. whether the second call originates from IHost.StopAsync, the
          using/await using of a CTS, or a finalizer
   - paste the captured stack into the commit message body for permanent
     record. This is non-negotiable: a fix without a stack is guesswork.

2. Fix at the smallest scope that makes the stack stop firing. The two
   most likely shapes:
   a. Add a private `bool _disposed` to SimulatedTagSource, guard
      Dispose with an early return when already true, set it inside the
      lock that already protects shutdown. Existing pattern in the
      codebase (search for `_disposed` in src/) likely already shows
      the idiom — match it.
   b. Reorder App.xaml.cs's OnExit so the host's StopAsync completes
      before any singleton owned by the host is disposed by the WPF
      shutdown path. If the bug is in this category, the fix is in
      OnExit only and SimulatedTagSource stays as-is.

   Pick (a), (b), or whichever the stack clearly points at. Do NOT do
   both. Do NOT add a `try { } catch (ObjectDisposedException) { }` —
   suppressing the symptom is what got the fix delayed in the first
   place.

3. Add a regression test under
   tests/InspectionPrototype.Tests/Infrastructure/SimulatedTagSourceDisposalTests.cs
   (or extend an existing infra test if one already covers SimulatedTagSource):
   - Construct a SimulatedTagSource with a minimal IOptionsMonitor and
     ISimulatorProfileProvider stub.
   - Call Dispose twice.
   - Assert no exception is thrown on the second call.
   - If the fix turned out to be (b) host shutdown ordering, replace this
     with a test that asserts OnExit's await StopAsync completes before
     any host-scope Dispose runs (this is harder to test without a
     custom IHost — if the test is non-trivial, document it as a manual
     verification in the runbook §3a App-exit-code subsection's removal
     commit instead, and skip the unit test).

4. In tools/Capture-Measurements.ps1, change:

       $KnownBenignExitCodes = @(-532462766)   # ObjectDisposedException at scenario-mode shutdown — to be removed in Pass 3

   to:

       $KnownBenignExitCodes = @()             # empty after TASK-1.5.1 Pass 3 fix; fail loudly on any non-zero code

5. In docs/runbook/capturing-measurements.md, REMOVE the §3a subsection
   that begins "### App exit code" (the paragraph documenting -532462766
   as a known-benign code). Do NOT remove the rest of §3a. The
   subsection is no longer accurate after deliverable 2.

6. Run row 0b capture:
       $date = Get-Date -Format 'yyyy-MM-dd'
       tools/Capture-Measurements.ps1 -Scenario DemoBaseline -Duration 600 `
         -OutputCsv "docs/captures/demo-baseline-automated-row-0b-$date.csv" `
         -CommitHash $(git rev-parse --short HEAD) `
         -OperatorDelayMs 0
   Confirm:
       * the app exits with code 0 (the whole point of deliverable 2)
       * runs.completed is in the 35-50 range — meaningfully closer to row
         0's 41 than row 0a's 57, because the inter-run Home from Pass 1
         now consumes wall-clock time per cycle
       * telemetry rate is within ±5% of row 0's 4.83 Hz

   If runs.completed lands outside 35-50, do NOT panic — document the
   number and the simulator profile timing and proceed. The 35-50 band
   is a sanity check, not a hard acceptance criterion. The row exists to
   be the new reference; whatever the corrected scenario produces, that
   is row 0b.

7. Append row 0b block to docs/reviews/phase-1-measurements.md:
   - new "### Row 0b — demo baseline (automated, post-Home-fix reference)"
     header BETWEEN the existing "### Row 0a" notes block and the
     "## Phase 1 rows" section
   - mirror the row 0a header (Scenario / Capture / Commit / Date lines);
     scenario tag is "§3a Demo baseline automated (10 min, Normal profile,
     operator-delay 0, post-TASK-1.5.1)"
   - 16-metric table with Slice column "demo-baseline-automated-0b
     (pre-Phase-1)" and Capture method naming the new CSV file
   - "### Notes on row 0b" block:
       (a) why row 0b exists — the inter-run Home step from TASK-1.5.1
           Pass 1 changes per-run cadence, so row 0a no longer represents
           the scenario the script actually runs
       (b) which row Phase 1 deltas now compare against — row 0b for
           every SLICE-1.2 onward capture; row 0a stays as historical
           evidence of the broken-scenario state
       (c) the runs.completed delta vs row 0 and vs row 0a, with one
           sentence explaining whichever direction it moved

8. Append a one-line note to "### Notes on row 0a" (without rewriting the
   existing notes):

       - **Superseded by row 0b** as of <YYYY-MM-DD>. Row 0a captured a
         scenario that skipped the inter-run Home step (TASK-1.5.1 Pass 1
         fixed the scenario; TASK-1.5.1 Pass 3 captured row 0b under the
         corrected behaviour). Row 0a is preserved unchanged.

9. Update CLAUDE.md "Current position" block:
   - Phase: 1 (Simulator to scale) — SLICE-1.5.1 complete
   - Last completed slice: TASK-1.5.1 Pass 3 — fixed
     ObjectDisposedException at scenario shutdown, captured row 0b,
     emptied allowlist, removed §3a App-exit-code subsection;
     commit <hash>
   - Next action: SLICE-1.2 (Real frame payloads) — first capture under
     the now-clean automation
   - Blocked on: nothing
   - Last updated: <today's date>

10. Append a session-log entry to docs/reviews/roadmap-progress.md under
    today's date naming the row 0b file, the runs.completed number, the
    fixed exit code, and the commit hash. Mark SLICE-1.5.1 as Completed
    in the progress table.

## Constraints

- Do NOT add a try/catch around the failing Dispose call. The fix must
  prevent the exception, not swallow it.
- Do NOT introduce a new shutdown phase or restructure the host
  initialization. Item 3 is a targeted bug fix.
- Do NOT capture row 0b until deliverable 2 lands and a smoke run shows
  exit code 0. A row 0b captured against an app that still throws is
  not row 0b — it is a re-run of row 0a with extra noise.
- Do NOT rewrite row 0a. The "Superseded by row 0b" note is appended,
  not edited in.
- Do NOT modify scenarios or tooling beyond emptying the allowlist
  variable.

## Verification before you report done

  dotnet build --configuration Release
  dotnet test --configuration Release

Plus:
  - the new SimulatedTagSourceDisposalTests (if added) passes
  - --scenario Fake --duration 5 exits with code 0 (no exception)
  - --scenario DemoBaseline --duration 60 exits with code 0
  - the row 0b CSV is committed under docs/captures/
  - the row 0b block is in docs/reviews/phase-1-measurements.md
  - the row 0a "Superseded by row 0b" note is in place
  - §3a's "App exit code" subsection is gone
  - CLAUDE.md current-position block reflects SLICE-1.5.1 closure

## Report format when finished

- the captured ObjectDisposedException stack (paste in the report)
- the chosen fix scope (a or b) with one-paragraph rationale
- files created and modified
- confirmation that all tests pass plus the new disposal regression test
- the row 0b markdown block included in the report
- the runs.completed comparison: row 0 vs row 0a vs row 0b
- the docs/captures/ row-0b CSV path
- a single commit hash
- commit message: "fix(infra): eliminate ObjectDisposedException at scenario shutdown; capture row 0b (pass 3/3 of TASK-1.5.1)"

Operator notes

  • One pass per Copilot session. Same protocol as TASK-1.5.
  • Pass 1 is the riskiest for test coverage. Replacing the state.Update block with ops.Home plus a state-wait is a behavior change in the run loop; the existing scenario tests need to be updated to drive the Home transition through the fake state store, otherwise they will deadlock waiting for a transition no one publishes. The new "RunLoop_AfterCompleted_CallsHomeBeforeNextStart" test is the load-bearing one; if it isn't green, the scenario is silently broken even if the build is.
  • Pass 2 must NOT empty the allowlist. The -532462766 allowance is a bridge between Pass 1 and Pass 3; emptying it before Pass 3 makes every smoke run hard-fail and blocks Pass 3's own pre-fix smoke verification. Pass 3 owns the allowlist cleanup as part of the same commit that lands the fix.
  • Pass 3's fix without a captured stack is guesswork. The deliverable list calls it out explicitly; if the agent skips the stack capture and proposes a fix from "likely cause" reasoning alone, bail and finish manually. The bug has been latent long enough that the cost of one more debugger session is small.
  • Row 0b's runs.completed is the load-bearing acceptance signal, not its absolute value. The 35-50 band is a sanity check; if the actual number is 33 or 52, that is fine and the row stands. The comparison-with-row-0 footnote is what matters for SLICE-1.2's deltas.
  • 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.