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
_disposedInterlocked guard onSimulatedTagSource(and the matching guard onSimulatedCamera) 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.
- Status: Superseded by SLICE-1.6 (FlaUI capture, shipped)
- Date: 2026-04-25
- Spec: SLICE-1.5.1: Automated Capture Follow-ups
- Depends on: TASK-1.5: Implement Automated Measurement Capture
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
AppStatemutation inDemoBaselineScenarioandMultiTagSoakScenariowithops.Home.Execute(null)plus the existingWaitForStateAsynchelper - correct the
§4.1and§4.2step lists indocs/runbook/capturing-measurements.mdto 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
$KnownBenignExitCodesallowlist totools/Capture-Measurements.ps1and rename the CSV to.partialplus exit non-zero on any non-allowed non-zero code - add a small
Test-PartialRenameOnFailurePester 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 offendingDisposecall, fix at the smallest reasonable scope (most likely a_disposedflag inSimulatedTagSourceor a host shutdown ordering change inApp.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
DemoBaselineScenarioand append a row block todocs/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.CanStartor 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-counterswith 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 thestate.Updateblock withops.Homesrc/InspectionPrototype.Application/Scenarios/MultiTagSoakScenario.cs— same correctionsrc/InspectionPrototype.Infrastructure/Simulator/SimulatedTagSource.cs—_disposedguard 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 reorderingtests/InspectionPrototype.Tests/Scenarios/DemoBaselineScenarioTests.cs— update fake transitions to expect Home between runstools/Capture-Measurements.ps1—$KnownBenignExitCodes, partial-rename branchtools/MeasurementExtraction.psm1— extract a smallMove-PartialCsvOnFailurefunction 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.
- Scenario Home fix and runbook step-list corrections — items 1 and 2 of the spec. Replace the
state.Updatehack in both scenarios withops.Home, update the existing scenario tests, fix the §4.1 and §4.2 step lists. No tooling work, no captures. - Orchestrator partial-CSV protection and §3a UI-binding caveat — items 2 and 4 of the spec.
$KnownBenignExitCodesallowlist with-532462766as the only initial entry, partial-rename branch with a Pester smoke test, §3a paragraph addition. No code changes outsidetools/. ObjectDisposedExceptionfix and row 0b capture — items 3 and 5 of the spec. Debug, fix at the smallest scope, empty the$KnownBenignExitCodesallowlist, 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.Updateblock withops.Homeplus 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
-532462766allowance 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.