diff --git a/docs/plans/2026-09-07-headless-console.md b/docs/plans/2026-09-07-headless-console.md index 04aece66..8382783f 100644 --- a/docs/plans/2026-09-07-headless-console.md +++ b/docs/plans/2026-09-07-headless-console.md @@ -66,3 +66,106 @@ it or the lead may, it is not a visual gate. ## Ledger - 2026-09-07 planned; implementer dispatched. +- 2026-09-07 IMPLEMENTED. The dispatch seam already existed: + `AcDream.Runtime.Chat.ChatCommandRouter.Submit` is the SAME presentation- + free pipeline `LoginCommandSequence` (headless) and every graphical chat + window (`ChatWindowController`, `FloatingChatWindowController`, + `RetailUiRuntime`) already call — no lift was needed. Added + `HeadlessSessionHost.SubmitConsoleLine` (`Hosting/HeadlessSessionHost.cs`) + as the one new call site, reusing the host's own retained + `LiveChatCommandSurface`/plugin registry (now promoted from ctor locals to + fields) instead of a second parser. + New files: `Configuration/HeadlessConsoleOptions.cs` (typed `--console` / + `ACDREAM_HEADLESS_CONSOLE=1` / terminal-default resolution), + `Hosting/HeadlessConsoleInputReader.cs` (background stdin thread → FIFO + queue, never executes handler code), `Hosting/HeadlessConsoleController.cs` + (drains the queue on the session tick via a new `HeadlessSessionHost. + ConsolePump` hook; owns `/quit`/`/status`), `Hosting/ + HeadlessConsoleChatFormatter.cs` + `Hosting/HeadlessConsoleRenderer.cs` + (renders the K2 bot event stream — `IRuntimeEventObserver`, the same + interface a bot policy subscribes — as bracket-labelled lines: + `[Tell] Bob: hi`, `[Fellowship] …`, `[Local] …`), `Hosting/ + HeadlessConsoleChatFeedback.cs` (decorates `RuntimeChatCommandFeedback` so + retail's transient SpewBox/`ClientLocal` interface text — which never + touches `ChatLog`, so it never reaches the K2 event stream — also reaches + the console). `/quit` cancels a `CancellationTokenSource` linked into the + scheduler's run token in `HeadlessProcessHost` (the SAME graceful-exit + path an external Ctrl+C/SIGTERM already takes); `/status` reports + generation, position (or "unknown" without a live movement controller), + and loaded-plugin count (no plugin today reports a richer macro-state + string). Console only attaches for a single-session `run` (per the plan's + "out of scope for the first cut" multi-session note); constructed AFTER + every session's own credential resolution so the reader thread never + races a `StandardInput`-provider password prompt on the same stream. + Chosen console default: on when `!Console.IsInputRedirected` (a real + operator at a terminal), off when redirected (scripts/CI/piped fixtures, + where a blocked `ReadLine` on a background thread would just sit idle) — + resolved once in `Program.cs`, the only place that can see the real + `Console`. + Deviation from the plan's illustrative example: retail's own transcript + never prefixes Tell/Local lines with a bracket (`ChatVM.FormatEntry` + renders "Bob tells you, ..."/"Bob says, ..." with no label) — Headless + cannot reference `AcDream.UI.Abstractions` (the dependency-boundary + test), so `HeadlessConsoleChatFormatter` is a deliberately DIFFERENT, + terminal-shaped "[Label] Sender: text" rendering using the SAME channel- + name strings (matching the plan's literal `[Tell] Bob: hi` example), not + a byte-for-byte port of the graphical prose. + Tests: `tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs` (20 new + tests — options resolution, command-line flag parsing, reader-thread + ordering/never-on-reader-thread, controller drain/quit/status, chat + formatting, and full `/say`/plain-text/plugin-verb/unknown-verb dispatch + against a real `HeadlessSessionHost` + `FixtureSessionOperations`, no live + server) plus the existing `LaunchOptionsDocumentationTests` (4/4 green) + and `HeadlessDependencyBoundaryTests` (3/3 green, unchanged — Headless + still references only `AcDream.Runtime`). Every test in this batch was + mutation-checked to fail before the corresponding production line existed + (see the implementer's final report for the specific mutations run: + skipping the interface-text callback, skipping `_quitRequested.Cancel()`, + forcing `TryHandlePluginCommand` to always return false, dropping the + reader thread's `Enqueue`, and swapping `ChatChannelKind.Say` for `.Tell` + in `SubmitConsoleLine`). + Suites: `dotnet test tests/AcDream.Headless.Tests -c Release` → 193 + passed / 1 pre-existing failure (`LinuxRejectsGroupOrOtherCredentialPermissions`, + a Linux-only lane test that cannot run on this Windows host — unrelated + to this change) / 194 total. `dotnet test tests/AcDream.Runtime.Tests -c + Release` → 1891/1891 passed. `dotnet test tests/AcDream.App.Tests -c + Release --filter "FullyQualifiedName~Chat|FullyQualifiedName~Command| + FullyQualifiedName~LaunchOptions"` → 414 passed / 2 pre-existing failures + (`ChatIndicatorButtonLiveMountProbeTests`/`OptionsPanelLiveMountProbeTests` + — both gated on `ACDREAM_PROBE_LIVE_MOUNT=1`, a manual live-DAT probe lane, + unrelated to this change) / 3 skipped / 419 total. `dotnet build + AcDream.slnx -c Release` green throughout. + +### Connected proof recipe (owner runs; NOT run by the implementer) + +Against a running local ACE at `127.0.0.1:9000` with MossTank loaded for +the second half: + +```powershell +$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" +dotnet run --project src\AcDream.Headless\AcDream.Headless.csproj --no-build -c Release -- ` + run --config ` + -user testaccount -password testpassword --console +``` + +The referenced config's one session should target character `+Acdream` +(server guid `0x5000000A`) against `127.0.0.1:9000`, an `idle` bot policy, +and (for the second half) the MossTank plugin id under `plugins`. Once the +console prints `entered world`: + +1. Type `/say hello` and press Enter — expect the SAME line ACE echoes back + to any other observer (a retail client or a second acdream session + watching `+Acdream`) to also print `[Local] You: hello` in this console + (the server's own HearSpeech echo, rendered through the normal chat + event stream). +2. Type `/status` — expect a line with `generation=`, `position=` (a real + cell/local-frame triple once in world), and `plugins=N loaded`. +3. With MossTank loaded, type `/vt start` (or whatever verb MossTank + registers) — expect MossTank's own handler to run (check its own + status/log output) and confirm NOTHING was sent to the wire for that + line (no `@vt` server command). +4. Type `/quit` — expect a graceful ACE logout (same as the existing + Ctrl+C behavior) and the process to exit 0. + +This is not a visual gate; the owner (or the lead) runs it opportunistically +before considering the plan CLOSED.