acdream/docs/plans/2026-08-14-launcher-campaign.md

31 KiB
Raw Blame History

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 34 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 LA5LA8 (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):

{
  "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}, 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).

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 <path>: 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-1135LiveSessionConnectOptions); 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 → no writer constructed, zero cost). Versioned event vocabulary ("v":1): started, connected, characterList, enteredWorld, pluginLoaded/pluginFailed, 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-622StopCore() → 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 (StateWorldGameState, EventsWorldEvents, SelectionSelectionState) 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 <dats> --out <DataDirectory>/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/<version>/, 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; F1F8 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