# Campaign LA11 — automated preflight and connected user gate **Status:** implementation checkpoint only. Run this script after the reviewed LA10/LA11 commits are integrated and the campaign branch is clean. Campaign LA, the Linux graphical client, and issue #397 remain open until the user records a verdict for every applicable row below. This is the single Campaign LA operator script. The automated section is display-free and connection-free. Rows A–I are deliberately manual and serial: they use real retail DATs, a local ACE server, user-entered credentials, and visual judgment that automation cannot supply. ## 1. Safety boundary and required inputs Use placeholders throughout; never paste a password into a terminal, this document, a screenshot, or a gate report. - ``: a clean Campaign LA worktree at the exact commit under test. - ``: a read-only source containing `client_portal.dat`, `client_cell_1.dat`, `client_highres.dat`, and `client_local_English.dat`. - ``, ``, and ``: a local ACE endpoint and account. Enter the account password only in the launcher's masked Password field. The launcher intentionally stores it as plaintext in the **isolated** `launcher-profiles.json`; children receive it through redirected stdin. - ``: a second user-controlled character that can observe a private `/tell` from each play mode. - ``: a server-operator-approved disposable character. Never substitute a primary character. If none exists, provision one with the local server's normal admin procedure before row G. - Windows 11 x64, PowerShell 7, .NET 10 SDK, a local ACE server, and a supported Vulkan Windows machine for rows A–H. Ubuntu x64 with PowerShell 7 and a Linux desktop/WSLg is required for row I. The Avalonia launcher is supported on Linux; `gui` and `guiSelect` **client** actions must remain disabled with the Modern Runtime Slice-L explanation. Close every unrelated `AcDream.App`, `acdream-headless`, and acdream launcher before starting. Do not run another acdream gate in parallel. All generated files must stay below one new gate directory; the canonical `%APPDATA%`, `%LOCALAPPDATA%`, and XDG acdream roots are out of scope. ## 2. Automated preflight — no UI, connection, credential, or bake In PowerShell 7 on Windows: ```powershell $Repo = [IO.Path]::GetFullPath('') $Stamp = [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss') $Gate = Join-Path $Repo "logs/campaign-la-user-gate-$Stamp" $Preflight = Join-Path $Gate 'automated-preflight' New-Item -ItemType Directory -Path $Gate | Out-Null pwsh -NoProfile -File (Join-Path $Repo 'tools/run-campaign-la-preflight.ps1') ` -Repository $Repo ` -OutputDirectory $Preflight $Report = Get-Content -LiteralPath (Join-Path $Preflight 'report.json') -Raw | ConvertFrom-Json if (-not $Report.success -or $Report.dirty) { throw 'Stop: automated preflight failed or recorded a dirty worktree.' } if ($Report.head -cne (git -C $Repo rev-parse HEAD).Trim()) { throw 'Stop: preflight HEAD does not equal the current HEAD.' } ``` The expected matrix is: | Platform | Automated command group | Required result | Typical time | |---|---|---|---:| | Windows | Release `AcDream.slnx` build, `-m:1` | exit 0 | 5–15 min | | Windows | complete Release solution test, serial | exit 0; ordinary known skips only | 20–60 min | | Windows | focused Launcher.Core update tests and launcher update/startup-option tests | exit 0 | 1–4 min | | Windows | canonical portable project build/test closure from `headless-portability.yml` | every project exits 0 | 10–25 min | | Windows | self-contained single-file launcher publish for `win-x64` and `linux-x64` | launcher + bake roots present, no root DLL fallback | 3–10 min | | Windows | native launcher `--verify-publish` and bake `--help` with bogus `DOTNET_ROOT*` | both exit 0 | <1 min | | Ubuntu/WSL | run the same helper natively from the Linux path to the worktree | Linux RID report and every row exit 0 | 35–90 min | `report.json` records the tested HEAD/dirty state, OS/RID, exact commands, durations, exits, redacted logs, and SHA-256/size inventory. A normal preflight plans 26 commands. It never launches App or Headless in connected mode and never reads a credential. ### Optional installed-DAT read-only row This is not a bake and must not replace row A. Add the switches below only when the DAT directory may be read by tests: ```powershell pwsh -NoProfile -File (Join-Path $Repo 'tools/run-campaign-la-preflight.ps1') ` -Repository $Repo ` -OutputDirectory (Join-Path $Gate 'automated-preflight-with-dat') ` -IncludeInstalledDat ` -InstalledDatDirectory '' ``` The mandatory installed-DAT result is `CharacterManagementLiveDatTests` with both `ACDREAM_PROBE_LIVE_MOUNT=1` and `ACDREAM_DAT_DIR` set inside the child environment. The helper reads the TRX and fails if the test skipped or did anything other than pass. The action-map and portal-asset probes are additional coverage, never substitutes. Expected matrix size: 30 rows. On Ubuntu/WSL, invoke the same script with native `pwsh`, a Linux repository path, and a Linux output path. Do not treat a Windows-hosted run over `wsl.exe` as the Linux row. ## 3. Prepare the deterministic local A/B feed Build distinct, version-stamped payloads so the staged launcher really changes from A to B. These commands write only below `$Gate` (normal project `obj/bin` incremental outputs are the already-authorized build outputs): ```powershell $VersionA = '1.0.1-la11.a' $VersionB = '1.0.1-la11.b' $Payloads = Join-Path $Gate 'update-payloads' $Fixture = Join-Path $Gate 'update-fixture' function Publish-LaRelease([string]$Version, [string]$Label) { $ClientWin = Join-Path $Payloads "$Label/client-win-x64" $LauncherWin = Join-Path $Payloads "$Label/launcher-win-x64" $ClientLinux = Join-Path $Payloads "$Label/client-linux-x64" $LauncherLinux = Join-Path $Payloads "$Label/launcher-linux-x64" dotnet publish (Join-Path $Repo 'src/AcDream.App/AcDream.App.csproj') ` -c Release -r win-x64 --self-contained true -p:Version=$Version ` -o $ClientWin --nologo if ($LASTEXITCODE) { throw "App win-x64 publish failed: $Label" } dotnet publish (Join-Path $Repo 'src/AcDream.Headless/AcDream.Headless.csproj') ` -c Release -r win-x64 --self-contained true -p:Version=$Version ` -o $ClientWin --nologo if ($LASTEXITCODE) { throw "Headless win-x64 publish failed: $Label" } dotnet publish (Join-Path $Repo 'src/AcDream.Launcher/AcDream.Launcher.csproj') ` -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true ` -p:Version=$Version -o $LauncherWin --nologo if ($LASTEXITCODE) { throw "Launcher win-x64 publish failed: $Label" } dotnet publish (Join-Path $Repo 'src/AcDream.App/AcDream.App.csproj') ` -c Release -r linux-x64 --self-contained true -p:Version=$Version ` -o $ClientLinux --nologo if ($LASTEXITCODE) { throw "App linux-x64 publish failed: $Label" } dotnet publish (Join-Path $Repo 'src/AcDream.Headless/AcDream.Headless.csproj') ` -c Release -r linux-x64 --self-contained true -p:Version=$Version ` -o $ClientLinux --nologo if ($LASTEXITCODE) { throw "Headless linux-x64 publish failed: $Label" } dotnet publish (Join-Path $Repo 'src/AcDream.Launcher/AcDream.Launcher.csproj') ` -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true ` -p:Version=$Version -o $LauncherLinux --nologo if ($LASTEXITCODE) { throw "Launcher linux-x64 publish failed: $Label" } } Publish-LaRelease $VersionA 'A' Publish-LaRelease $VersionB 'B' pwsh -NoProfile -File (Join-Path $Repo 'tools/new-campaign-la-update-fixture.ps1') ` -OutputDirectory $Fixture ` -ClientWinX64DirectoryA (Join-Path $Payloads 'A/client-win-x64') ` -LauncherWinX64DirectoryA (Join-Path $Payloads 'A/launcher-win-x64') ` -ClientLinuxX64DirectoryA (Join-Path $Payloads 'A/client-linux-x64') ` -LauncherLinuxX64DirectoryA (Join-Path $Payloads 'A/launcher-linux-x64') ` -ClientWinX64DirectoryB (Join-Path $Payloads 'B/client-win-x64') ` -LauncherWinX64DirectoryB (Join-Path $Payloads 'B/launcher-win-x64') ` -ClientLinuxX64DirectoryB (Join-Path $Payloads 'B/client-linux-x64') ` -LauncherLinuxX64DirectoryB (Join-Path $Payloads 'B/launcher-linux-x64') ``` The helper rejects nonempty output, invalid or non-monotonic versions, missing root executables, and nonabsolute inputs. It writes fixed-timestamp sorted ZIPs, the exact LA10 v1 SHA/size manifest, `fixture-report.json`, a loopback-only server, and an A/B selector. It does not download or mutate payload sources. Start the Windows loopback server without a shell or visible helper window: ```powershell $ServerInfo = [Diagnostics.ProcessStartInfo]::new() $ServerInfo.FileName = (Get-Command pwsh).Source $ServerInfo.UseShellExecute = $false $ServerInfo.CreateNoWindow = $true foreach ($Value in @( '-NoProfile', '-File', (Join-Path $Fixture 'serve-fixture.ps1'), '-Root', $Fixture, '-Port', '43119')) { $ServerInfo.ArgumentList.Add($Value) } $FixtureServer = [Diagnostics.Process]::Start($ServerInfo) $ManifestUri = 'http://127.0.0.1:43119/manifest.json' if ((Invoke-RestMethod -Uri $ManifestUri).version -cne $VersionA) { throw 'Stop: local fixture did not begin on release A.' } ``` ## 4. Windows isolated launcher command and evidence rule ```powershell $WinRoot = Join-Path $Gate 'windows-roots' $WinConfig = Join-Path $WinRoot 'config' $WinData = Join-Path $WinRoot 'data' $WinCache = Join-Path $WinRoot 'cache' $Evidence = Join-Path $Gate 'evidence' New-Item -ItemType Directory -Path $Evidence | Out-Null $LauncherA = Join-Path $Payloads 'A/launcher-win-x64/acdream-launcher.exe' $LauncherArguments = @( '--config-dir', $WinConfig, '--data-dir', $WinData, '--cache-dir', $WinCache, '--update-manifest-uri', $ManifestUri) & $LauncherA @LauncherArguments ``` All four options are process-local. The three roots are an indivisible set; the local feed reaches only the updater and is never persisted. A self-update must preserve the same validated suffix through helper and confirmation restarts. The launcher, profiles, installer, current-version store, updater, session composer, and orchestrator must all use this one exact path set. For every play/probe row, copy the session id shown in the launcher's Sessions list into ``, then run: ```powershell $Status = Join-Path $WinCache 'launcher/sessions//status.jsonl' pwsh -NoProfile -File (Join-Path $Repo 'tools/test-campaign-la-session-status.ps1') ` -StatusFile $Status ` -Mode '' ` -ExpectedSessionId '' ` -ReportPath (Join-Path $Evidence '-status.validation.json') ``` Add `-ExpectedPlugin acdream.smoke` to rows D–F. The validator enforces exact v1 fields **and property order**, one session id, UTC monotonic timestamps, mode-specific lifecycle order, exit code 0/reason, no unexpected plugin/login command failure, credential redaction, and no surviving App/Headless process. Its report contains event names and a hash, not account, character, command, or error payloads. Keep raw `session.json`/`status.jsonl` local; never upload them. ## 5. Serial Windows user rows A–H ### A — isolated first run, real DAT bake, and release-A client baseline 1. Confirm the launcher opens First-run setup and all launch buttons are unavailable. Save a redacted screenshot as `A-first-run-required.png`. 2. Enter `` in the wizard, select a sensible worker count, and click **Validate**. Confirm all four DATs pass. 3. Click **Build and install**. Do not cancel or close the launcher. The real bake may take 30–180 minutes. Confirm every phase reaches **Completed** and the status says `Client content installed and verified. Launch is enabled.` 4. Open **Check for updates**. Confirm available release A, click **Install client**, and wait for `Client update installed and activated.` Do not stage launcher A; the test launcher already has version A. 5. Confirm these exact isolated artifacts exist and no `.previous-install` remains after success: ```powershell $RequiredA = @( (Join-Path $WinData 'install.json'), (Join-Path $WinData 'pak/acdream.pak'), (Join-Path $WinData 'app/current.json')) foreach ($Path in $RequiredA) { if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Missing $Path" } } $RequiredA | ForEach-Object { $Item = Get-Item -LiteralPath $_ [ordered]@{ name = $Item.Name size = $Item.Length sha256 = (Get-FileHash -LiteralPath $_ -Algorithm SHA256).Hash.ToLowerInvariant() } } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Evidence 'A-install-hashes.json') ``` Do not copy `install.json` into shared evidence because it records the local DAT path. Expected time: 45–200 minutes including bake. ### B — server/account CRUD entirely through the UI 1. Add `` at `127.0.0.1:`, edit its name and port, then remove it. Confirm Cancel/Escape makes no mutation. 2. Add `` at `127.0.0.1:`. 3. Under it add `` with a user-invented throwaway field value, edit its account name/value, then remove it. Do not reuse a real password for this temporary row. 4. Add `` and enter its real password only in the masked field. 5. Close and reopen the launcher with the **same** `$LauncherArguments`. Confirm only the real server/account persisted. Save redacted before/reopen images as `B-crud-before-reopen.png` and `B-crud-after-reopen.png`. 6. Record only the profile file's size/hash, never its contents: ```powershell $Profile = Join-Path $WinConfig 'launcher-profiles.json' $Item = Get-Item -LiteralPath $Profile [ordered]@{ size = $Item.Length sha256 = (Get-FileHash -LiteralPath $Profile -Algorithm SHA256).Hash.ToLowerInvariant() } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Evidence 'B-profile-hash.json') ``` Expected time: 10–15 minutes. ### C — live character probe twice, no stale ACE session 1. Select ``, click **Refresh characters**, and wait for the probe row to finish. Confirm the roster appears without entering world. 2. Run the validator in `probe` mode for its session id. Confirm its exact event order is `started, connected, characterList, disconnected, exited`, with no `enteredWorld`, terminal code 0, and terminal reason `probe`. 3. In the ACE console/session administration view, confirm the account is no longer logged in. Save a redacted `C-probe-1-ace-cleared.png`. 4. Repeat steps 1–3 immediately, producing a different session id, `C-probe-2-status.validation.json`, and `C-probe-2-ace-cleared.png`. 5. Confirm Refresh is re-enabled and no `acdream-headless` process remains. Expected time: 5–10 minutes. A stale ACE account or timeout is a gate failure; do not wait three minutes and call the next attempt a pass. ### D — `guiSelect`, retail character screen, plugin, and login command 1. Select one non-disposable roster character. Set its mode to `guiSelect`, Plugins to exactly `acdream.smoke`, and its one login command to `/tell , LA11-D-`. Save settings. 2. Click **GUI — character select**. Confirm the flat retail character list, selection highlight, Enter button, Delete/Restore swap state, dialogs, and absence of any invented rotating 3D preview. Save redacted `D-character-select.png`. 3. Select the configured character and enter world. Confirm the observer gets the exact D nonce once. Save `D-observer-tell.png` with names redacted. 4. Click **Stop** in the launcher. Confirm the game closes gracefully and ACE releases the account. Validate `guiSelect` with `-ExpectedPlugin acdream.smoke`. Expected time: 5–10 minutes. ### E — direct `gui`, plugin, and login command 1. Change the same character to `gui`, retain `acdream.smoke`, and change the command nonce to `LA11-E-`. 2. Click **GUI — enter world**. Confirm it selects the exact cached character, reaches the world, loads the plugin once, and the observer gets the E nonce once. 3. Stop from the launcher, confirm ACE logout, and validate `gui` with the expected plugin. Save `E-world.png`, `E-observer-tell.png`, and `E-status.validation.json` with identifying text redacted. Expected time: 5–10 minutes. ### F — headless, plugin/login command, and connected #397 acceptance 1. Change the same character to `headless`, retain `acdream.smoke`, and use `LA11-F-`. 2. Click **Headless**. Confirm `pluginLoaded(acdream.smoke)`, `enteredWorld`, and the observer's single exact F nonce. 3. Click **Stop** once. On Windows this must target that child's distinct process group with `CTRL_BREAK`; it must reach `disconnected` then `exited(code:0, reason:graceful)` before the timeout, without a hard kill. ACE must release the account immediately and the launcher must stay open. 4. Validate `headless` with the expected plugin and save `F-status.validation.json` plus redacted ACE-clear evidence. The real automated fixture separately proves complex argv and redirected stdin survive native `CreateProcessW`, the target receives `CTRL_BREAK`, a sibling process group receives nothing, exit 0 precedes timeout, and `Kill` is never called. This connected row proves the actual ACE graceful-logout half. Issue #397 remains open if either half is missing. Expected time: 5–10 minutes. ### G — disposable delete and restore 1. Launch `guiSelect` for ``. Do not enter world. 2. Confirm ordinary selection enables Enter/Delete and disables Restore. Click Delete, inspect the retail confirmation dialog, cancel once, and confirm no state change. 3. Delete again and confirm. Verify the wait dialog, greyed roster row/countdown, disabled Enter/Delete, and enabled Restore. Save `G-deleted.png`. 4. Click Restore and confirm the same GUID returns to ordinary state with Enter/Delete enabled and Restore disabled. Save `G-restored.png`. 5. Close through launcher **Stop**, confirm graceful terminal status and ACE release. Validate with: ```powershell pwsh -NoProfile -File (Join-Path $Repo 'tools/test-campaign-la-session-status.ps1') ` -StatusFile (Join-Path $WinCache 'launcher/sessions//status.jsonl') ` -Mode guiSelect ` -ExpectNoEnteredWorld ` -ExpectedSessionId '' ` -ExpectedPlugin acdream.smoke ` -ReportPath (Join-Path $Evidence 'G-status.validation.json') ``` If restore fails, stop the row, preserve evidence, and restore only that disposable character with the server's normal admin recovery. Never continue with another character. Expected time: 5–10 minutes. ### H — local A→B client update, active-session refusal, rollback, self-update 1. Record release A's `app/current.json`. Start one headless session and wait for `enteredWorld`. 2. Switch the fixture atomically to B: ```powershell pwsh -NoProfile -File (Join-Path $Fixture 'set-active-release.ps1') ` -Release B -Root $Fixture if ((Invoke-RestMethod -Uri $ManifestUri).version -cne $VersionB) { throw 'Stop: fixture did not switch to B.' } ``` 3. Open **Check for updates** and **Check again**. While the session is active, confirm Install client, Rollback client, and Stage launcher are disabled or refuse without changing `app/current.json`. Save `H-active-refusal.png`. 4. Stop the headless session and validate its graceful status. Install client B. Confirm `app/current.json` names B, A is previous, all installed-file hashes verify, and new sessions resolve from the B directory. 5. Click **Rollback client**. Confirm A becomes current and B becomes previous. Check again and install B once more, leaving B current. Save sanitized copies of the three pointer states as `H-pointer-a.json`, `H-pointer-b.json`, and `H-pointer-rollback-a.json`; they contain no credentials. 6. Click **Stage launcher**. Confirm restart is required, then close the launcher normally. The copied helper must apply B and restart the launcher with the same config/data/cache/feed suffix. 7. Confirm profiles, install record, and update state still come from the isolated roots; `campaign-la-fixture-release.txt` beside the relaunched executable says `release=B`; `launcher-update/pending.json` is gone; and no transaction backup remains. Check again and confirm launcher B is current. Save `H-self-update-confirmed.png` and a hash-only post-state inventory. Never edit a manifest to force this row and never point the launcher at a non-loopback HTTP endpoint. Expected time: 15–30 minutes. ## 6. Row I — native Ubuntu/WSL launcher, XDG-shaped isolated roots Stop the Windows launcher and fixture server only after every Windows session is terminal: ```powershell if (-not $FixtureServer.HasExited) { $FixtureServer.Kill() $FixtureServer.WaitForExit() } ``` In a native Ubuntu/WSL PowerShell 7 terminal, set Linux paths. The repository and fixture may be read from a mounted Windows path, but roots must live on the Linux filesystem. Run the generated server natively so its `127.0.0.1` URLs cannot escape the Linux environment: ```powershell $RepoLinux = [IO.Path]::GetFullPath('') $FixtureLinux = [IO.Path]::GetFullPath('') $PayloadsLinux = [IO.Path]::GetFullPath('') $LinuxGate = [IO.Path]::GetFullPath('') $env:XDG_CONFIG_HOME = Join-Path $LinuxGate 'xdg-config-home' $env:XDG_DATA_HOME = Join-Path $LinuxGate 'xdg-data-home' $env:XDG_CACHE_HOME = Join-Path $LinuxGate 'xdg-cache-home' $LinuxConfig = Join-Path $env:XDG_CONFIG_HOME 'acdream' $LinuxData = Join-Path $env:XDG_DATA_HOME 'acdream' $LinuxCache = Join-Path $env:XDG_CACHE_HOME 'acdream' $LinuxEvidence = Join-Path $LinuxGate 'evidence' New-Item -ItemType Directory -Path $LinuxEvidence | Out-Null pwsh -NoProfile -File (Join-Path $FixtureLinux 'set-active-release.ps1') ` -Release A -Root $FixtureLinux ``` Start `serve-fixture.ps1 -Root $FixtureLinux -Port 43119` in a dedicated native terminal and leave it running. In another terminal: ```powershell $LauncherLinuxA = Join-Path $PayloadsLinux 'A/launcher-linux-x64/acdream-launcher' & $LauncherLinuxA ` --config-dir $LinuxConfig ` --data-dir $LinuxData ` --cache-dir $LinuxCache ` --update-manifest-uri 'http://127.0.0.1:43119/manifest.json' ``` Complete this exact serial matrix: 1. **Manual-DAT first run:** enter ``; auto-detection may be empty by design. Validate, bake to `$LinuxData/pak/acdream.pak`, verify, then install release-A client. 2. **CRUD:** add/edit/remove a temporary server and account entirely in the launcher, then add the real Linux-reachable ACE profile. Enter its password only in the masked field. Restart and confirm persistence. Run `stat -c '%a' "$LinuxConfig/launcher-profiles.json"`; the exact result must be `600`. 3. **Probe twice:** run Refresh twice, validate both status streams in `probe` mode with native `pwsh`, and confirm ACE clears the account after each. 4. **Platform posture:** confirm GUI and GUI-select client buttons are disabled and show the explicit Modern Runtime Slice-L message. Do not bypass this disablement and do not claim a Linux graphical-client gate. 5. **Headless:** configure `acdream.smoke` and `/tell , LA11-I-`, launch, observe the tell, click Stop, and validate `headless` + expected plugin. Native Linux sends SIGINT and must reach graceful terminal status with no process leak. 6. **Update:** switch the native fixture to B, prove update actions refuse while a headless session is active, stop it gracefully, install B, rollback to A, reinstall B, stage launcher B, and close normally. Confirm the relaunched binary's B marker, preserved explicit roots/feed, cleaned pending journal, and executable owner bits on App, Headless, Launcher, and Bake. Copy only redacted screenshots, validation reports, pointer JSON, hashes, and file-mode results into `$LinuxEvidence`. Keep the Linux profile and raw session files local. Expected time: 60–220 minutes, dominated by the real bake. ## 7. Evidence, redaction, verdict, and cleanup Expected evidence tree: ```text logs/campaign-la-user-gate-/ automated-preflight/report.json automated-preflight/commands/*.log automated-preflight/publish/{win-x64,linux-x64}/... update-fixture/fixture-report.json update-fixture/{A,B}/manifest.json evidence/A-install-hashes.json evidence/B-*.png evidence/C-probe-{1,2}-status.validation.json evidence/D-*.png + D-status.validation.json evidence/E-*.png + E-status.validation.json evidence/F-*.png + F-status.validation.json evidence/G-*.png + G-status.validation.json evidence/H-*.png + H-pointer-*.json evidence/I-*.png + I-status.validation.json + I-modes.txt verdict.json ``` Before sharing evidence: - remove or mask account names, character names, DAT paths, hostnames other than loopback, and server-admin identifiers from screenshots; - never copy `launcher-profiles.json`, raw session configs/status streams, stdout/stderr that may contain user text, or environment values; - search the shareable evidence for the exact user-entered password and any gate-only sentinel secret; the match count must be zero; - retain SHA-256 and sizes so local raw artifacts remain auditable. No additional raw child/plugin diagnostic sink is required: `pluginLoaded`, the strict terminal status, the observer's redacted tell evidence, and the automated targeted-signal fixture cover the acceptance questions without capturing credentials or arbitrary chat. Create `verdict.json` manually with schema version 1, exact tested HEAD, rows A–I as `pass`, `fail`, or `notApplicable`, a short redacted note per row, and the user's overall verdict. Row I is not applicable only when no native Ubuntu/WSL desktop is available; it blocks Campaign LA shipping under the current Linux requirement, so it cannot be silently omitted. Cleanup is graceful-first and serial: 1. Restore `` and verify it is ordinary before closing its session. 2. Stop every launcher session once; require a passing validator and ACE-clear observation. If a child survives the timeout, record the gate failure and its PID before any emergency termination. 3. Close each launcher normally, then stop only the fixture-server process created above. Do not kill ACE as a substitute for logout evidence. 4. Leave update pointers on B or roll the **isolated** client back to A through the UI; never edit pointers or journals by hand. 5. Remove the real account through the isolated launcher UI. After review, delete only the explicitly recorded `$WinConfig`/`$LinuxConfig` gate roots that held plaintext passwords, or change the test account password. Do not recursively delete a computed, empty, canonical, home, repository, or XDG parent path. 6. Preserve the redacted evidence and reports. The large isolated pak/payload trees may be removed only after resolving and checking their full paths are descendants of the recorded gate roots. Estimated total: 3–7 hours, primarily the two real DAT bakes and full serial test suites. A failure stops the current row; restore/stop/collect evidence, then diagnose before advancing. Do not mark LA11, Campaign LA, or #397 shipped until the user accepts the complete applicable matrix.