acdream/docs/plans/2026-08-14-launcher-campaign.md
Erik 55fc51ed8c Merge campaign-cc2: CC2 CharacterCreate wire, review-closed
CC2 review PASS (checksum term set confirmed against the CG_Pack
accumulator; account-outside-body and GetPackSize=172 independently
proven), fix round e77ebf10 (F1 latch scope + pin test, AD-100, ACE
double-NameInUse note, creationFailed reason/name split, pointer fix,
retail-discriminator citations), narrow re-review CLOSED, residual
anchor fix 95e95bb6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 13:39:08 +02:00

860 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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):
```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 <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-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 <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 + 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/<version>/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 `<version>/` 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/<transactionId>/` 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": "<absolute current launcher directory>",
"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-<transactionId>/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 directory>/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 §AI
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
§AI 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; 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 | **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 LA0LA10 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 AI 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. |