# 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}`, `characterCreated{guid,name}`, `creationFailed{code,reason,name}`, `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). **Campaign CC CC2 amendment (this section is the contract; the writer and tailer below implement it, in that order):** `characterCreated{guid,name}` fires on the Ok reply to a `CharacterCreate` (opcode `0xF656`) request — `guid`/`name` come straight off the shared `0xF643` `CharGenVerificationResponse` Ok identity payload (`AcDream.Core.Net.Messages.CharGenVerificationResponse`), deliberately named `guid`/`name` rather than `characterId`/`characterName` to mirror that payload's own field names and to read distinctly from `enteredWorld` — a freshly created character is logged straight in by retail without a fresh `characterList` (see that type's doc comment), so `characterCreated` can precede an `enteredWorld` for the same character rather than replacing it. `creationFailed{code,reason,name}` fires on any non-Ok reply: `code` is the raw wire `CharGenVerificationResponse.Code` value, `reason` is that code's enum member name (e.g. `"NameInUse"`) so a reader gets a stable readable reason without hard-coding the numeric mapping itself, and `name` is the ATTEMPTED character name so a launcher can render "the name Bob is taken". (CC2 review F4: the enum member originally rode the `name` key, colliding in meaning with `characterCreated.name`; renamed before any consumer shipped.) `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`, `characterCreated`/`creationFailed` (Campaign CC CC2), `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 + target-local atomic replacement 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. ### Pinned updater contracts (v1, BINDING) This section is the single source of truth for every LA10 feed and on-disk shape. Readers use strict, case-sensitive `System.Text.Json` parsing, reject unknown or duplicate properties, and reject unsupported schema versions before doing network, extraction, or activation work. The production feed is pinned to GitHub owner/repository `eriknihlen/acdream`; the launcher reads `https://github.com/eriknihlen/acdream/releases/latest/download/manifest.json`. Tests use a separate internal fixture constructor that may admit loopback HTTP; that allowance never propagates to the production feed. Production manifest and artifact URIs use HTTPS. Automatic redirects are disabled and every redirect hop is validated before it is requested; redirect loops, a chain over five hops, and any HTTPS-to-HTTP downgrade are rejected. `manifest.json` is: ```json { "schemaVersion": 1, "version": "1.2.3", "minimumLauncherVersion": "1.1.0", "clients": { "win-x64": { "url": "https://github.com/eriknihlen/acdream/releases/download/v1.2.3/acdream-client-win-x64.zip", "sha256": "<64 hex characters>", "size": 123 } }, "launchers": { "win-x64": { "url": "https://github.com/eriknihlen/acdream/releases/download/v1.2.3/acdream-launcher-win-x64.zip", "sha256": "<64 hex characters>", "size": 123 } } } ``` `version` and `minimumLauncherVersion` are strict SemVer 2.0 strings. Build metadata is ignored for precedence; numeric identifiers are compared without fixed-width integer overflow. RID keys are exact lowercase portable RIDs. Both dictionaries are required and the running RID must have a client and a launcher row. Artifact sizes are positive and capped by the launcher's download limit; SHA-256 is exactly 64 hex characters. ZIP URLs are absolute. Client ZIPs have the two host executables at their root (`AcDream.App[.exe]`, `acdream-headless[.exe]`); launcher ZIPs have `acdream-launcher[.exe]` at their root. No implicit wrapper directory exists. Every extracted client version has `DataDirectory/app//install.json`: ```json { "schemaVersion": 1, "version": "1.2.3", "rid": "win-x64", "archiveSha256": "<64 hex characters>", "archiveSize": 123, "files": [ { "path": "AcDream.App.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 } ] } ``` Paths use `/`, are relative, normalized, unique under ordinal-ignore-case, and sorted ordinally. `unixMode` contains only the portable permission bits captured from the ZIP entry. Startup verifies every recorded regular file by size/SHA, rejects unrecorded files/reparse points, and requires the two host executables before admitting a version. Extraction uses a random sibling directory under `DataDirectory/app/`; promotion to `/` is one same-volume directory rename. `DataDirectory/app/current.json` is the only activation authority: ```json { "schemaVersion": 1, "currentVersion": "1.2.3", "previousVersion": "1.1.0" } ``` `previousVersion` is omitted for the first activation. Pointer writes are write-through temporary-file + same-directory atomic rename. The last valid pointer is also atomically preserved as `current.previous.json`; startup may restore that exact backup only when `current.json` is missing/malformed and the referenced version verifies. Orphan LA10 staging directories, download archives, corrupt-version quarantine directories, and pointer temporaries are transaction-owned by exact lowercase GUID names and are removed only under the exclusive update lease; near-matching user names are preserved. A corrupt installed version is never silently selected; the explicit one-step rollback swaps the two verified pointer versions. `DataDirectory/app/.update-session.lock` is the cross-process barrier. Each supervised launcher activity holds a shared OS handle from before executable resolution until terminal process observation; launcher disposal requests child termination and does not release that handle until the child is actually observed terminal. An update/rollback holds the exclusive handle for its entire recovery/download/extract/promote/pointer transaction. Failure to acquire the exclusive handle is an immediate refusal, not a wait behind a running session. The open handle, not lock-file contents, owns the lease and therefore releases after process death. Launcher self-update staging lives at `DataDirectory/launcher-update/transactions//` and the sole durable authority is `DataDirectory/launcher-update/pending.json` (schema 3): ```json { "schemaVersion": 3, "transactionId": "0123456789abcdef0123456789abcdef", "state": "staged", "version": "1.2.3", "rid": "win-x64", "targetDirectory": "", "archiveSha256": "<64 hex characters>", "archiveSize": 123, "files": [ { "path": "acdream-launcher.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 } ], "apply": null } ``` Before mutation the verified staged launcher becomes the next-start helper and waits for the initiating launcher PID without invoking a shell. It first copies the complete verified payload into the target-local `.acdream-self-update-/incoming/` tree. The plan then advances to `applying`; `apply` is an ordinally sorted union of new payload paths, the owned metadata path, and obsolete paths from the previous ownership record: ```json [ { "path": "acdream-launcher.exe", "operation": "install", "hadOriginal": true, "priorSha256": "<64 hex characters>", "priorSize": 123, "priorUnixMode": 0, "replacementSha256": "<64 hex characters>", "replacementSize": 456, "replacementUnixMode": 0 }, { "path": "new-support.dat", "operation": "install", "hadOriginal": false, "priorSha256": null, "priorSize": null, "priorUnixMode": null, "replacementSha256": "<64 hex characters>", "replacementSize": 456, "replacementUnixMode": 0 } ] ``` Every `hadOriginal` entry persists the exact pre-mutation SHA-256, length, and Linux mode bits; a no-original entry has all three prior fields null. Every install entry likewise persists the verified replacement metadata, while a remove entry has all three replacement fields null. The journal is invalid unless those fields agree with `hadOriginal` and `operation`. Existing targets are replaced with one same-filesystem atomic replace whose backup is also target-local. Previously absent noncanonical files use one same-filesystem rename; obsolete owned files use one rename into backup. The canonical launcher path therefore contains either the complete old file or the complete new file at every durable crash boundary. Rollback first performs a zero-mutation preflight of the complete target-local transaction and every journal entry. It rejects reparse points, unsafe parents, unrecorded paths, ambiguous file layouts, and any SHA-256/length/mode mismatch in a prior, incoming, or discard file. Only a fully preflighted rollback may atomically restore backups; newly created files move to target-local discard rather than being deleted. The complete prior target set is then reverified before the plan enters durable `rolledBack` state while retaining the journal. Retry is allowed only after that prior set is reverified again and the plan returns to `staged`. Thus rollback is atomic per file and idempotent after a process/power loss. Any ambiguity preserves the applying plan and transaction evidence and forbids launching the canonical path for manual recovery. Linux mode bits come from the verified incoming file. A helper that cannot immediately acquire the exclusive update lease defers the staged plan and exits without restarting the old launcher, preventing restart loops. Successful application writes strict target ownership metadata at `/launcher.install.json`: ```json { "schemaVersion": 1, "version": "1.2.3", "rid": "win-x64", "files": [ { "path": "acdream-launcher.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 } ] } ``` The archive may not supply that reserved metadata path. A prior valid record is the only authority for obsolete-file removal; the first managed update does not infer ownership of unrelated legacy files. On success the plan becomes `awaitingConfirmation`; the new launcher confirms at its first managed instruction, after which the helper releases its lease and the confirmed launcher reclaims plan, data-transaction, and target-local residue. An `applying` plan is rolled back before retry, and failure to start/confirm the new launcher restores every original (and removes every no-original target). The helper restarts the restored canonical launcher only after a fresh complete verification of the retained `rolledBack` journal; rollback corruption or an unsafe backup/discard tree exits without starting either launcher. Reading `pending.json` never performs cleanup. Ordinary startup attempts the exclusive lease without waiting and skips update cleanup entirely when another session/staging transaction owns it. All plan paths are re-derived/contained under pinned roots; the target directory must equal the actual launcher base directory. Every portable archive and persisted relative path rejects Windows device segments on every host: `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`, `CONOUT$`, `COM1`-`COM9`, `LPT1`-`LPT9`, and the Windows-equivalent superscript forms `COM¹`/`COM²`/`COM³` and `LPT¹`/`LPT²`/`LPT³`, including extensions. ## LA11 — closeout - One exact operator script `docs/research/2026-08-14-campaign-la-test-script.md`, fronted by the connection-free `tools/run-campaign-la-preflight.ps1` and followed by serial user rows, 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. ## Gate round 1 — 2026-08-15 (first live launch by the user) The user's first hands-on launch found the launcher exiting on every click. Root cause (`d54b8a78`): `MainWindow`'s constructor called `AvaloniaXamlLoader.Load(this)` instead of the generated `InitializeComponent()`, so every `x:Name` backing field was null and any modal open/close threw out of the dispatcher into `Program`'s exit-74 guard. It reached the gate because NO test constructed `MainWindow` — filed and closed as **#399** (`2b439cc1`, merged): `Avalonia.Headless.XUnit` view tests with falsification evidence (12/12 fail against the old code, 12/12 pass against the fix; launcher suite 66/66 Windows + native Ubuntu; xunit→xunit.v3 in that test project). Same round (`e1e94697`): **#398** closed — fatal exceptions now write a full-stack crash report under the data root (isolated-roots-safe; the first cut leaked to the real data root when parsing failed, caught live and fixed) — and `acdream-bake.exe` is now co-deployed on plain Build, not just Publish, so a developer-built launcher can actually run its first-run wizard (79.6 MB single file beside the launcher, incremental, `--help` verified). One transient 65/66 on the first post-merge test run did not reproduce across a clean rebuild + six repeats — consistent with stale-artifact mixing, but if it EVER recurs, capture the failing test name before anything else. Merged slice worktrees/branches (la2/la3/la7a/la-uitest) removed. Opus batch review: PASS (HIGH confidence) with 6 findings, all landed same-day: F1 the crash reporter's by-construction claim was FALSE (the launcher holds passwords in three fields; the true invariant — no throw site interpolates a credential value — is now pinned by a forced-failure test), F2 the co-deploy's Inputs covered only Bake's own sources, not its Content/Platform/Core/ Plugin.Abstractions closure (the stale-artifact class again; fixing it exposed and fixed two more incrementality traps: SkipUnchangedFiles leaving outputs older than inputs, and %(Item.Metadata) in a plain Include not batching — a literal '%(...)' input is permanently out-of-date), F3 dual bake publish on RID publishes (guarded by _IsPublishing; verified 0 build-target co-deploys during a real publish), F4 misattributed comment, F5 template-scoped x:Name false-fail (sweep now walks the XML with template-ancestor tolerance), F6 dead using, plus the optional Path.IsPathFullyQualified hardening on the crash reporter's --data-dir fallback. Launcher 67/67, Launcher.Core 317/317. The §A–I connected script remains the open user gate. ## Gate round 2 — 2026-08-15 (first live launcher→client flow) — char-select matrix USER-PASSED **USER-PASSED 2026-08-15 (end of round):** the character-select visual/ interaction matrix — stretched-canvas look with bilinear filtering, aligned widgets, left-justified roster, World box reading the live server name ("sawato"), and the centered exit confirmation — all accepted on the live launcher→client flow. Round-2 commits after the round-1 batch: `6e1c0967` (session-config launches force the retail UI), `9ce72925` (PFID_CUSTOM_RAW_JPEG decode + resolution guards), `73041d70` (whole-canvas AD-98 scale + inverse input), `308f40a3` (linear-twin bilinear stretch), `ef96c554` (exit confirmation + authored justify + world name, AD-99), `2e6d69dd` (#400), `0a7dc7d6` (durable world-name read + canvas-centered dialogs). Remaining before shipment: the formal §A–I script rows (probe ×2, headless+plugins+login commands, delete/ restore, A→B update swap, row I Linux), and the final-HEAD preflight re-run. **Round REVIEW-CLOSED 2026-08-15:** the owed Opus dual-lens batch review of the six round-2 commits returned PASS with 8 findings; the fix round (`0baebce2` — headline: `RetailWaitDialogView` was the ONE dialog view the EffectiveCanvasSize sweep missed, firing on ENTER; plus the two stale deleted-mechanism doc assertions, the Confirmation `0xAC` property, truncating input mapping, the IsCurrent world-name gate, the AD-98 evidence note) closed all seven in the narrow re-review; F2 filed as #401 (invert RetailUi to opt-out). The review also proved the `DatWidgetFactory` justify widening has ZERO regressions across all 35 layout fixtures (303 buttons swept; the 16 authored-Left all already left-aligned via their face-child branch) and is a move TOWARD retail (`CalcJustification @0x00467260` has no lifted-from-child condition). Two real defects, both root-caused and fixed: 1. **`6e1c0967` — launcher-spawned clients had NO interface at all.** `RetailUi` rode the dev env var `ACDREAM_RETAIL_UI`; `FromSessionConfig` inherited the env parse; the launcher strips `ACDREAM_*` from children (LA11 isolation). Product launches therefore got the dev default: world rendering, zero UI — character screen included. Session-config launches now force `RetailUi = true` (a session-config launch IS a product launch); the env flag remains the dev-launch opt-in. Test-pinned with a null env. 2. **`9ce72925` — character-select screen rendered magenta background/ fills.** The screen's 800×600 root background (`0x06007576`) is `PFID_CUSTOM_RAW_JPEG` — a complete JFIF stream retail hands to the Intel JPEG Library (`RenderSurface::CreateFromSourceData @0x004440a0`), with Width/Height legitimately 0 on disk. `SurfaceDecoder` had no JPEG case AND a non-positive-dimension guard, so it fell silently to the magenta placeholder; the listbox/ENTER fills are transparent, so one broken background bled through as three symptoms. Fixed via StbImageSharp (managed, Linux-safe; codec-library substitution per the BCnEncoder precedent — no register row). BOTH silent traps now log once per id (id-resolves-but-undecodable in `SurfaceDecoder`; id-missing-from-DATs in `TextureCache`) — the existing magenta guard only covered id-0. New installed-DAT sweep asserts every char-select media id decodes non-magenta. Full suite 14,034 green. Session-orchestration facts this round: the machine gained PowerShell 7 (winget, user-approved — the LA fixture tooling hard-requires it); an orphan feed server from the earlier session held port 43119 with stale fixture data (stopped); the launcher self-update bootstrap restart on a dev binary is EXPECTED (staged launcher update → exit → respawn). Observations still open for this round: the duplicated "versioned client is unavailable" status line (cosmetic), and verifying Create Character is disabled on the live screen. ## 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 | **DONE + MERGED 2026-08-14** | `41b15efd`, `259f0e5a`, merge `2bb8ccb6` | Dual-lens/CH regression review found one Headless wire-parity gap; narrow re-review PASS | Runtime owns the sole parser/router/catalog and shared four-route live binding. Both hosts run generation-scoped login commands after world entry with strict monotonic delay and nonterminal v1 failure status. Headless permit/chat/notell semantics match App. Branch complete suite 13,787+4 skip; WSL Runtime 1,662, Headless 165, Launcher.Core 167, UI/chat 922. | | 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 | **DONE + MERGED 2026-08-14** | `6cfab727`, `aeac874d`, `1dd5706e`, merge `fe63ce18` | Initial retail/architecture review found 4 issues; first narrow re-review left 2 retry-transaction/order gaps; final narrow re-review PASS | Installed DAT enum table 5 proves `0x10000005 -> 0x21000004`, root `0x1000039A`, exact flat list/buttons/templates/dialog assets, and no viewport. Runtime remains the only selection owner; row sizing, modal priority/retry, restore ordering, reset/disposal, and explicit live-DAT skip/probe are covered. Branch full suite 13,796+5 skip; LA11 owns physical visual/live-ACE acceptance. | | LA9 | **DONE + MERGED 2026-08-14** | `ff6ebb6a`, `3f688951`, `208a70ac`, merge `2198a0cc` | Initial integrity review found 5 issues; narrow re-review left one orphan-child publication race; final narrow re-review PASS | First-run installer validates four DATs, consumes strict v1 Bake JSONL, preserves/reverifies SHA+size+tool-version records, and co-publishes self-contained launcher+Bake. Cross-process install/publish locks plus durable nonce prevent post-recovery mutation across real parent-only hard kills on Windows/Linux. Branch full suite 13,799+4 skip; real retail-DAT bake remains LA11. | | LA10 | **DONE + MERGED 2026-08-14** | `2d2a5b50`, `1955ca8a`, `09d84387`, merge `da4fb3de` | Initial architecture/security review found 10 crash, trust, integrity, cleanup, and lifecycle issues; first narrow re-review left one rollback-source P1; final narrow re-review PASS | Production feeds and redirects are HTTPS-only, fixture loopback trust is explicit, downloads and archives are bounded and verified, version activation and rollback are atomic, active sessions hold the cross-process update lease, and schema-v3 self-update recovery verifies every prior/replacement file before apply, rollback, or restart. Real Windows/Linux process tests cover kill boundaries, staging races, lease deferral, corrupt backups, junctions/symlinks, and fail-closed recovery. Branch gates: Core 302/302 and Launcher 29/29 on Windows/WSL, full Release 13,945+4 skip, win/linux self-contained publishes. Integrated LA0–LA10 gate: 13,972+5 skip. | | LA11 | **AUTOMATED CLOSEOUT REVIEW-CLOSED + MERGED 2026-08-15 — USER GATE PENDING** | `f881e5b4`, `134edabe`, `accd01a0`, `9f9c1167`, merge `d39f3098` | Initial dual-lens review found 7 startup/evidence/safety issues; first narrow re-review left 2 PID-reuse/ZIP-mode gaps; final narrow re-review PASS | Strict isolated roots and process-local feed override compose one exact launcher path graph. Windows targeted CTRL_BREAK is group-isolated and preserves stdin; exact-PID/start-identity status validation, credential-value scanning, deterministic Unix-mode A/B fixtures, Windows/native-Linux helper safety, and the exact A–I operator script are implemented. Clean branch preflight passed 32/32 with 13,985 tests + 4 skips. Integrated clean-head preflight at `a22f5411` passed 32/32 with 14,012 tests + 5 skips and report SHA-256 `49f225bc6043b9256f17b7bf0f29df919c894b8355633077751fd279756470df`. No connected/UI/real-DAT row has run; campaign shipment and #397 closure remain pending the user gate. |