# Campaign LA — launcher / installer / updater + retail character-select **Status:** ACTIVE (started 2026-08-14) **Spec (approved):** `docs/superpowers/specs/2026-08-14-launcher-campaign-design.md` **Memory crib:** `claude-memory/project_launcher_direction.md` **Branch:** `claude/acdream-launcher-credentials-4d2f7c` (merge to main at coherent checkpoints) Campaign LA ships the alpha launcher (Avalonia, Windows + Linux): triple-duty launcher + installer + updater, ThwargLauncher-model profiles with full in-UI CRUD, plaintext credential file (user-decided), file-contract orchestration of `AcDream.App` and `AcDream.Headless`, plugins + login commands on both hosts, the headless character probe, and the retail character-select screen (no Create). All architectural decisions live in the spec — this plan sequences the work. ## Process (binding) - **Fable plans/sequences/integrates. Sonnet implements bounded slices. Opus reviews at every slice boundary, dual-lens:** (a) architectural — ownership, layering, dependency-guard integrity, seams; (b) retail fidelity vs `docs/research/named-retail/` wherever the slice touches retail behavior. Findings → fixes → narrow re-review. - Max 3–4 agents in parallel including children; subagents never spawn subagents; implementer prompts carry spec+plan paths, files-to-read, acceptance criteria, commit style. - `dotnet build` + `dotnet test` green before a slice is DONE; ≥1 commit per slice tagged `Campaign LA`; retail deviations add their `docs/architecture/retail-divergence-register.md` row in the same commit; no workarounds without explicit user approval. - Connected/visual gates are the ONLY stop-and-wait points; each gets an exact script under `docs/research/` and non-blocked slices keep moving. ## Slice map | Slice | Deliverable | Depends on | |---|---|---| | LA0 | `AcDream.Platform` extraction (`ApplicationPathSet`) + guard amendments | — | | LA1 | Launch contract: App `--session-config` + stdin credential; status.jsonl writer both hosts; roster plumbing | LA0 | | LA2 | Headless probe mode + `idle` policy | LA1 | | LA3 | `AcDream.Launcher.Core`: profile store CRUD, config composition, spawn/supervise, status reader | LA0 (LA1 contract shapes) | | LA4 | `AcDream.Launcher` Avalonia UI: CRUD views, per-char settings, sessions, probe action | LA3 | | LA5 | Plugin hosting: headless `IPluginHost` + capability flag; session-driven plugin set both hosts | LA1 | | LA6 | Login commands: parser-core extraction + execution on both hosts | LA1, LA5 | | LA7 | Character-select: Runtime selection state + wire (delete/restore/error) + no-selector flow | LA1 | | LA8 | Character-select authored retail screen (flat listbox — NO 3D preview, recon-corrected) | LA7 | | LA9 | Installer: first-run wizard (DAT locate/validate, bake w/ progress, SHA record) | LA3, LA4 | | LA10 | Updater: GitHub Releases manifest, download/verify/install/swap, self-update | LA3, LA4 | | LA11 | Closeout: connected-gate script, roadmap/CLAUDE.md/memory, program ledger | all | Parallelism guide: LA3/LA4 (launcher side) proceed alongside LA5–LA8 (client side) — different assemblies, no shared files. LA9/LA10 close the launcher side; LA11 closes the campaign. ## Linux posture (binding — user decision 2026-08-14) Everything the launcher does must WORK ON LINUX in this campaign, except GUI client launches: the Linux graphical client is Slice L, parked at L1, resuming later ("ok we will do it later"). Concretely: - **Linux-shipping in LA:** the Avalonia launcher UI, profile CRUD + 0600-permission file, installer (manual DAT picker — the auto-detect paths are Windows-only; `acdream-bake` is GL-free and runs on Linux), updater (staged swap; Linux can replace a running binary but keep the same staged-atomic flow), headless launches with plugins + login commands, and the character probe. - **Launcher UX on Linux:** the `gui` / `guiSelect` launch modes render disabled with an explicit "requires the Linux graphical client (Slice L)" note — never a silent failure. - **Per-slice enforcement:** every slice touching Launcher.Core, Headless, Runtime, Bake, or Platform runs its test projects on Linux (native Ubuntu or WSL, matching the K-slice practice) before the slice is DONE; LA4/LA9/LA10 additionally prove a real `linux-x64` self-contained publish. LA11's connected-gate script gets a Linux section: launcher on Ubuntu doing CRUD, probe, headless launch with plugin + login commands, first-run install with a manual DAT path, and an update swap. - When Slice L later ships, the launcher's Linux GUI modes light up with NO launcher changes (the session-config contract is host-agnostic) — that expectation is part of LA's design acceptance. ## LA0 — `AcDream.Platform` extraction New BCL-only project `src/AcDream.Platform/` holding `ApplicationPathSet` + `IApplicationPathEnvironment` (today `src/AcDream.Runtime/Platform/ApplicationPathSet.cs` — self-contained, no intra-Runtime dependencies; clean cut). Runtime/App/Headless reference it. Recon facts (2026-08-14): blast radius is the definition, six source files (`GraphicalHostPlatformServices.cs`, `GraphicalLegacyConfigurationMigrator.cs`, `App/Program.cs`, `GameWindow.cs:533`, `HeadlessPathSet.cs`, `HeadlessPlatformEnvironment.cs`; two more files are doc-comment-only), two test files (`ApplicationPathSetTests.cs` moves to a new `tests/AcDream.Platform.Tests/`; `GraphicalLegacyConfigurationMigratorTests.cs` fixtures), and the dependency guards — CORRECTED post-review (the original recon here asserted the wrong guard, the C4-closeout failure mode): the K0 Headless guard (`HeadlessAssemblyReferencesOnlyTheRuntimeProject`) asserts HEADLESS's own csproj reference list, which this move does not touch — it stays UNCHANGED; the guard that actually needs amending is Runtime's own `RuntimeDependencyBoundaryTests.RuntimeProjectDeclaresOnlyApprovedProjectDependencies` (Runtime gains the `AcDream.Platform` reference), amended with a cited comment in the same commit. Namespace stays `AcDream.Runtime.Platform`? NO — rename to `AcDream.Platform` and fix the eight usings (clean naming beats avoiding a mechanical edit). Register new projects in `AcDream.slnx`. **Acceptance:** build + full test suite green; guard test asserts the new exact reference set; launcher-side consumability proven by the LA3 project referencing only `AcDream.Platform`. ## LA1 — launch contract (client side) ### Pinned launch-contract schema (v1, BINDING — committed per LA3 review) This text is the single source of truth for the launcher↔host file contract. Both host readers (LA1), the composer (LA3), and the probe loader (LA2) implement EXACTLY this; any change is an amendment to THIS section first, implementations second. The LA1+LA3 merge adds a cross-assembly test feeding a composer-produced document to both host loaders — that test is the seam's permanent enforcement. Session-config document (System.Text.Json, camelCase, `UnmappedMemberHandling.Disallow`, camelCase string enums): ```json { "version": 1, "process": { "content": { "datDirectory": "...", "preparedAssetPath": "..." } }, "sessions": [{ "id": "sess-1", "endpoint": { "host": "127.0.0.1", "port": 9000 }, "account": "testaccount", "mode": "probe", "character": { "id": 1342177290 }, "policy": { "id": "idle" }, "credential": { "provider": "standardInput", "reference": "session" }, "plugins": ["ExamplePlugin"], "loginCommands": ["/vt start"], "loginCommandDelayMs": 500, "statusFile": ".../launcher/sessions/sess-1/status.jsonl" }] } ``` Field rules: - `process.paths` is OMITTED unless a caller genuinely supplies overrides (never an empty object — the App reader has no `paths` member and strict parsing rejects unknown keys; LA3 review finding 1). - `mode`: ABSENT for normal play sessions; `"probe"` for the LA2 probe (connect → characterList → graceful disconnect, no EnterWorld). The headless loader accepts the field starting at LA2. - `character`: exactly ONE of index|id|name; OMITTED entirely (not null) for guiSelect and for probe sessions. - `policy`: `{ "id": "idle" }` for headless play sessions ONLY; omitted for gui/guiSelect/probe. - `credential`: always `{ "provider": "standardInput", "reference": "session" }` for launcher-composed configs. - `plugins`: absent/null means load all discovered plugins (preserving the developer flow); explicit `[]` means load none. Launcher-composed normal-empty and probe sessions emit `[]` so they cannot load arbitrary machine-local plugins. - `loginCommands`/`loginCommandDelayMs`/`statusFile`: optional, omitted-when-unset (never null, never `[]` for empty). Absent `loginCommandDelayMs` means 500. Status stream (`statusFile`, one JSON object per line, writer flushes per line, writer opens `FileShare.Read`, tailer opens `Read/FileShare.ReadWrite|Delete`): events `started`, `connected`, `characterList{accountName,slotCount,characters[{id,name,secondsGreyedOut}]}`, `enteredWorld{characterId,characterName}`, `pluginLoaded{plugin}`, `pluginFailed{plugin,error}`, `loginCommandFailed{commandIndex,command,error}`, `disconnected{reason}`, `exited{code,reason}` — every line carries `"v":1`, `"e"`, `"t"` (ISO-8601 UTC), `"sessionId"`. `secondsGreyedOut` is a uint on BOTH sides. Unknown `e` values must parse to a typed Unknown event, never throw; a known `e` with a wrong payload shape should be distinguishable from an unknown `e` (LA3 review finding 12). `loginCommandFailed.commandIndex` is the zero-based index in the configured `loginCommands` array. `command` is the exact configured line and `error` is the isolated parser/router/handler failure. The event is observational: the host continues with the next configured line and never converts the command failure into a login, plugin, session, or process failure. **Known LA1 status limitation:** the stream has no independent mid-play wire-drop detector. If a transport becomes silent without raising through the host's tick/teardown path, no immediate `disconnected` line can be promised; the launcher must not treat the absence of that line as proof that the socket is healthy. Explicit reconnect is ordered and observable — it emits `disconnected{reason:"reconnect"}` before the replacement connection's second `connected` — and normal stop/process teardown closes any still-open connection before `exited`. A future transport-health signal may improve the timing without changing this pinned event vocabulary. Three pieces, one slice, because they share the session-config/status seam: 1. **App `--session-config `:** parsed once in `Program.cs` into `RuntimeOptions` (code-structure rule 4); carries endpoint, account, optional character selector, `Plugins`, `LoginCommands`, `Content` (DatDirectory/PreparedAssetPath), status-file path, credential reference. Recon: `Program.cs` has NO subcommand dispatch today — args handling is one positional DAT-dir (`Program.cs:35`), so the flag is purely additive (preserve the positional arg). The live-credential seam is a single call site (`SessionPlayerComposition.cs:1128-1135` → `LiveSessionConnectOptions`); the config path populates the same `RuntimeOptions` fields from a different source. Env-var dev flow untouched. App gains the `StandardInput` credential read (mirroring `HeadlessCredentialResolver.ResolveStandardInput` — one line, immediately wrapped in an erasable secret, redacted `ToString`; today `RuntimeOptions.LivePass` is a bare string — the config path must not widen that exposure). 2. **Status stream both hosts:** per-session `status.jsonl` (path given in config; absent → permanent no-op sink). Versioned event vocabulary (`"v":1`): `started`, `connected`, `characterList`, `enteredWorld`, `pluginLoaded`/`pluginFailed`, `loginCommandFailed`, `disconnected`, `exited`. Recon: today's `HeadlessDiagnosticWriter` is a single shared-stdout JSONL sink with four kinds (lifecycle/failure/event/resources) and NO per-session file — the status writer is a second, separate sink, not a rework of the diagnostics writer. App has no structured writer today; it gets the same shared implementation (lands in Runtime so both hosts borrow it). 3. **Roster plumbing:** `CharacterList.Parsed` is consumed inside `LiveSessionController.StartCore` (`LiveSessionController.cs:612`) and never escapes — add a typed roster report on the lifecycle-host seam (`ILiveSessionLifecycleHost`) so hosts can emit the `characterList` status event and (later) the char-select screen can populate. No behavior change to selection itself in this slice. **Acceptance:** round-trip tests (config → `RuntimeOptions`; stdin credential; status events in order with exact shapes; roster surfaced); App/Headless/ Runtime suites green; redaction test proves the password never appears in status/diagnostics output. ## LA2 — headless probe mode + `idle` policy Recon facts: the probe's shape already exists as the `NoCharacters` early-exit (`LiveSessionController.cs:613-622` → `StopCore()` → 4-stage `SessionScope.DrainTeardown`, graceful, `_inWorld == false` so no pre-logoff flush) — but it fires only on selection FAILURE and maps to exit code 5 (`HeadlessProcessHost.RunOnUpdateThread:203-212` treats any non-`Connected` start as `ConnectionError`). 1. **Probe:** a `Probe` flag on the connect options short-circuits `StartCore` right after `GetCharacters` (before `TrySelectCharacter`): report roster, `StopCore()`, return a NEW `LiveSessionStartStatus.ProbeComplete`. `HeadlessProcessHost` maps it to exit code 0 with a final `characterList` + `exited(reason: "probe")` status pair. Config: `mode: "probe"` on the session descriptor relaxes the `JsonRequired` character selector + policy for probe sessions ONLY (loader keeps strict validation otherwise — recon: violations currently surface as raw `JsonException` → exit 3; probe relaxation must be shape-level in the loader, not attribute removal). 2. **`idle` policy:** new consumer `HeadlessBotPolicy` id — enter world, run plugins/login-commands (arrive in LA5/LA6), stay until stopped, clean SIGINT teardown (K4's graceful-logout path already proves the mechanism). **Acceptance:** probe test (fixture session → roster event → graceful teardown receipt → exit 0, no `EnterWorld` on the wire); loader tests for probe-shape relaxation + strict normal validation; idle-policy lifecycle test; suites green. Connected verification (user gate, LA11 batch): live probe against ACE twice in a row with no lingering session (spec §11.9). ## LA3 — `AcDream.Launcher.Core` New BCL-only project + `tests/AcDream.Launcher.Core.Tests/`. References `AcDream.Platform` ONLY. - Profile store: `launcher-profiles.json` (spec §5 schema) — load/save/ validate, full CRUD operations, roster merge (fold `characterList` events in, preserving per-character user settings), 0600 on Linux. - Session-config composition: profile + install records → the LA1 config shape (typed writer; probe shape included). Passwords excluded — stdin only. - Process orchestration: spawn App/Headless per launch mode, feed password to child stdin then close, supervise lifetime, tail `status.jsonl` (share-tolerant reads), surface typed session state. - SHA-256 utility (pak record + download verify — consumed by LA9/LA10). **Acceptance:** CRUD/round-trip/merge tests; composition tests (all three modes + probe); supervision tests against a fake child process (echo script); status-tail tests including partial-line handling; suites green. ## LA4 — `AcDream.Launcher` (Avalonia) New Avalonia project (Windows + Linux). MVVM over Launcher.Core; no game solution references beyond `AcDream.Platform` transitively. - Views: server list → accounts → characters tree; add/edit/remove dialogs for servers (name/host/port) and accounts (account + password entry); per-character settings editor (launch mode, plugin set, login commands); per-account "refresh characters" (probe); running-sessions status column. - Launch actions per mode (`gui` / `guiSelect` / `headless`); probe disabled while the launcher runs a session for that account. - First-run wizard shell + update prompt shell (bodies land in LA9/LA10). **Acceptance:** ViewModel tests in Launcher.Core.Tests patterns (VMs live in the Avalonia project but stay logic-thin; anything testable pushes down); build green on Windows; `linux-x64` publish compiles. Visual polish is gated at LA11 (user). ## LA5 — plugin hosting on both hosts Recon facts (2026-08-14): `PluginLoader`/`PluginDiscovery`/`PluginManifest` already live in `AcDream.Core` (Headless-reachable). App's single load loop (`App/Program.cs:110-121`) loads ALL discovered plugins from two roots (`AppContext.BaseDirectory/plugins` + `ApplicationPathSet.PluginsDirectory`, dup-id skip) — no allow-list exists on either host. `AppPluginHost` is a 26-line pass-through; three of four `IPluginHost` surfaces (`State` → `WorldGameState`, `Events` → `WorldEvents`, `Selection` → `SelectionState`) are backed by Core-owned types already; only `Ui` (`BufferedUiRegistry`) is genuinely App-only. Headless has zero plugin hosting today (confirmed). 1. Session-config `Plugins` allow-list filters the discovery result on BOTH hosts (absent/null list = load all, preserving today's dev behavior; explicit `[]` = load none). Launcher-composed normal-empty and probe sessions emit `[]`. 2. `HeadlessPluginHost : IPluginHost` in Headless over the same Core-owned `State`/`Events`/`Selection`; `Ui` is an explicit no-op behind a new capability flag on `IPluginHost` (e.g. `HasUi`) so plugins can detect headless. Contract documented in `Plugin.Abstractions`. 3. `pluginLoaded`/`pluginFailed` status events from both hosts' load loops. **Acceptance:** fixture plugin in Headless suite (load, capability flag, markup no-op, teardown via collectible ALC); allow-list filter tests both hosts; status events asserted; suites green. ## LA6 — login commands on both hosts Recon facts (2026-08-14): the command core is dependency-CLEAN — `ChatInputParser` (zero usings), `ChatCommandRouter` (BCL + `AcDream.Core.Chat`), `RetailClientCommandCatalog` (FrozenDictionary), `ChatVM` (Core.Chat/Combat + `System.Numerics` only), `ICommandBus` + the four command records (BCL-only). The block is assembly identity, not coupling. `ChatCommandRouter.Submit`'s two entanglements: a hard `ChatVM` parameter (uses only `ShowInterfaceText`/`ShowSystemMessage`/ `LastIncomingTellSender`/`LastOutgoingTellTarget`) and the `ICommandBus`, whose production implementation (`LiveSessionCommandRouter`, `App/Net/LiveSessionCommandRouter.cs`) is App-only and wraps wire-send delegates from the live session. GUI already has a login-command analog: `RetailUiAutomationScriptRunner` feeds `ChatCommandRouter.Submit` at `RetailUiRuntime.cs:523-527`. 1. **Extraction:** move parser/router/catalog + `ICommandBus` + the four command records (+ sibling tables they require) into Runtime (`AcDream.Runtime/Chat/...`); the router's `ChatVM` parameter becomes a narrow feedback interface defined beside it (exactly the four members used); `ChatVM` (stays in UI.Abstractions) implements it. GUI path stays bit-identical — same call sites (`ChatWindowController.cs:326`, `FloatingChatWindowController.cs:157`), same routing, CH-accepted behavior regression-checked by the existing chat suites. 2. **Headless dispatch:** a Runtime/Headless `ICommandBus` binding the same session send delegates (`SendTalk`/`SendTell`/`SendChannel`/ `SendTurbineChat`) + Runtime state that App's router binds — paralleling `LiveSessionCommandRouter`'s registrations, feedback lands in `RuntimeCommunicationState.AddText`. 3. **Execution:** both hosts run `LoginCommands` sequentially as-if-typed (default 500 ms inter-command delay, config-overridable) once entered-world; per-command failures → status stream, never abort. 4. K0 guard: if the code folds into Runtime, the single-reference assertion stands untouched; the forbidden-prefix closure tests keep passing. Any guard text change is deliberate and documented. **Acceptance:** extraction lands with zero GUI chat test regressions (UI.Abstractions + App chat suites bit-green); headless executes a login-command script against a fixture session with ordered wire sends; delay + failure-tolerance tests; suites green. ## LA7 — character-select: state + wire Recon facts (2026-08-14): retail's screen is `gmCharacterManagementUI` (`acclient.h:56545`) — flat listbox + Create/Enter/Delete/Restore buttons + dialog contexts. **No 3D preview exists on retail's select screen** (the `gmCG3DView`/`CreatureMode` viewport is chargen-only; the old "rotating pedestal" line in `retail-ui/05-panels.md` §13 is uncited and wrong). Our `CharacterList` parse already matches ACE's serializer exactly (two-array shape, status/deleted always zero from ACE) and the two-phase enter-world (0xF7C8 → 0xF7DF → 0xF657) is implemented. Missing wire: delete/restore/error. 1. **Wire messages** (`AcDream.Core.Net/Messages/`, retail citations in file docs per house style): `CharacterDelete` 0xF655 — outbound account String16L + **slot index** (`Proto_UI::SendDeleteCharacter @0x00546b30`; NOT guid), inbound opcode-only ack followed by a fresh CharacterList; `CharacterRestore` 0xF7D9 guid-only (ACE + holtburger consensus; the decomp's apparent extra strings are a decompiler artifact — spec §11.4), response 0xF643 (flag + guid + name + secondsDisabled); `CharacterError` 0xF659 parser (new — today NO character-stage server error can be surfaced). 2. **Runtime selection state** (J-owner pattern): roster with per-entry greyed/pending-delete state (`SecondsGreyedOut != 0` ⇒ pending; ACE sends a constant 1 during the grace window — treat as boolean, never a countdown), highlight, pending-delete dialog state, typed commands (highlight / enter / delete-request / delete-confirm / restore). Retail behavior oracles: `RebuildCharacterList@0x004ec3a0`, `SelectCharacter@0x004ec160`, `UpdateButtons@0x004ec240` (Delete↔Restore swap on greyed state), `EnterGame@0x004ed440`. 3. **No-selector flow:** a graphical session config without a character selector stops at selection state instead of auto-enter; the first-available fallback (`CharacterList.TrySelectFirstAvailable`, used at `LiveSessionController.cs:848-851`) remains ONLY for selector-carrying/headless sessions. Selection feeds the existing `EnterWorld` path unchanged. LA7b hazards carried from the LA7a review (2026-08-14): ACE's restore handler has a SILENT no-reply path (unknown guid → `return;`, no 0xF643, no 0xF659) — selection state must never block awaiting a restore reply; outbound routing is delete via retail's SendToLogon, restore via SendToControl, ACE replies on UIQueue; `charError.NumErrors` (0x19) is an enum-range sentinel and must never render as a user-facing message. Register row AD-97 (guid-only restore request, an adaptation) rides the LA7a branch. **Acceptance:** message round-trip tests against ACE's serializer shapes; selection-state tests (greyed transitions, delete→list-refresh, restore, error surfacing); no-selector stop + enter flow tests; suites green. ## LA8 — character-select: authored retail screen Scope: project LA7's state through the REAL retail screen. No 3D preview (recon-corrected; a preview would be an unapproved divergence). 1. **Layout resolution:** retail resolves the root via `UIMainFramework::CreateAndAddRootElement(0x10000005, 0x1000039a)` + `DBObj::GetDIDByEnum(..., 5)` — reuse OP8's ported GetDIDByEnum machinery (category 4 precedent) for enum-table 5; slice starts by dumping that table from installed DATs to pin the concrete DataID. Child ids: listbox `0x1000039d`, create `0x100003a0` (present, disabled — Create is a future campaign), enter `0x100003a2`, delete `0x1000039f`, restore `0x1000039e`. 2. **Dialogs:** delete-confirm, please-wait, entering-world, error — the retail dialog machinery from the OP8 WaitDialog work (`2a81e813` mapped WaitDialog class type 0x19) is the base. 3. **Open item resolved here:** whether retail draws a render-loop background scene behind the UI (pseudo-C proves only that the UI class owns no viewport) — settle via user recollection + the visual gate before polishing. **Acceptance:** authored screen builds from DAT assets; button-state matrix matches `UpdateButtons` oracle (incl. Delete↔Restore swap); enter/delete/restore/error flows drive LA7 state end-to-end; suites green. User visual gate at LA11 (screen look, dialog flows, delete + restore against local ACE). ## LA9 — installer (first-run) - DAT locate: auto-detect `%USERPROFILE%\Documents\Asheron's Call` and `C:\Turbine\Asheron's Call` + manual picker; validate the four DATs. - Bake: spawn `acdream-bake --dat-dir --out /pak/acdream.pak --threads N`. Recon: default `--out` is INSIDE the DAT dir — the launcher always passes `--out` explicitly. Progress: add `--progress-json` to `AcDream.Bake` (JSONL progress lines alongside the existing 5-second human text, which stays default) — scraping human text is fragile and we own the tool. Recon: the bake has NO whole-file SHA — after a successful bake the LAUNCHER computes and records SHA-256 + size + `BakeToolVersion` in its install record, and re-verifies on subsequent startups (fast corruption check trades a few seconds of hashing for never launching against a half-written pak). - Install record feeds LA3's session-config composition (DatDirectory/PreparedAssetPath). **Acceptance:** wizard flow tests over Launcher.Core (fake bake child emitting `--progress-json` lines); bake-tool progress flag tests in `tests/AcDream.Bake.Tests`; SHA record/verify tests; suites green. Connected gate (user): clean-profile first-run against real DATs. ## LA10 — updater - Manifest: GitHub Releases; `manifest.json` release asset — version, per-RID client zip URL + SHA-256 + size, minimum-launcher version. Launcher pins owner/repo in its config. - Client update: poll on launch (+ manual check), download to staging, SHA verify, unpack to `DataDirectory/app//`, atomic `current.json` pointer swap, refuse while any session runs, keep previous version for one-step rollback. - Launcher self-update: staged download + rename-dance on next start. - Session-config composition targets `app/current`'s binaries. **Acceptance:** manifest/download/verify/swap tests against a local HTTP fixture; rollback test; refusal-while-running test; self-update staging test; suites green. Connected gate (user): staged-manifest update swap end-to-end. ## LA11 — closeout - One connected-gate script `docs/research/2026-XX-XX-campaign-la-test-script.md` covering: all three launch modes vs local ACE, probe round-trip ×2 (no lingering session), char-select visual matrix + delete flow, login-commands + plugin behavior on both hosts, add-server/add-account purely in UI, clean-profile first-run wizard, staged update swap. - Roadmap shipped-table entry, CLAUDE.md Current-state flip, memory distill, ledger below completed, program closeout section. ## Review protocol Per slice: implementer commit(s) → Opus dual-lens review (architectural + retail-where-applicable) → fix round → narrow re-review of fixes → slice DONE in ledger. Reviews name blast radius explicitly (`claude-memory/feedback_blast_radius_single_lens.md`). Slices LA7/LA8 add the retail-fidelity lens against named-retail symbols cited in the slice body; LA6 adds CH-regression scrutiny; LA0 adds guard-integrity scrutiny. ## Ledger | Slice | Status | Commits | Review | Notes | |---|---|---|---|---| | LA0 | **DONE 2026-08-14** | `cb6502c8`, `a49e92df` | Opus dual-lens PASS; all 6 findings CLOSED in narrow re-review | Byte-identity proven; Linux CI lanes restored; Platform BCL-only self-guard added; K0 guard untouched | | LA1 | **DONE 2026-08-14** | `db9ad53c` (mixed — see `e1322a06`), `75a6724d` (recovery WIP), `d511e4c3`, ledger `890cf267` | Initial review FIX FIRST; F1–F8 CLOSED; narrow dual-lens re-review PASS | Release build green (0 errors / 18 warnings). Windows: Runtime 1634 / Headless 127 / App 5038+3skip. WSL: Runtime 1634 / Headless 127. Known mid-play silent-wire-drop limitation recorded above. The LA1+LA3 composer-to-both-hosts contract gate and portable CI lane landed at `8a03a25f`. | | LA2 | **DONE + MERGED 2026-08-14** | `c6019424` (recovery WIP), `000ea979`, `1c5e66c0`, merge `e01b2cd1` | Dual-lens review FIX FIRST; all 3 findings CLOSED; final narrow re-review PASS | Probe success requires a reported roster and remains before selection/EnterWorld; terminal status derives from the actual start outcome; conditional fields distinguish omission from explicit null without weakening strict JSON. Branch gates: Runtime 1,632/1,632 and Headless 149/149 on both Windows and Ubuntu/WSL. Integrated gates: Release solution build green; Windows Runtime 1,636/1,636, Headless 151/151, App 5,039+3 skip, Launcher.Core 114/114; WSL Runtime 1,636/1,636, Headless 151/151, Launcher.Core 114/114. Repeated live ACE probe remains the LA11 user gate. | | LA3 | **DONE + MERGED 2026-08-14** | `37d74e44`, `26feba81`, `347a1a5d`, merge `7749545d`, seam `8a03a25f` | Initial 12 findings CLOSED; four-gap narrow review FIX FIRST; final narrow re-review PASS | `AcDream.Launcher.Core` remains BCL + Platform only. Windows/WSL Core 114/114; full Release build green. Composer output is parsed by BOTH real host loaders from one linked fixture; Launcher.Core build/tests run in the portable Windows+Ubuntu lane. Windows graceful-stop gap remains tracked as #397. | | LA4 | **DONE + MERGED 2026-08-14** | `d0a9c65d`, `10a712d6`, `ae2cbbee`, merge `60f62799` | Initial dual-lens review found 10 issues; fix re-review left one Linux execute-bit gap; final narrow re-review PASS | Avalonia 12.1.1 launcher remains thin over one BCL-only Core orchestrator. Windows/WSL Launcher.Core 162/162 and Launcher 17/17. Native `linux-x64` publish evaluates self-contained + single-file, runs without a discoverable runtime, and CI verifies executable launcher/App/Headless artifacts. LA9/LA10 bodies and LA11 visual/accessibility confirmation remain intentionally later. | | LA5 | **DONE + MERGED 2026-08-14** | `95f4be94`, `fbe9c8a2`, `f820eb25`, merge `5535d0ad` | Initial review found 5 issues; first narrow re-review found 4 ownership/race gaps; final narrow re-review PASS | Both hosts share exact absent/null=`all`, `[]`=`none` allow-listing; transactional scoped UI/entity/selection rollback precedes unload; graphical/headless status and teardown ordering match; headless replay is exact-once under Runtime's borrowed membership lease. Branch complete suite 13,679+4 skip; portable WSL closure green. | | LA6 | — | | | | | LA7 | **DONE + MERGED 2026-08-14** | LA7a `6a32f375`, `4338b1c1`, `0c8643a7`, merge `fa2de1c4`; LA7b `0e82cbf7`, `1b9e7e41`, `ff406562`, merge `7691cf75` | LA7a retail-lens PASS; LA7b review found 4 issues, first narrow pass left one restore/delete interleave, final narrow re-review PASS; AD-97 filed | Runtime owns the sole generation-scoped pre-world selection graph. Exact retail roster/grey/button/delete/restore behavior and queue routing are preserved; `NumErrors` is a sentinel, paused selection retains reliable transport sweeping, silent restore cannot block, and App has no mirror. Windows Runtime 1,653, Core.Net 958, App 5,042+3 skip; WSL Runtime/Core.Net green. | | LA8 | — | | | | | LA9 | — | | | | | LA10 | — | | | | | LA11 | — | | | |