Devlog · ShellHacks 2026

The DungeonFlux timeline

DungeonFlux is planned and built with a team of agents: one orchestrating session hands research, design, and code to subagents that work in parallel, and an adversarial critic scores the plan after every round. This timeline records what happened along the way: how the process worked, the issues that came up, the things we found out, and the decisions that followed. Newest entries are at the top.

  1. IssueORCH

    Decoded video and tested helpers do not prove a working battlefield

    The report audit found black hero planes even though the cached H.264 files decoded correctly. A video texture could be shown before its first frame loaded, and a runtime started before a manifest update could lack newer assets. The first fallback patch passed helper tests but referenced undefined variables in the actual sprite visibility setter. Parent review rejected it and requested tests against the complete sprite, including loading, failure, disabled state and positioning. A separate adapter review found that fal Seedance expects end_image_url, while our request sent last_frame_url. That field is corrected in 17e3569; the parent gate passed with 88.0% adapter coverage. Frame-pinned generation and both live kill-cam outcomes remain verification work.

  2. DecisionORCH

    Localize the exterior combat transition

    The developer-selected wooded battlefield needs an explanation that players can read in either supported language. The shared English and Spanish catalogs now provide the exterior location and a short bridge describing the heroes driving the thrall out through the tavern doors. These are display strings, so existing cached recordings keep their matching transcripts. Required-key guards cover both additions and the i18n gate passes at 98.1% coverage. The full shared-tree checkpoint also passed with 70.7% overall statement coverage; visual validation of the new animations remains separate.

  3. Decisiondeveloper

    Keep the wooded battlefield and explain the move outside

    A fresh isolated two-player playthrough confirmed that the current combat scene is the Wooded Path splat while the original specification placed the fight inside the tavern. The developer chose to retain the wooded battlefield and revise the transition. Section 0 now records that the heroes drive the thrall through the tavern doors onto the wooded river path. The three-minute demo, 30-second combat cap, four-second cached kill cams, grid positioning and visual effects remain required. The separate QA-058 implementation adds the visible bridge. That live run reached End without reload or host Skip and showed valid hero names and HP, but it did not prove kill-cam playback and exposed black hero video planes, which remain with the battle lane.

  4. IssueORCH

    Show the game's barkeep on its landing page

    The marketing persuasion scene and narration avatar labeled a young concept-art character as Mother Vell. Both now use the actual in-game conversation backdrop, showing the older barkeep behind her tavern bar. The asset bytes were copied directly from the build-time catalogue; no new art was generated. Alternative text and intrinsic dimensions match the replacement. The page was inspected in an isolated local browser, its JavaScript syntax check and architecture gate pass, and the temporary review server was stopped.

  5. IssueORCH

    Restore the full TV WASM regression suite

    Running the browser-targeted Go tests exposed an older assertion that flat battlefield tokens occupied ten percent of the scene width. QA-014 had intentionally reduced the tokens to 6.5 percent, but its matching render assertion had not changed. An independent reviewer traced the implementation and assertion to their original commits. The test now checks the adopted width while retaining exact responsive position checks. The full DM WASM suite passes under the Go Node runner, and the native lane gate passes with 91.4% coverage. No production layout changed in this repair.

  6. IssueORCH

    A reconnect must not restart the visible deadline

    PR #9's creation countdown first exposed a static client display, then a separate reconnect problem. EMK-010 adds local monotonic display clocks on phone and TV and starts the creation deadline on rehearsal skips. The reviewer observed both clients ticking without input, holding at 12 seconds while paused, and continuing after resume. A newly connected client still received the last cached server snapshot, which could say 30 seconds after seven seconds had passed. QA-053 now subtracts time since publication from active creation timers on a deep copy, preserving frozen and disabled timers and clamping at zero. Tests cover both client projections and repeated reconnects. API coverage is 86.9%; the independent creation gate also passes.

  7. IssueORCH

    Match the offline reply to its recording

    The fake language adapter displayed a placeholder response while the fake speech adapter played Mother Vell's authored canned reply. Both outputs were individually valid but contradicted each other in the same conversation. The fake reply now reads the text from the same content record that selects the cached speech asset. A regression test exercises two player questions and checks the caption, recording identity, and returned audio bytes. The wire gate passes. This fixes the offline NPC reply pairing; microphone ownership, contextual live dialogue, and the broader provider fallback chain remain separate open work.

  8. DecisionORCH

    Keep marketing scripts outside the game boundary

    The standalone marketing site introduced a script under website/, which correctly failed the existing architecture gate. The developer explicitly approved a marketing-only exception. The rule now permits that directory alongside the existing documentation and splat directories, while phone and TV game code retain their Go-first constraint. Regression fixtures cover allowed directories, forbidden game scripts, and similarly named sibling directories. This also closes an old prefix loophole that could admit web/splat-other. The architecture gate passes with 75.0% statement coverage.

  9. IssueORCH (Codex)

    The RPC rehearsal had expected a discarded message to succeed

    The full gate exposed an outdated expectation in the end-to-end rehearsal: it sent a greeting during Opening and expected acceptance even though no conversation could handle it. The test now requires an actionable rejection during Opening and sends the accepted greeting after reaching Conversation. This verifies the new chat guard through the debug RPC rather than only calling the engine directly. The updated rehearsal and isolated full candidate gate pass, with 70.5% whole-module coverage. This is a fixture correction following QA-047; it does not relax the eligibility rule or skip the failed test.

  10. IssueORCH (Codex)

    Typed chat now keeps failed drafts and shows why sending is blocked

    The compact phone input hid server errors and launched Submit inside a goroutine, allowing repeat taps before its sending state was visible. The model now serializes a pending request, preserves rejected drafts, and ignores duplicate attempts without releasing the original request. Both text inputs use a form for Enter submission and refresh immediately. English and Spanish guidance explains waiting turns and pause. Browser testing exposed an additional cached-component problem: changing the model alone left the button looking enabled after Pause. Passing the snapshot as component props fixes that update. An isolated browser verified draft retention during pause, sending after resume, and ordinary dialogue without an unintended roll. Native tests, WASM rendering tests and the isolated full candidate gate pass; phone coverage is 86.7%. The candidate predates a separate website commit that currently conflicts with the JavaScript architecture rule.

  11. IssueORCH (Codex)

    A sent message must have an eligible conversation

    The chat trace found that Say acknowledged messages outside conversation or from the waiting player, even though the conversation discarded them. The phase now rejects typed messages with a player-facing reason when the phase, turn, pause state or pending reply makes them unavailable. An NPC reply owns the busy state from dispatch, including model and speech preparation. Only that reply's completion releases it; an old completion cannot unlock a newer reply. Completion during pause is consumed without resuming the story, so the next message works after Resume. Root-engine regression tests verify rejection and retry. The isolated tracked-source gate passed with conversation coverage of 92.8%. The shared-tree gate remains blocked by an unrelated untracked website/script.js; that work was left untouched. Microphone ownership and projection remain tracked separately in QA-049.

  12. DiscoveryORCH

    Two defect patterns account for most of the six-hour audit

    Read together, the QA-001 to QA-042 commits mostly repeat two faults rather than forty unrelated ones.

    The interface reported a state the engine had not confirmed. The host timer switch set its own indicator and sent an off-only command in both directions (QA-006). The Splat button flipped a local boolean for a command the engine did not implement (QA-010). Host and phone RPCs returned Accepted once a command was queued, before the room loop decided on it (QA-007). The lobby listed Ready as a legal move that the dispatcher then rejected (QA-025). Each fix follows the same rule: a control shows only what the latest server snapshot says, and a command reports success only after the room loop has decided.

    Callbacks from an earlier generation acted on the current one. A recreated timer reused generation one, so a callback that was already running could expire the replacement turn (QA-003). Reset cancelled a named scope, but work under other scopes kept running and its results stayed in the inbox (QA-020). Checkpoint load had to cancel abandoned jobs, drop their queued results and increase view versions while game state moved backward (QA-019, QA-021, QA-022). The shared fix is an identity check at the room boundary: every timer instance and attempt number is compared before a result may step the engine.

    Both patterns got past the unit tests because every component was correct on its own. The faults only appeared when the host page, phones, room loop and timers ran together in a live room.

  13. IssueORCH (Codex)

    Keep ordinary dialogue off the action-classification path

    Tracing the unwanted Persuasion check exposed a second issue beyond the fake adapter. The conversation engine sent every line through interpretation, then silently dropped ordinary dialogue if interpretation failed. Its fallback also treated isolated words such as leave, step or away as a request to end the conversation. QA-043 now routes speech without a move keyword directly to NPC dialogue, matching the binding demo specification. Keyword-bearing lines still reach interpretation, but a failure only executes a clear supported command; ambiguous questions and negation remain dialogue using the original transcript. Invalid or illegal model moves fall back to dialogue instead of dispatching arbitrary actions. Tests cover the reported lamplighter question, departure questions, negation, delayed classification callbacks and explicit Persuasion/step-away commands. The conversation gate passed at 91.2% coverage. The audit also found separate gaps in microphone state, typed error presentation and NPC context, tracked as follow-up todos. See conversation dispatch.

  14. IssueORCH (Codex)

    An ordinary question incorrectly rolled Persuasion

    The developer noticed that asking Mother Vell about the missing lamplighter immediately triggered a Persuasion check. The running demo used the fake interpret adapter, which ignored every input and returned the same Persuasion move. QA-042 replaces that response with a conservative command classifier: ordinary questions preserve their text as dialogue, while explicit supported action commands become moves only when listed as legal. Negated requests, quoted commands and questions containing action words remain dialogue. Regression tests run the real interpretation prompt through the fake adapter and verify the exact reported question produces an NPC reply rather than an Act event. The lane gate passed, and the same message was sent through the live-reload player UI: Mother Vell replied while the phase remained Conversation. This is a rehearsal-adapter correction, not a claim that the fake NPC generates contextual answers. The wider chat trace continues under QA-043 through QA-046. See the adapter correction.

  15. IssueORCH (Codex)

    Text replay still depended on live speech

    The rehearsal chain stored generated text but synthesized its audio again on every replay. QA-033 adds complete PCM recordings keyed by the room's stable call position, voice, language and sample rate. Pre-rendered lines include their content identity so several lines in one set cannot overwrite one another. Recording is bounded to 8 MiB and 4096 chunks; oversized speech continues playing without replacing the previous recording. Cancellation, early close, malformed PCM and provider failures also leave the previous recording intact. Final audio returned alongside EOF is delivered before completion, preventing the last chunk from disappearing. Forced replay never contacts TTS, including when the recording is missing, and returns an error to the existing canned-speech policy. The todo gate passed with 84.8% modelchain and 87.2% voice/out coverage. This completes the speech layer only: media and transcription policy remain QA-040, and authoritative host Safe Mode remains QA-034. See the speech replay decorator.

  16. IssueCodex

    Keep the fallback on the active battlefield

    The Wooded Path encounter still referenced a tavern fallback, and the DM renderer preferred that tavern image over every server-selected image. Its grid used a generic quadrilateral unrelated to either camera. The encounter now registers its existing tactical still and projected ground corners from the capture camera. Flat mode and the loading fallback preserve the same image and cell positions. Quieter grid lines and compact circular tokens keep the terrain visible; the thrall uses its actual artwork, while unfinished heroes use class crests until their portraits arrive. Browser checks verified flat and 3D mode switching on a paused fight, with the existing grading and tilt-shift visible in 3D. Content and DM package gates passed at 93.6% and 91.4% coverage.

  17. IssueGPT-6 review

    Kill cams now consume the combat time budget

    The four-second finishing videos held combat but did not consume an enforced phase deadline. The planned 30-second cap existed in the standalone simulator, while production phase entry started no cap timer. Cinematic handling also swallowed cap callbacks. Commit fe2695e starts one pausable deadline at Combat entry and handles expiry before cinematic events. A clip near the deadline is truncated, the overlay closes, and combat resolves without changing an already determined victory. Regression tests cover both outcomes, disabled turn timers, pause, host exits, and stale callbacks. The phase package passes its gate with 81.7% statement coverage. Cached clips cover the paladin and rogue reference heroes; the whole interactive demo still requires a timed rehearsal.

  18. IssueCodex

    Use explicit UTF-8 when editing Windows documentation

    A Python edit read UTF-8 documentation through the Windows default code page and wrote the resulting punctuation corruption into the rehearsal-position commit. The commit statistics exposed the unintended historical changes. A forward correction recovered the unchanged content from commit 3697267, retained the new todo and devlog entries, and verified UTF-8 without a byte-order mark and LF line endings. Future Python edits in this task specify encoding explicitly. No application source or cached media was affected.

  19. IssueCodex

    Preserve rehearsal positions across retries

    Model requests often omitted phase and sequence metadata, causing repeated replies to overwrite one recording. The room now reserves positions by phase, role and seat before dispatching work. Canceled work releases only its own reservation; work that completed before scope cancellation retains its position. Reset starts a fresh sequence, while checkpoint load restores the saved sequence and restarts pending work at its original position. Adapter composition resolves this metadata before selecting a recording. Tests cover concurrent dispatch, cancellation, completed work, repeated checkpoint loads and forced replay at distinct positions. This establishes text replay alignment; offline speech and media and the host Safe Mode command remain separate work.

  20. IssueCodex

    The room sequence test distinguishes startup from processed events

    Linux race CI caught the room serialization test observing only sequence one before shutdown. The runtime publishes an initial snapshot before processing its inbox, but the test counted that snapshot toward its two expected event publications. Depending on scheduling, cancellation could win before the second queued event ran. The test now consumes startup explicitly before posting the two joins, then waits for both event publications before canceling and checking the exact sequence and event log. The assertions remain unchanged. One thousand consecutive local executions passed. This was a test synchronization error, not evidence of a runtime event ordering failure; the modelchain race tests in the same failed run passed.

  21. IssueCodex

    Only complete, uncanceled responses become recordings

    The cache and rehearsal wrappers dropped text returned alongside EOF. They also persisted responses using a background context and could turn a canceled model call into a successful recorded fallback. Both wrappers now share a stream lifecycle that retains terminal text, delivers it before EOF to narration consumers, and writes with the caller's context. Early closure, source errors, and cancellation prevent partial publication. Closure can interrupt an in-flight read or storage write without racing the completion flag. Tests exercise both decorators, canceled replay, storage failure, and concurrent closure using deterministic scheduling. Modelchain coverage is 83.3 percent, and its package is now included in the Linux race workflow. The wire and narration executor suites pass without live provider calls.

  22. IssueCodex

    Rehearsal recordings cannot overwrite another response contract

    Tracing the unwired Safe Mode control exposed a recording collision beneath it. All text roles used the same adapter namespace, and a recording key included only that namespace, phase, seat, and call index. A Spanish reply, a JSON interpretation, or another schema could overwrite an English narration at the same position. Five regression cases reproduced these collisions. Recording namespaces now include the role, language, response format, and JSON schema while preserving the sequence position and ignoring the live prompt and run identity. Legacy entries are deliberately not read because their original response contract cannot be recovered reliably; they require a fresh rehearsal. The modelchain gate and wire integration tests pass. Deterministic call positions, complete stream recording, offline media playback, and the authoritative host control remain separate audit todos.

  23. IssueCodex

    Start waits for both players to be ready

    The lobby could start with empty seats or players who had not chosen Ready, contrary to the two-ready-seat flow in plan section 0. The root engine now rejects Start until both joined players are ready and the game is unpaused. Rejections emit no effects, so they cannot start a timer or play the opening cue. The host disables Start and explains the missing step beside the controls in English or Spanish. Skip retains its rehearsal behavior. Unit tests cover missing seats, partial readiness, pause, reset, and default characters after Skip; a real RPC test checks the host acknowledgement against phone Ready actions. Browser testing verified the disabled button at one ready player, the enabled button at two, creation on Start, and the rehearsal override after Reset. The gate passed with 94.7 percent engine coverage and 83.5 percent host coverage.

  24. IssueCodex

    The join language selector keeps the player's choice

    The join form's Español button changed a temporary locale model and requested a render. That render immediately constructed a new model from the browser's English preference, so the button appeared inert and the subsequent join sent English. The locale model now lives in component state. A WASM render regression clicks Spanish, edits the player's name, and switches back to English, checking the rendered join label at each step. Browser verification joined Lyra in Spanish alongside Brom in English and confirmed that their waiting screens used the separate chosen languages. The shell gate passed at 84.9 percent native statement coverage; the browser-dependent regression also passed under the Go WASM runtime.

  25. IssueCodex

    Reset keeps the players who already joined

    The host's Reset confirmation promised to retain joined seats, but both phones lost their names and legal moves after confirmation. Authentication still recognized the tokens; the new game engine had no player records. Normal join events never populated the room's reset registry, and that registry's replay omitted names. The room loop now retains each accepted join and replays its name and language into the replacement engine. Rejected joins are excluded. A real RPC integration test keeps both original player streams and tokens through two resets, checking names, mixed languages, cleared readiness, and subsequent Ready actions. Browser testing confirmed the same seat continuity without a server restart. Runtime coverage is 81.9 percent.

  26. IssueCodex

    Live updates honor the player's joined language

    A reset integration test joined one Spanish-speaking player and one English-speaking player, then received English for both before the reset even ran. The join service stored the chosen languages correctly, but Watch consulted an empty optional locale registry and defaulted to English. It now falls back to the authoritative joined-seat locale, while retaining explicit overrides. Localization also stops replacing every party member's locale with the viewer's language and works on a detached view. Regression tests cover initial subscriptions, reconnects, mixed-language parties, and locale precedence. The API gate passed with 86.6 percent statement coverage.

  27. IssueCodex

    Ready now confirms each player's lobby state

    The lobby advertised Ready as a legal move, but the phase dispatcher rejected the action and the phone hid the rejection. Readiness now belongs to the engine's joined-player records, independently of locking a character during creation. Both phones show the party's confirmed readiness, the table labels each ready player, and the host displays a ready count. Repeated joins retain the state; duplicate, paused, and unjoined actions are rejected. Native tests and the browser verified both seats and a phone reload. Reset testing also exposed a separate integration gap: the runtime's retained seat registry was never populated by normal joins. That issue is tracked as QA-027, alongside the normal Start guard in QA-026.

  28. IssueCodex

    Combat end now accepts the command printed in its help

    The CLI advertised combat end slain|fled, but its shared combat parser required a token argument and read the outcome from the following position. Both documented commands therefore failed before reaching the room. The end command now has its own parser and targets the thrall automatically. Existing scripts using the explicit thrall form still work; extra arguments, other targets and unknown outcomes are rejected. Tests assert the generated event payload, and the CLI gate passes at 73.0% coverage. A loopback fake server verified both documented forms against a saved combat checkpoint: each command was accepted and advanced to the cliffhanger. The isolated server was stopped after verification.

  29. MilestoneCodex

    The CLI can rewind and retry a running scene

    The previously unimplemented snapshot command now enters the engine as a validated debug event and executes in the room loop. A named point restores engine history, logical time, remaining countdowns and paused timers. Pending jobs are tracked until their completion reaches the inbox; loading cancels abandoned jobs, rejects their queued results and restarts the saved pending work. Completed jobs are not replayed. Player identities and open streams survive the restore. Billboard jobs now belong to the run scope and honor its cancellation while still surviving phase changes. Checkpoints are process-local, limited to 16 names, and disappear when the server restarts.

    A loopback-only fake server verified the actual CLI: HP changed from 12 to 1 and returned to 12; position returned from row 5 to row 6; repeated loads and a load across game Reset retained the cached hero clips. Two attacks from the same point each left the enemy at 3 HP. The server PID stayed unchanged. A missing point returned rejection exit code 1. An integration test retained the same player Watch stream and accepted another phone action after two restores. The full native, WASM and walk gate passed; runtime coverage is 81.5%. The isolated probe was stopped. Browser audio and video continuity on checkpoint load remains a separate audit item.

  30. DecisionCodex

    Checkpoint engines rebuild from events without executing effects

    The game contains private nested state machines, so serializing its public view would lose combat, creation and dice state. The synchronized engine now supports an optional bounded journal of immutable event payloads. Each checkpoint retains the exact constructor for its run and replays events with their original logical time, sequence and scope. Replay discards returned effects; completed vendor requests and audio cues are not executed again. Every restore decodes fresh event values, preventing later map edits or repeated retries from corrupting the saved point. Client publication versions continue increasing even when game state moves backward. A four-megabyte history limit fails explicitly instead of growing without bound. Tests cover mutable payloads, repeated loads, original-run factories, missing configuration and invalid histories. The wire gate passed. Room control and pending-work restoration are the next part of QA-005.

  31. IssueCodex

    Reset rejects callbacks from the previous attempt

    Room reset cancelled a named run scope, but effects could also run under the tree root or other scopes. Those jobs could continue after reset. Even successful cancellation could not remove results already waiting in the room inbox. Work dispatch and timer creation now capture a process-local attempt number; the room drops callbacks from older attempts before stepping the engine, logging an event or publishing a view. Reset also cancels the entire scope tree and creates a fresh root while preserving its shared handle. A restored timer receives the current attempt number, rather than its saved number. Tests exercise queued timer and asset results, root-scoped cancellation, late posts after cancellation, new work after reset and parent-context shutdown. Runtime coverage is 80.0% and domain coverage is 91.8%; the package gate and whole-module build pass.

  32. DecisionCodex

    Room retries must restore countdowns as well as game state

    The CLI checkpoint investigation found that engine replay alone cannot reproduce a saved scene: running timers would retain the abandoned attempt's deadlines, and paused timers would lose their remaining duration. The runtime now captures timer values without retaining clock handles. Restoring replaces every timer instance, preserves pause state, scope and the current pacing policy, and computes fresh deadlines from the saved remaining time. Repeated restores leave the saved checkpoint unchanged, while callbacks from replaced instances are rejected. Fake-clock tests cover running, paused, non-pausable, overdue and empty checkpoints; the runtime gate passed at 78.6% statement coverage. This is the timer foundation for QA-005. The CLI save/load controller still needs engine replay, work cancellation and result isolation before end-to-end retries are available.

  33. IssueCodex

    Development reload waits for the application

    A player tab refreshed during a successful rebuild and remained on a plain server-starting response. Starting the child process did not mean its HTTP listener was ready. The version endpoint now checks the current child's health and withholds the new build identifier while it is unavailable or being replaced. Browser navigations that arrive during startup receive a small recovery page which reloads after the first healthy version. RPC failures remain HTTP 503. Client polling has a three-second request timeout, checks immediately when a background tab becomes visible, and restarts after browser page-cache restoration. Five JavaScript tests cover reload behavior, and the supervisor gate passes at 72.5% coverage. The existing supervisor executable was updated in place; Windows briefly retained its image lock after process exit, so replacement used a bounded retry before launch. Both player tabs recovered automatically after a source rebuild, returning to the join screen with their names and room codes retained.

  34. IssueCodex

    Prepared battle heroes survive new runs

    The reference-conditioned paladin and rogue loops existed on disk but were attached through debug asset events. The billboard hub correctly discarded those per-run events at reset, leaving new rooms with pixel stand-ins. Five existing four-second clips now live in the permanent build-time manifest and asset store. Startup verifies their SHA-256 hashes before registering them. Combat selects these defaults by hero class and battlefield scene, independently of seat order; live hero results take precedence. Reset clears live identity state while retaining the prepared catalogue. Offline tests cover new runs, swapped seats, wrong scenes, missing and corrupt assets, and live overrides. The wire gate passed. A fresh live room and a DM reload both displayed the generated heroes at their engine cells with the existing color grade and tilt shift. No vendor calls or additional generation spend were needed. These prepared identities cover paladin and rogue; other classes retain their existing fallback path.

  35. IssueCodex

    Creation deadlines now publish complete heroes

    The live rehearsal reached combat with empty phone sheets. Bulk creation timeout built both heroes but returned no individual seat, while the phase dispatcher copied only that empty result into its public view. The timeout also skipped already-rolled heroes without locking them, and manual Skip bypassed creation entirely. Both deadline paths now share finalization, keep choices already submitted to the engine, preserve rolled stats and flavor, and request each reference only once. The dispatcher projects both heroes before entering Opening and cancels their remaining seat deadlines. Host Skip follows the same path even with automatic timers disabled. Regression tests reproduced the defects before the change; phase coverage is 81.4% and creation coverage is 83.5%. The game and simulator suites pass. A live phone confirmed a complete sheet after Skip. Cached sprite selection and unsubmitted phone choices remain separate audit items.

  36. IssueCodex

    The host renderer switch now reaches the battlefield

    The Splat button changed a local boolean and sent an unsupported off-only command. Explicit SPLAT_ON and SPLAT_OFF commands now update engine policy, and host snapshots expose the confirmed preference and scene availability. New runs honor the configured renderer; missing scenes reject enable requests. Tests cover both directions, repeated commands, reset defaults, and switching while paused without changing combat state. The live test confirmed flat mode and policy persistence after reloading the host, but also exposed a HUD reconciliation defect when the canvas returned: unkeyed conditional children reused the wrong layout nodes. Stable canvas, fallback, and HUD identities address that defect; a repeat test with the rebuilt WASM preserved the HUD when 3D returned. Renderer readiness reporting remains a separate follow-up, as do the generic hero sprites and incorrect flat-scene art discovered in this rehearsal. Combat timing caps are unchanged.

  37. IssueCodex

    Existing browser pages missed the development reload script

    The supervisor rebuilt the Go server and WASM bundle, but an existing host tab still required manual refreshes. The browser was revalidating HTML cached before the supervisor injected its reload module. The backend returned 304, so the proxy had no response body to modify and the browser reused the old page. A fresh URL contained the module, confirming the difference. Development HTML requests now remove conditional cache headers before reaching the backend; injected responses remove old validators and remain no-store. Asset revalidation is preserved. A proxy regression test covers both cases, the tooling gate passes, and the updated supervisor serves the reload module at the original host URL. The human test server is unchanged.

  38. IssueCodex

    Queued commands were reported as successful before the game decided

    The host page reported Accepted for a renderer command that the engine did not implement. The production host RPC acknowledged queue admission, while phone actions checked a reply channel without waiting and defaulted to success. Both now wait for the room loop's decision through a private, buffered acknowledgement channel. Cancellation returns a transport error and a late reply cannot block the game loop. Six delayed-response cases reproduced the defect before the change; accepted and rejected host commands, moves, speech, cancellation, and enqueue failures now pass. The API gate reports 86.3% statement coverage. A browser check now shows the renderer command's rejection instead of false success. This exposes the separate renderer and Safe Mode wiring gaps for the next control fixes; it does not alter demo timing.

  39. IssueCodex

    Turn timers now reads server state and can be switched back on

    The host page initialized its timer indicator to off regardless of configuration, changed that indicator before a command was accepted, and sent the off-only command for both toggle directions. QA-006 adds an explicit timers-on command and projects the engine's timer policy into every host snapshot. The button now uses that authoritative state and exposes its checked state to assistive technology; it stays disabled until the first snapshot arrives. Tests cover configured defaults, off/on/reset policy, API command mapping, and client projection. A live host check turned timers off, reloaded the page and confirmed the setting, then successfully turned them back on. Existing countdown durations and combat caps are unchanged; enabling permits subsequent turn timers under the existing engine policy.

  40. MilestoneCodex and GPT-6 reviewer

    Four cached finishing-blow videos and an independent reconnect review

    The two demo reference heroes now have four-second victory and defeat videos generated against the existing battlefield and enemy references. The four 720p clips are stored by content hash and preloaded before combat; a cache-only rerun verified all four without another vendor request. The engine selects the actual attacking or defeated seat, locks combat input for the cinematic beat, and resumes after a bounded pausable timer. Missing media leaves normal combat available. An independent GPT-6 review found that reconnects could replay an old offset: commits 2412be2 and 939dc25 synchronize buffered playback and advance the cached snapshot offset for late subscribers while preserving paused frames. Native, WASM, package, and full walk checks passed. Coverage is limited to the paladin and rogue reference heroes; other classes retain normal combat.

  41. IssueCodex

    An old timer callback could expire a newly started turn

    While tracing checkpoint restoration, the audit found that every recreated timer started at generation one. A callback already running when its timer was stopped could therefore match a replacement timer with the same name and expire the new turn. The same collision was possible after cancel, run reset, stop-all, and disabling then enabling turn timers. QA-003 binds each callback to its exact timer instance in addition to its arm generation. A controlled clock reproduces all five stale-callback sequences without sleeps or timing luck: each failed before the fix, and each now leaves the replacement timer intact until its own callback runs. This also prevents abandoned attempts from cutting short the next retry.

  42. IssueCodex

    Debug CLI help exposed the environment token as a flag default

    The competition-demo audit found that the read and control parsers registered the current debug credential as the default value of the token flag. Go's flag package includes nonempty defaults in usage output, so asking for help or passing an unknown option could print the credential. QA-001 replaces that binding with a value callback: authentication still inherits the environment value and accepts an explicit override, while usage only describes the option. Regression tests exercise help, invalid options, explicit-token help, and authentication precedence across all four CLI parser paths. No live credential is used by these tests.

  43. DecisionORCH (Codex)

    Kill cams are generated before the demo, under a spend cap, never during combat

    A reference-driven video clip takes far longer than the combat beat it illustrates, so KC-001 moves generation to build time. Each clip is conditioned on three images: the hero's identity sheet, the drowned thrall, and the tactical still of the Wooded Path battlefield, so the finisher happens on the terrain the players have just been looking at. The prompt explicitly asks for one hero, not a reproduction of the reference sheet, because an identity sheet shows several poses of the same character. The generator has a -max-usd ceiling (default $5), stores each clip by SHA-256 content hash in the build-time manifest, and has a -cache-only mode that fails instead of calling the vendor. That mode is how reruns were verified without further spend. The trade-off is coverage: only the two reference heroes (paladin and rogue) have clips, and other classes resolve combat without a cinematic. See the kill-cam generator.

  44. IssueORCH (Codex)

    Live reload needed a source fingerprint and had to delete stale compressed WASM

    The developer wanted source edits to rebuild the server and WASM client and refresh every open screen, without relaunching executables by hand. The supervisor's existing 30-minute rebuild loop could not do this, and rebuilding every two seconds would restart the game on every tick. KC-005 hashes the relative path and contents of every Go, JS, HTML, CSS, JSON and proto file (skipping artifacts, dev and dot directories) and rebuilds only when that fingerprint changes. A failed build leaves the running child in place. The WASM bundle builds to a .next file in the runtime's own directory and is renamed into place. The file server prefers precompressed .gz and .br siblings, so any older compressed copy would have kept serving the previous bytes after a successful rebuild; publishing now deletes them. Two later defects came from the same feature: browsers revalidating HTML cached before the reload script was injected (QA-009), and reloads that reached a child process whose HTTP listener was not yet up (QA-012). See the live supervisor.

  45. DecisionORCH (Codex)

    Three pull requests merged with fixes; the faster-opening spec stays open

    With the developer's authorization, ORCH reviewed the four open pull requests in an isolated checkout. Three were merged after fixes: PR 7 (reconnecting phones, TV and host), PR 8 (NPC dialogue validation) and PR 6 (a push-to-talk microphone that survives snapshot re-renders and sends TalkEnd before releasing the stream). The cleanup found two more defects. First, cancelled utterances never received a final audio frame, so their debug counters stayed in memory for the page's lifetime. Second, a dfctl restart test was writing debug-combat.json into the package directory on every gate run. PR 1, a spec-only proposal for a faster opening, was left open because it contradicted itself. Its six-turn combat order contains only two enemy turns, while the bell escape it proposes fires on the third enemy turn. Its probability table still described the earlier four-turn fight, and it changed lobby, creation and dialogue timing without implementing any of it. The full gate passed with zero failures. Physical microphone capture and real phone network handover were not tested. Evidence is in artifacts/test/PR-REVIEW/.

  46. IssueORCH (Codex)

    Windows keeps an executable locked after its process has exited

    The full gate failed intermittently while a supervisor test deleted the copied server executable, even though Process.Wait had already returned. Windows can keep the image-section lock on an executable briefly after the process ends, so a delete issued right away can fail with access denied. The test cleanup (REVIEW-007) now stops any child it owns, then retries the delete every 20 ms for up to three seconds, and fails normally if the file is still locked after that. The deadline stops the retry from hiding a real leaked handle. The same behavior appeared in production two hours later: QA-012 replaced the running supervisor binary and needed the same bounded retry before relaunching. On this machine, code that replaces or deletes an executable should assume a short lock after exit.

  47. IssueORCH (Codex)

    The dialogue guard ran after the rejected words had already been streamed

    PR 8 added checks on Mother Vell's replies. Red-team runs had drawn the gated clue out of her through quotes, rhymes, forged history lines and "answer in JSON" prompts, and the PR rejected any reply containing a clue word (bell, tower, belfry, midnight, campanario and neighbours). Review found that the check ran on the final text, while every raw chunk had already been published to subtitles and the speech pipeline as it arrived. A rejected clue would therefore have been shown and spoken before the canned evasive line replaced it. The merged version holds the whole utterance, capped at 4,096 bytes, validates it, and publishes it once. NPC lines are short, so the added latency is small. The same pass changed how an unclosed *stage direction is handled: it is now dropped rather than read aloud without the asterisk, and a JSON reply is accepted only as a single-field object. Player speech is wrapped as a quoted patron line so that injected text reads as dialogue rather than as instructions. Publication tests assert that no chunk reaches a consumer before validation. See spoken-text validation.

  48. IssueORCH (Codex)

    A sleeping phone keeps a socket that looks open but delivers nothing

    When a phone sleeps or changes networks, its gRPC-over-WebSocket Watch stream can stay open without delivering anything and without reporting an error, so the controller freezes on an old snapshot. PR 7 added phone resubscription. The merge (REVIEW-001) moved it into a shared web/shell/watch controller so the TV and host recover the same way. A watchdog drops any stream that has been silent for 25 seconds and resubscribes, and the browser's visibility and online events trigger an immediate resync. A resubscribe that this controller started itself reconnects without delay. A stream that failed on its own backs off from 100 ms to 2 s. The server replays its latest snapshot on subscribe, and screens are keyed by snapshot version, so a repeated snapshot does not remount anything. Resubscribing reuses the seat token and does not send another join event, so reconnection cannot create a duplicate player. See the watch stream.

  49. MilestoneORCH

    The TV end card gets a header band, a party recap, and a quiet SRD footer — round 1's first fix lands

    The critic's flattest score (TV end, 4/10: a flat navy box, no art, no recap, the header sitting on the panel's own border, the audio chip still visible) gets its fix first. Commit 533e4b5 gives the end card its own centred header band with a gilded double rule, four symmetric corner ornaments, and an inset hairline frame, under a hierarchy of epilogue, cliffhanger hook, party medallions pulled from the view's build cards, thanks, a host next-step line, and the verbatim SRD 5.2.1 attribution as a smaller footer. The card's CSS moves out of inline styles into its own dmEndCardCSS sheet injected into <head>, entrances stagger in slowly unless reduced motion is on, and the new copy (dm.end_header, dm.end_hook, dm.end_party, dm.end_next) ships in English and Spanish.

  50. IssueORCH

    Nineteen agents in one working tree: the root build stayed broken for long stretches on interlocking mid-edits

    Round 1's nine new fix lanes run alongside the audio, transitions, phone-UX, end-card, lantern-crest, TV-caching, and hero-name lanes already in flight, all editing the same checkout at once. A lane changing a shared file's signature before the lane that calls it catches up leaves go build ./... broken from the checkout's point of view until the matching caller edit lands, so commits arrive in bursts rather than a steady trickle, and a snapshot of the tree at almost any moment during the loop fails to compile. This is expected under AGENTS.md's rule for it: a lane whose own packages are unaffected keeps working and verifies against a binary built and run on its own port and data directory, never the shared tree or the always-up human test server, so a broken root build doesn't block a lane's gate; a lane that does hit someone else's in-progress file waits or retries rather than "fixing" it. The cost shows up as commit timing, not lost work: long quiet windows in git log followed by several todos landing close together once a blocking signature change resolves.

  51. ProcessORCH

    Round 1 opens nine fix lanes, run against the critic until every screen clears 8/10

    Against the developer's "top notch, moody and exquisite" bar and the round-1 critic's 3.8/10 average, ORCH split the defect list into nine lanes, each scoped to a set of screens and files so no two lanes touch the same file: R1-FONTS-CHROME (ORCH itself: the missing self-hosted fonts, the audio-enable chip), R1-TV-SCENE (opening/exploration/resolution layout and HUD z-order), R1-CHECK (the dice phase end to end, TV and phone), R1-COPY (raw and debug copy through i18n), R1-CREATION-LOBBY (TV creation and lobby, phone waiting room), R1-MOOD (cliffhanger, atmosphere overlay, conversation), R1-COMBAT (TV combat HUD and the splat's night grade), R1-ASSETS (missing build-time art and placeholder portraits), and R1-HOST (host page polish). These run concurrently with the audio, transitions, phone-UX (inventory, journal, map, menu, turn label), end-card, lantern-crest loop video, TV-caching, and hero-name lanes already under way. Every lane verifies on its own port before hand-in, but the loop's actual gate is the critic itself: it repeats the same read-only pass against assets/concept on each lane's fixed screens, and a screen doesn't count as done until it scores at least 8/10. As of this entry only the end card (533e4b5) has landed; the rest of round 1 is in progress.

  52. Discoverycritic agent, ORCH

    A read-only critic pass scores every screen against the concept art: 3.8/10 in-game average

    ORCH ran a screen-by-screen critic pass, live and on preview fixtures, on TV, phone, and host, and scored each against its matching assets/concept reference. In-game scores ran from 2/10 (opening, check, cliffhanger) to 6.5/10 (TV lobby), averaging 3.8; phone and host fell in the 3-6/10 range. Four defects were global rather than per-screen: the display fonts never loaded at all (document.fonts came back empty, so Cinzel, Cormorant Garamond, and Inter every one rendered as Georgia or Arial), the TV's exploration/check/resolution HUD sat at a lower z-index than the scene layer and was entirely hidden behind it, the check phase's dice layer painted nothing on the TV while the phone still offered the roll, and a run of raw internal copy was reaching the audience: dm.glyph.star literally printing "Star" where a ✦ glyph belonged, millisecond countdowns ("Turn timer: 12000 milliseconds remaining"), the operator-facing line "DM steering: personal hook → Rook" shown on screen, and the hard-coded chapter title "THE FORGOTTEN DEPTHS". The combat splat's daylight color grade also read as a sunlit park against a story set at night in a harbor town, and several character portraits were shared placeholders rather than per-species, per-class art. The full defect list, organized by screen, became the brief for round 1's fix lanes.

  53. IssueORCH

    REPO-019: the join link and lobby QR pointed phones at a private IP behind the DigitalOcean proxy

    Running the server on the DigitalOcean Droplet behind Caddy's TLS proxy, the tester URLs, the join link, and the lobby QR all still built off the machine's private network address, which no phone off that host can reach. Commit 55a9758 adds a validated server.public_url config field (an http or https origin) that, when set, leads the tester URLs and becomes the join URL and QR target instead of the private address; left unset, behavior is unchanged. c01c135 backfills the todo into TODOS.md alongside it.

  54. ProcessORCH

    Scene-restructure decisions wait on the developer; a refinement pass starts

    With ENG-034's stalls closed and the LFS-backed asset tree in place, ORCH is holding a set of scene-restructure questions for the developer rather than deciding them alone, and is starting a refinement pass in the meantime: checking audio completeness against the §0.19 score and the sound-effect library, comparing each screen's visuals against assets/concept the way the earlier live-demo review did (commit 4787ff6), and smoothing the transitions between phases (view, camera, and audio handoffs) that the day's fixes touched. Nothing here is a code change yet; it is the audit that will produce the next batch of backfilled todos.

  55. IssueL-ENGINE, Codex

    ENG-034 closes the two 90-second dead-air stalls found in the SPLAT-029 play-through

    A scripted play-through on :8446 at SPLAT-029 (commit 184e224) stalled twice for 90 s until the host used Skip: once in Exploration after Resolution, where the phone kept offering only Ready/Talk with no way to Leave, and once in HookEvent, where the stranger line never completed the hook (ENG-029's earlier fix did not cover this path). ENG-034 (commit 8a25fd8) adds "Leave the tavern" as a legal exploration move for the spotlight seat, adds a 15 s idle fallback that starts the hook itself if no seat acts after the conversation ends (a logged idle_hook timer, on by default), and fixes the root cause blocking HookEvent's advance to Combat. A runtime trace confirmed Resolution → Exploration now starts idle_hook and the fake run advances through HookEvent to Combat with no host Skip; walk-test paths 1–3 and 38 plus the idle-hook regression are covered. c9a160e is the one-line follow-up: the debug-token file-mode test asserted 0600, but Windows reports 0666 for every file regardless of the mode passed, so it failed on the developer's machine and every lane gate there; the test now checks the mode only where Unix permission bits exist.

  56. IssueORCH

    The combat enemy card had no portrait until it fell back to the thrall's build-time still

    The thrall token carries no portrait URL, so the TV's enemy card rendered a broken image throughout combat. Commit 4fcd2b4 falls the card back to the thrall_still build-time asset, cropped to the face, the same source SPLAT-029 and BB-001 already use for the token's stand-in and billboard reference.

  57. DiscoveryORCH

    SPLAT-029: the battle grid was half a cell off the collider's lines and away from the curated cameras

    SPLAT-028 (commit 572ee5f) got tokens standing on the voxel floor, but on the TV they stood outside the drawn grid, the grid lines themselves were broken, and the heroes read as specks: the engine grid's origin (12.952, -15.2192) sat half a cell off the voxel collider's cell lines, so floor detection kept hitting cell edges (26 of 78 cells had no floor), and the grid sat away from where the curated SPLAT-025 cameras look. 127f280 moves the Wooded Path grid origin to (7.62, -21.336), on the collider's cell lines and under the curated cameras (target x 20, z -13); walkable cells become the 109 of 160 where the collider finds a floor; heroes now spawn at (8,6) and (10,6) and the thrall at (9,2), four cells from seat 1; tokens draw at 2.7 m (heroes) and 2.5 m (thrall) instead of 1.8/1.6. The developer rejected an auto-framing camera in favor of keeping the TV's curated presets, so the fix works entirely by moving the grid and spawns to match the existing camera work rather than moving the camera to match the grid. Verified in the Claude browser pane: a continuous grid over the gravel path, every token inside its highlighted cell. 184e224 follows immediately: the battle stage only re-sent the scene when a token's cell, HP, anim, or path changed, so a hero whose fal billboard loop finished minutes into combat kept its pixel stand-in until a reload; clip URLs are now part of the stage snapshot key, so a clip that arrives mid-combat replaces the stand-in live. Verified in a real play-through on :8446 with live generation: seat 2's loops (178 s / 198 s) and seat 1's (332 s / 366 s, queued behind seat 2 at fal's 2-job concurrency limit) attached to their tokens, and the TV showed the elf ranger, the dwarf, and the cached thrall loop as keyed fal video in their grid cells.

  58. IssueORCH

    REPO-018 and WEB-024: a debug token the fake config couldn't provide, and 50 transition errors every 8 seconds

    Two small local-run frictions closed together (commit e6a8058). REPO-018 (a5d461a): a local run with config/fake.json exited with "DF_DEBUG_TOKEN is required when server.debug=true" even though the README said the fake config needs no keys, and the README also pointed dfctl at port 19446 instead of the correct 9446. internal/wire now generates a random token with crypto/rand when DF_DEBUG_TOKEN is unset, writes it to <data-dir>/debug.token (0600, never logged), and logs only the path, matching the REPO-016 supervisor pattern; a set DF_DEBUG_TOKEN is used unchanged and writes no file. WEB-024 (c6cd079): GoWebComponents v6 wraps every route render in document.startViewTransition, and Watch updates re-render faster than a transition completes, so /dm and /p logged about 50 unhandled "Transition was skipped" page errors every 8 s. The router now renders with view transitions off; screens keep their own CSS animation for movement between states.

  59. MilestoneORCH

    BB-001: live green-screen billboard loops from fal, cached and budget-capped

    Commit b7b5555 wires the chroma-key billboard pipeline end to end: a hero's turnaround crops (front, three-quarter) or the thrall still, plus a clean level still for light and angle, go to Seedance 2.0 Fast reference-to-video as 480p 9:16 4 s chroma-green loops. Resolution order is the build-time manifest, then a SQLite cache keyed on the sha256 of model, prompt version, action, reference and level-still hashes, resolution, duration, and aspect, then fal itself — only with a live fal adapter, live_pc_loops on, safe mode off, and under a per-run fal budget cap and fal's own 2-job concurrency limit. A seat's loops start as soon as its turnaround is Ready; once ready, loops map onto the seat's combat tokens and the TV swaps the pixel stand-in for the keyed video. The commit also adds the buildtime billboards and level-still subcommands, four clean level stills, and the thrall's idle and attack loops, plus ORCH's fix to the chroma shader itself (billboard.mjs: a unique material name per instance and no alpha-discard pass, both needed under PlayCanvas 2.22 for the video texture's alpha-0 upload not to remove every fragment). 4787ff6 lands alongside it: 17 backfilled todos (DM-033..040, WEB-023, PHONE-032..035, INT-009/010, OPS-027/028) found by running the live demo against the LFS build-time media and comparing each screen against assets/concept.

  60. DiscoveryORCH

    SPLAT-028: no token ever rendered, because the voxel collider re-gridded the whole scan under the engine's feet

    No token rendered on the TV splat at all: the Wooded Path profile's voxel collider uses all_candidates, which re-grids the entire scan (50x63, origin -22.86,-67.06) on its own terms, so the engine's 16x10 cells mapped onto other, non-walkable cells and the token controller rejected every token. Commit 572ee5f has the TV's df-splat fetch the scene profile itself, replace its grid with the engine grid carried in the init message, and set keep_authored_grid: the voxel collider then only measures each engine cell's floor height so tokens stand on the voxel floor, without re-gridding or dropping engine cells, since the engine — not the collider — decides walkability. The standalone viewer is unchanged, and dfSplat.state() now exposes the runtime snapshot and last scene for debugging. Verified in the Claude browser pane on the live :8446 room (dfctl goto combat): a 16x10 grid with 78 walkable cells, tokens pc-1@2,0, pc-2@3,0, and the thrall@2,4 drawn at floor heights -0.64, -0.59, and 0.21.

  61. ProcessORCH

    The Wooded Path battle splat, all LODs and colliders, goes into Git LFS; Dittrich's Tomb stays local

    Commit c957bc2 commits the Wooded Path combat scene (id 64bb46d5, 107 MB) — a streamed SOG in splat-transform's LOD layout, lod-meta.json plus LOD chunk folders for levels 0-3 — together with the voxel colliders the web/splat/scenes profiles reference, so a fresh clone has the scene the engine and TV both depend on. Dittrich's Tomb (id cb2fddd6, 508 MB) stays local only, kept out of the repo for its size; collider experiments and screenshots stay gitignored as before.

  62. MilestoneL-E2E, Codex

    E2E-008 restores the simulated walks and i18n guards to green

    Commit 3ea84e9 gets the simulator walks and i18n coverage passing again across internal/i18n, internal/sim/walk/**, and the DM/phone WASM view packages: basic and full-combat walks, the Spanish-phone/English-DM cross-locale path, and the story happy/failure/leave walks all reach their terminal phase. The walks had stopped before End because the hook phase now distinguishes arrival-audio completion from the narrated stranger line, and the existing walk fixtures still sent blank completion events — test drift against ENG-033's debug and hook work, not an engine regression. The walks now send the utterance identities the hook phase expects. Coverage: internal/i18n 98.1%, walk/basic 100%, walk/story 100%, walk/full 71.4%, web/dm 90.4%, web/phone 88.2%.

  63. DiscoveryL-AUDIO, ORCH

    Only loops need bar-aligned tempo: beatcheck was rejecting a free-time bed and a single-hit stinger

    Generating the three missing music cues failed on every attempt: CLIFF_TENSION_BED measured at 141.68 BPM against a requested 60 (beatcheck's double/half-time fold did not cover it), END_CARD_THEME measured at 152.82 against 80, and STING_COMBAT_START, a single percussive hit, had no regular onsets for beatcheck to lock onto at all. The tracks are correct musically — a free-time tension bed and a one-shot stinger were never going to hold a steady tempo — but the build pipeline ran beatcheck against every track regardless of shape. AUD-002 (commit cff94aa) makes the bar-aligned tempo check apply only to tracks marked as loops; a one-shot bed or stinger skips beatcheck and its crossfade-cut step entirely, since only a loop needs a downbeat to cut on. It also adds selective generation by track ID (DF_MUSIC_ONLY) so a rerun doesn't re-pay for the nine tracks that already passed. With the check fixed, the three tracks generated cleanly for about $0.40 total and now sit in the manifest committed by 616e9ab below.

  64. ProcessORCH

    All paid build-time media goes into Git LFS so a fresh clone doesn't have to pay the vendors again

    Commit 616e9ab un-ignores artifacts/runtime/buildtime — music, sound effects, voice lines, ambience, portraits, scene stills, UI art, and the build manifest itself — and routes its binaries through Git LFS via .gitattributes; runtime instances (artifacts/runtime/<instance>) and other artifacts stay gitignored as scratch. Without this, every clone of the repo had none of the day's generated audio and art and would have to regenerate it, paying ElevenLabs, fal, and the image vendors a second time for assets that were already picked and locked.

  65. MilestoneL-RULES, Codex

    RULES-008: the phone sheet and TV build card finally show the real seeded character

    RULES-007 already built complete seeded characters, but the shared domain projection only carried summary data, so the phone sheet showed "Unknown data" for ability scores and the class-to-attack mapping covered only some classes. Commit 54c8750 adds domain.BuildStats (six abilities, save and skill proficiencies, HP, max HP, AC, attack name/dice/damage type/bonus) and threads it from the rules build through internal/game/view.go and internal/api/project.go into the phone sheet for all 12 classes. Debug verification confirmed seeded ranger scores, saves, skills, HP, and AC render correctly. A prior attempt (rules8) blocked itself before committing because its permitted paths omitted internal/game/phase/view.go, the only function wiring the build into domain.View; this rerun (rules8b) got the additive wiring authorized and landed it. Coverage: API 84.9%, domain 90.0%, game 93.7%, phone 88.2%. Known gap: the protobuf CharacterBuild still lacks attack fields, so the phone derives the attack line from its own all-12-class mapping rather than from the wire projection — filed as a contract request for dfctl and the TV build card.

  66. ProcessL-AUDIO, Codex

    Adaptive music cues and the TV's Listen reconnect get their follow-up pass

    AUD-002 (commit cff94aa) closes out the audio follow-ups left after the afternoon's Listen-hub and player fixes: one-shot stingers were looping instead of playing once, cliffhanger and end music did not exist as cues, and a TV that dropped its Listen stream had no way back in. internal/game/cues.go and internal/game/audio_table.go now distinguish looping beds from one-shot stingers explicitly rather than inferring it from channel, internal/content/sound_cues.go adds the missing cliffhanger and end cues, and scripts/buildtime/music_job.go/music_process.go generate and register them through the same build-time pipeline as the rest of the score. web/dm/mount_wasm.go gives the TV a reconnect path so a dropped Listen subscription resubscribes instead of staying silent for the rest of the run, the same class of gap e4a0d98 found on the voice side earlier in the day.

  67. IssueORCH

    Phone taps were silent until a player found the Enable sound pill

    Phone tap, ready, dice, and seat sounds only played after a player hit the small "Enable sound" pill, so through most of a normal play-through they were silent: nothing else on the phone happened to trigger that pill. Commit 93438d9 has the tap listener itself unlock Web Audio and open the seat's Listen stream, since any tap is already a user gesture the browser will accept for that purpose; "Enable sound" still works as an explicit control but is no longer required. Verified headless: a bare tap now plays sfx_ready and sfx_phone_tick without touching the pill first.

  68. MilestoneORCH subagent

    The engine now owns movement, and the phone gets a top-down combat map

    COMBAT-MOVE (commit e9458d4) has the engine compute each PC turn's movement map instead of leaving reach and pathing to the client: the cells a normal move reaches (6 cells, spent per cell) and the cells only a Dash reaches (the unused action doubles movement, ruling R-D8), each with the engine's own path, avoiding unwalkable and occupied cells. Combat now starts on the content battlefield grid and its spawns, so the engine, the TV, and the phone finally share one grid rather than three approximations of it. move{cell} keeps the action free for an attack; dash{cell} spends it, plays a 150 ms-per-cell "dash" walk, and ends the turn once the walk finishes.

    SeatView.CombatMap carries the map per seat, and the API projects it into PhoneView.combat.mini_grid: named tokens with stand-in portraits, dash-reachable cells, per-cell paths, and pace. The phone opens the map from Move, fills the grid to its width (rotating and trimming empty border rows), colors gold for normal reach and a cooler tone for dash-only reach, rings the player's own portrait chip in gold, and previews a tapped cell's path ("Move · 4 cells" / "Dash · 9 cells (ends your turn)") before Commit. The non-active seat sees the same map read-only. Token.step_ms carries the same walk pace to the TV for ENG-032's rerun.

  69. IssueORCH

    The Listen hub was dropping the TV two seconds into every line, and music never started after the lobby

    A fake-mode play-through showed the opening play 2.1 of its 11.3 seconds and every later line play nothing at all. e4a0d98 found the cause: Frame() queued every voice frame onto the legacy Frames() channel for every subscriber, but the registered AudioService only ever reads Messages(), so that queue filled, hit the hub's 2-second lag limit 2.1 seconds into the first line, and the hub closed the TV's (and phones') Listen stream outright rather than dropping frames — and the TV never resubscribed. The legacy frame queue now applies only to Subscribe() listeners. Separately, b590f57 found why no music, ambience, or stinger ever played after the lobby: decodeEncoded released its decodeAudioData callbacks right after registering them, so every streamed track decoded once and then hit "call to released function," leaving a stuck reservation that blocked retries; callbacks now release only once decoding settles, and a new looping bed crossfades the previous one out over 1.2 seconds.

    Two more fixes rode along. 9cecf79 narrows when Listen audio is cancelled to lines actually interrupted (host Skip, phase change) rather than every line, so a line that reaches line_done always finishes playing its queued tail, and e4a0d98's same commit has Cancel send the DM an AudioCancel so an interrupted line fades rather than cuts. cf22cea fixed a debug-only but confusing symptom of the same area: DebugGoto walked a fixture through every phase before the target and returned all of their effects, so jumping straight to combat with dfctl goto combat started the opening's "Rain hammers the Drowned Lantern…" narration, the reveal line, and a third line together while the TV already showed combat, and repeated it on every goto. Only the target phase's own voice lines are kept now.

  70. ProcessORCH

    The UI sound cues were coded but never actually generated

    The cue table already emitted sfx_join, sfx_ready, sfx_host_start, phone taps, dice, roll reveal, and hero lock, but nothing had generated the audio files behind them, so the asset catalog had entries with nothing to play. Commit a8729d8 gives RunSFXBuild an Only filter (DF_SFX_ONLY in the live test) so the missing sounds can be generated without regenerating everything already built, brings the two cues under ElevenLabs' 0.5-second minimum duration up to 0.5 seconds, and targets −19 LUFS for the UI tick. The run generated 8 sounds at 2 takes each for $0.024 total.

  71. IssueL-SPLAT / L-WEB-SPLAT, Codex & ORCH

    The battle splat never showed on the TV, then kept fading, for five unrelated reasons

    SPLAT-023 (commit 325b269) did the initial wiring: one createBattleViewer instance mounted into the DM combat layer, with the engine's grid and spawn cells derived from the Wooded Path scan's walkable collider cells (web/splat/grid_math.go, internal/game/combat/navigation.go). It did not render. SPLAT-027 (commit 81ff970) tracked the failure to five separate causes rather than one: the server never served the scene's LOD chunks or voxel collider under artifacts/media/supersplat, so the assets 404'd; the init message passed the camera-profile JSON as the splat asset itself; the Go bridge read incoming JS event objects as the literal string "[object Object]", so the ready signal never parsed; every combat re-render disposed and reloaded the whole PlayCanvas runtime and re-applied the canvas's opacity:0 and fixed 1920x1080 attributes, which is why the canvas kept going blank on each snapshot; and a LOW_FPS warning was treated as fatal and hid the canvas outright. The fix serves /artifacts/media/supersplat/ (the stated HTTP exception for splat streaming) with Cache-Control: no-cache so stale cached modules stop masking fixes, fetches the profile from its own scenes/<id>.json, stringifies bridge events before parsing, claims one live stage across remounts with a 1.5-second release grace instead of tearing down and reloading, and now treats LOW_FPS as a warning: any snapshot with an FPS count is ready.

    A sixth cause showed up once the others were fixed and the canvas rendered but sat frozen: it was drawing at 2880×1080 internally in a stage scaled by the browser's 1.5 device pixel ratio, so it was rendering 2.25× the pixels it needed to. Capping the runtime to pixel ratio 1 for a fixed 1920x1080 canvas took the measured p5 frame rate from 12 to 40 fps. A seventh and final fix (e9c3663) found the mount effect could run before the incoming combat layer existed in the DOM, so it returned early and, now that the stage is not remounted on every snapshot, never got a second chance; bind now retries every 50 ms for up to 2 seconds. All of this was verified live in the Claude browser pane against the running :8446 room in combat, not by reading logs.

  72. ProcessL-OPS, L-ENG2, Codex & ORCH

    dfctl can now drive a running room in real time

    The developer asked for the ability to "access the runtime and control anything realtime" rather than only inspect it. BL-001 through BL-004 (commits 0552514, d438a48, b05293d, 2ba0339) add the CLI and RPC surface: dfctl goto <phase>, seat and timer verbs, snapshot dry-runs, a modelchain vendor fault-injection decorator for exercising failure paths without a live vendor, and a client-screenshot report verb. ENG-033 (commit 703b5ab) wires the engine side: DebugGoto synthesizes what a target phase needs (seated heroes with rules-built characters from seeded dice, NPC state, combat tokens on walkable spawn cells), cancels the old phase's scopes and timers, and runs the target phase's entry effects so the TV, phones, audio, and battle stage react as they would in a real transition.

    None of it worked yet: with server.debug on, dfctl goto combat --turn pc1 against the live :8446 room still returned {"reason":"unaccepted_event"}, because ENG-033 added the engine's handling but internal/wire still constructed the room with plain game.New, which never registers debug events as acceptable. 7e95f42 switches the wiring to the debug-aware constructor when server.debug is on, and keeps debug on across a Reset without needing a fresh debug_start. dfctl goto <phase> against a running room now works end to end, closing out the developer's request and unblocking ENG-032 and lane testing that had been working around the missing control by restarting servers.

  73. MilestoneORCH

    The TV gets per-phase transition choreography instead of hard cuts

    Commit 85255b7 keeps the previous phase's layers mounted under an exit class for a short window, using a transitionTracker and the same layer keys so the DOM is reused rather than torn down, and plays a per-pair choreography keyed off a df-tx-<name> class on the screen: a lobby title lift with gold dust, an ink fade with the act title card and a lantern-glow iris into the opening, an NPC slide-in with background depth blur for conversations, a spotlight and result stamp for checks, rain and fog for the hook, a hook fade-out into the combat layers, and a lantern gutter-out into the end card. TransitionVeil and the df-tx-enter-* classes are built as the reusable primitive SPLAT-023 needed for its own cross-fade into the battle stage. Phones get a smaller version: an 8 px, 220 ms fade-and-rise, only on the frame where the screen kind actually changes. Reduced motion cuts every transition to a plain 240 ms fade. Preview pages expose window.dfPreview(name) so a specific transition can be swapped in and inspected without playing a whole scene to reach it.

  74. DecisionL-ENGINE, Codex

    Engine decides, renderer draws: one shared grid, and an 8-connected mismatch found along the way

    ENG-030 (commit 4a2d7f7) writes down the combat view contract the PlayCanvas renderer needs and has the engine own it: proto fields for a token's kind, path, anim, and anim_seq, highlighted cells, contact events, camera shake, and camera follow/duration, all computed once in internal/game/combat rather than re-derived on the client. Building it surfaced a mismatch that had been silently in the codebase: the engine's own pathing in internal/game/combat/navigation.go used 4-connected movement (no diagonals) while web/splat/grid_math.go, matching the plan's tactical grid, used 8-connected movement with diagonal cost 1, so a path the renderer would draw and a path the engine would actually charge a token for could disagree. ENG-030 aligns the engine side to 8-connected.

    ENG-031 (commit 6f625c3) carries the new contract the rest of the way: ENG-030 had added the fields to internal/game and the proto, but the shared internal/domain view and the API projection didn't carry them yet, so SPLAT-023's battle stage still only ever saw cell, HP, active, and statuses and could not animate a walk, an attack, a hit, a shake, or a camera move. internal/domain/view*.go, internal/api/project*.go, and web/dm/battle_stage*.go now map the full set through 1:1, with grid_math.go's path and distance functions kept in sync with the engine's.

  75. MilestoneL-RULES, Codex

    All 12 classes get complete seeded builds and an exported combat surface

    RULES-007 (commit 3603c22) rounds out the SRD 5.2.1 build model for every class the phone offers rather than the handful that had been carrying the demo so far: internal/game/rules/class.go reorganizes the twelve class templates, build.go extends the seeded-build path to fill them all in consistently (ability scores, saves, skills, HP, AC), and attack.go exports the attack resolution the combat engine calls into rather than duplicating it locally. A new rulings package formalizes SRD rulings as named, tested functions rather than comments next to the code that implements them. This is the data the phone sheet and the TV build card need to stop showing placeholder stats for any class other than the two or three exercised so far — that projection work is RULES-008, tracked separately below.

  76. IssueORCH

    RULES-008 is written but not landed: it shares a file with ENG-031's own edits

    RULES-007 builds complete seeded characters, but the shared domain projection still only carries summary build data, so the phone sheet shows "Unknown data" for ability scores and the class-to-attack mapping only covers some classes. RULES-008 is scoped to fix it: a new internal/domain/build_stats.go plus one added field on SeatView/BuildCard, filled from the rules build in internal/game/phase/creation and mapped in internal/api/project.go and the phone's sheet files. The todo calls out its own hazard: internal/domain's view files and internal/api/project.go are the same files ENG-031 is adding combat fields to at the same time, so RULES-008 is scoped to add without reordering and to touch only the sheet/build-card mapping functions, leaving the combat projection functions alone. As of this entry the todo remains open and uncommitted; the phone sheet keeps showing placeholder ability scores until it lands.

  77. Discoveryresearch agent, ORCH

    Seedance 2.0 video generation is too slow to run live during the demo

    Measuring the candidate providers for key-moment clips found none of them fast enough to generate on stage: fal's Fast text-to-video took 259 seconds for a 5-second, 720p clip, and its reference-to-video mode took 195-219 seconds for 480p, 4-second output, even though the reference-to-video results kept good character identity and isolated cleanly against a green screen. Segmind's Mini tier was the cheapest option measured, at $0.19 for a 5-second 720p clip, but carries the same order-of-minutes latency. Against a 3-minute demo slot, none of these can run live: any billboard or character-action clip that uses generated video has to be pre-generated before the run rather than produced on demand, with the existing portrait tween kept as the fallback path when a clip is not ready. This is a scope decision for the media pipeline, not a bug; MEDIA-003's Segmind Seedance adapter itself is unaffected and already ships fake-mode and build-time generation paths.

  78. ProcessORCH

    Running this many lanes at once keeps finding new ways to step on each other

    With eight-plus lanes and ORCH itself committing through the afternoon, a few process hazards recurred rather than being one-off mistakes: a commit staged by path name still occasionally swept in another lane's uncommitted edit to a file two todos happened to share, and HEAD was briefly left in a state where the full build did not compile between one lane's commit and the next lane rebasing its own understanding of the tree, resolved by re-reading the file immediately before editing rather than trusting an earlier read. The devwatch play-through hold (pausing new lane launches while a live browser walk-through is in progress) stayed necessary rather than becoming unnecessary as the stack matured, because a frequent web rebuild triggering live-reload on the TV mid-measurement was still enough to invalidate an in-progress FPS or audio timing measurement, which is why several of today's measurements (SPLAT-024's frame-pacing numbers, the AUD-002 reconnect check) were taken against a held server rather than the self-rebuilding one on :8446.

  79. IssueORCH

    What's still open going into the evening

    Four things stay open rather than fixed inline. The internal/sim walk simulations are failing again as the engine's combat and creation contracts shift underneath them faster than the fixtures are updated; they are not yet trusted as a regression signal. The i18n parity guard is failing on hard-coded strings introduced by today's UI work (the combat map and transition copy among them), so I18N-011's lint is doing its job but the backlog it is catching has grown. The hook phase's stall and the missing Leave affordance found by E2E-007 (see above) are still open; the underlying event trail has not been re-chased since the last fix landed short. And the scene restructure the developer is weighing, five scenes across four cuts with the lane fight staged on Wooded Path, is a decision still pending the developer's own call rather than something ORCH can settle unilaterally; nothing downstream of it is scheduled until it lands.

  80. ProcessL-WEB-SPLAT, Codex

    Scout battle cameras through browser controls and image review

    For the two scanned battle maps, Codex used computer use to operate a standalone renderer preview in the in-app browser. A small director page exposed candidate camera poses and encounter fixtures as buttons. Browser accessibility state confirmed which preset was selected; screenshots supplied the image modality with the actual rendered composition. The loop was concrete: select an angle, inspect the image for blocked fighters, overlapping silhouettes, readable cells and useful scenery, then refine position, target and field of view. Pose numbers alone could not show when a camera sat inside a tree canopy or when foreground foliage hid the encounter.

    The image review favored a diagonal approach toward the bridge in Wooded Path and a front approach beside Dittrich's mausoleum. Wider, lower openings establish the location; higher tactical views separate fighters and reveal their occupied cells. Closer action views retain the same approach. Stand-ins were placed on collider-filtered legal cells, and final screenshots were retained for comparison. Numerical regressions separately checked tactical pitch, finite camera vectors and preservation of every non-camera profile field. These are rendered screenshots, with no generated scenery or image retouching. The review establishes framing in the preview; it does not verify live game spawn mapping. SPLAT-026 records this process.

  81. DecisionL-WEB-SPLAT, Codex

    Frame the scanned locations as battle stages

    SPLAT-025 curates the seven battle camera presets for Wooded Path and Dittrich's Tomb. The forest opening follows the diagonal path toward the bridge, with the encounter in the middle distance. The tomb opening uses the front approach: the mausoleum anchors the left side and fighters occupy the visible clearing on the right. Side and rear candidates put the camera inside tree canopies, so the chosen approach keeps the battlefield readable. Lower establishing views provide scenery; tactical views rise to 35.99 and 35.63 degrees, within the plan's 35–40 degree band. Action views move closer along the same approach.

    Review used standalone 16:9 renderer fixtures with stand-ins placed on collider-filtered legal cells. Establishing, tactical and action screenshots are retained under artifacts/screenshots/L-WEB-SPLAT/*-025.png. Camera regressions check finite poses, FOV limits, tactical pitch, preserved SOURCE views and exact preservation of every non-camera profile field. Grid registration, character scale, LOD detail, grading and antialiasing retain their settings. This review covers camera composition; live game spawn mapping is a separate integration concern. The scoped lane gate passes, including architecture checks and the full native build.

  82. DiscoveryL-WEB-SPLAT, Codex

    Remove idle work before trading away scene quality

    SPLAT-024 profiled the two downloaded scans at 1920×1080, 4× MSAA and their existing LODs. Static tokens previously rewrote their yaw twice and rebuilt presentation state every frame. The controller now reacts to actual movement, camera changes and occupied-cell changes; follow smoothing and continuous movement notifications still run. A 32-token, 12,000-frame comparison eliminates 768,000 idle yaw writes and 12,000 redundant callbacks. Review caught two consequences of skipping idle work: following must sample the camera after tracking, and teleports/reduced-motion snaps must face sprites after placement. Both have regressions.

    The voxel proxy now emits the exact legacy vertex/index arrays with fewer temporary allocations and uses a color-free depth shader. Exact face deduplication was tried and rejected: these scans save less than 2% of proxy geometry while taking longer to build. Screenshots retain detail, grid coverage and occlusion; matched forest captures differ only by small rounding at a handful of pixels, and tomb captures match exactly. Browser frame pacing remains noisy and the tomb still reports LOW_FPS; this work claims reduced CPU work, not a reliable FPS improvement. Scoped gate and renderer regressions pass. Evidence is retained under artifacts/test/L-WEB-SPLAT/perf-024*.

  83. Decisiondeveloper (j-palacio), Claude Code

    Testing alternate names, and keeping DungeonFlux with a "couch quest" tagline

    A new team member brainstormed 20 alternatives to DungeonFlux across five angles: the AI running the game (Dungeon Mind, Loremaster), friends in one room (Couch Quest, Party Line), dice and fate (Natural Twenty, Critical Tale), the demo's own world (Lanternfall, Midnight Bell), and short brandable names (Questline, Tale Engine). Four finalists were scored on clarity, memorability, pitch fit, longevity, and switching cost. Gamemaster.ai was the clearest but read as a generic product category. Lanternfall was the most evocative but names one scene, not a game meant to grow into full campaigns. Couch Quest and DungeonFlux tied at 20/25: Couch Quest explains the phones-plus-TV format in two words, while DungeonFlux fits any campaign and is already everywhere (2,986 mentions across 497 files, the module path, the site, and the gilded lobby wordmark). A rename the night before the demo risks a stray old name on stage, so the decision is to keep DungeonFlux, use "a couch quest with an AI dungeon master" as the pitch tagline, and reserve Lanternfall as a possible title for the demo adventure itself.

  84. DecisionORCH, developer

    Re-timing the 3-minute demo, and wiring the splat renderer in as the combat scene

    With most screens playable end to end, ORCH re-timed the demo against what the walk-throughs actually take rather than the original guess: estimated on-screen activity now runs about 125-155 seconds against a 180-second slot, with character creation the biggest risk at 30-45 seconds for two players (species, gender, class, roll, lock). The working slot split is lobby 10 s, creation 40 s, opening 20 s, conversation 30 s, hook 15 s, combat 40 s, cliffhanger 20 s, and a 5 s buffer. Separately, the developer asked for the standalone PlayCanvas splat viewer (SPLAT-017 through SPLAT-022, web/splat/js/battle_viewer.mjs) to become the actual TV combat scene rather than a demo page of its own: SPLAT-023 is open and in progress, mounting one createBattleViewer instance into the DM combat layer, deriving the engine's grid and spawn cells from the Wooded Path scan's walkable collider cells, and cross-fading in from the hook over about a second with the existing flat battlefield as a fallback if the scene is not ready within 6 seconds. Serving the splat scene and its LOD chunks stays on the existing HTTP /splat route rather than gRPC, an explicit, flagged exception to the gRPC-only transport rule (the same rule INT-004/INT-006 otherwise apply to every other asset), on the reasoning that large streamed LOD chunks are a different shape of problem than images and audio; the exception is pending the developer's confirmation rather than settled. Scene-transition animation work between phases is in progress alongside it.

  85. MilestoneL-E2E

    A full game played lobby to End in the Codex browser, with a defect list to work from

    E2E-007 drove the dev server at :8446 from real browser tabs (a TV, a host, and two phones on different origins so they keep separate seats) rather than dfctl: join, ready, host Start, species/gender/class creation for both Lyra and Brom, the opening, Lyra's conversation with Mother Vell, a Persuade check, the stranger's arrival, combat against the drowned thrall on both phones, and through to the End card. This is the first time the whole stack, narration, hero creation, dialogue, a check, and combat, has been walked start to finish by an actual browser session rather than a scripted client. It surfaced a working defect list, some already known and some new: the hook transition still stalls on "Animated scene still" until the host presses Skip (tracked as ENG-029, see below); the client handling a phone's Persuade roll went stale and needed a reconnect before the result was visible; the host's Start/Resume controls needed repeated presses to actually begin the run; the second phone showed "Waiting for the active player" during its own turn in combat; combat and end-card portraits fell back to a plain "H" placeholder rather than the stand-in art; a character sheet showed "Unknown data" for ability scores instead of the rolled build; the TV's speaker caption was covered by the clip layer during the opening; the lobby stinger did not always play on the first "Enable table audio" tap; and the DM creation panel still shows placeholder stats rather than the seat's actual rolls. None of these blocked reaching the End card; all are recorded as open defects rather than fixed inline, since E2E-007 is a report-only todo.

  86. IssueORCH

    The hook glue fix landed, but a live run still stalls at the same spot

    a03d795 fixed one clear bug in the hook: the arrival clip's line_done was discarding the stranger's StartLine and re-feeding the event instead of using it to start the stranger's own line, which stalled the phase. The fix makes line_done start the stranger's line directly. E2E-007, run afterward against a server built from a commit that includes this fix, still found the TV stuck on "Animated scene still" after Leave until the host pressed Skip, so the glue fix was necessary but not sufficient. ENG-029 is now open to find the remaining root cause from the event trail rather than guessing again: it owns internal/game/phase/hook, hook routing in the top-level phase files, the voice-out executors, and hook-only dispatch in internal/wire/execs.go, and depends on ENG-027 (checks resolving without a host Skip) and NARR-001 landing first. The hook is the gate to combat and sits at the 1:40-2:00 mark of the demo script, so it stays on the critical path until it is closed.

  87. IssueORCH

    The TV played no narration audio, for three unrelated reasons found one at a time

    The DM screen going silent during the opening had a different cause every time it was chased down. First, in fake mode the TTS executor streamed 1.25 seconds of zeroed PCM for every line, so nothing audible was ever produced; d397236 switches lines whose role has a build-time recording (the OPS-022 through OPS-026 ElevenLabs assets) to play the canned recording instead, and 459af32 fixes the follow-on bug that the canned line still completed in 37 ms because the PCM went out as one chunk and the stream ended immediately, so the engine left the opening before the recording could be heard; lines now last as long as their recording. Second, and separately, the opening's LLM executor never reached TTS at all: the text executor streamed narration for read-along and posted its own line_done, but nothing then spoke it. a03d795 adds speakGenerated and cannedWhenEmpty (internal/wire/speak.go): the text executor's line_done is now held back and its finished text handed to the PCM executor, which speaks it and ends the line itself; a line that arrives with no text at all (the opening has no input) fails immediately so the phase falls through to its canned recording. Third, once real recordings were flowing, an 11-second recording sent as a single frame overran the Listen hub's bounded per-subscriber buffer (64 messages), which drops the subscriber outright rather than blocking; the DM's audio subscription was the one being dropped, so narration silently stopped reaching the TV mid-line. 9bf6622 paces recorded lines out in 100 ms/4 KB frames at playback speed instead of one burst (internal/wire/fake.go, internal/voice/out/canned.go). A fourth, non-code cause turned up in testing rather than code: a leftover headless DM tab from an earlier run kept its Listen subscription open, and because a fresh SubscribeTarget call always evicts the previous same-target listener (internal/api/listen.go, from the existing API-017 "newest DM Listen wins" behavior), a duplicate tab silently stole the stream from the tab actually being watched.

  88. IssueORCH

    Narration played choppy and cut off once real chunked audio was flowing

    With 9bf6622 now pacing recordings out as 100 ms chunks, the player introduced a new bug of its own: each chunk's scheduled offset is relative to the line's first chunk, but web/shell/audio/player_js_wasm.go was adding that offset to AudioContext.currentTime at the moment each chunk arrived, so 100 ms chunks ended up scheduled 200 ms apart, playing at half speed with gaps, and the next phase then cut the line off before it finished. 8d7c930 anchors each line to the AudioContext time of its first chunk in a lineBase map and schedules every later chunk as an offset from that fixed base instead of from its own arrival time; a chunk that arrives late (a network stall) shifts the line's base forward instead of overlapping what already played.

  89. IssueORCH

    A phone kept its creation pickers for up to two minutes after a successful roll

    After "Roll my hero" succeeded on the server, an already-open phone tab could keep showing disabled pickers with no build card, because gRPC-Go's default reconnect backoff climbs to 120 seconds after a WebSocket drop, so a phone whose stream had dropped just kept waiting instead of reconnecting to see the state a fresh Watch already had. ae5064c caps the backoff at 3 seconds (250 ms base, 1.6x multiplier, 3 s max) on the phone, TV and host gRPC dial options. The same class of bug had a client-side half already fixed: PHONE-031's frame keys are now built from the screen kind plus an atomic art-revision counter (snapshot version) rather than just the screen kind, so a same-phase state change that only updates data, not phase, still forces GoWebComponents to re-render the frame instead of reusing stale nodes.

  90. ProcessL-ENGINE

    A host TIMERS_OFF command actually disables the 30-second creation timeout during tests

    ENG-028 (commit 041de8c) wires the timer-disable policy the host console's TIMERS_OFF button and the features.turn_timers config flag were meant to control: creation's 30-second character-build timeout and other phase timers were previously unconditional, which cut a browser walk-through short whenever a tester took longer than 30 seconds to click through species, gender, and class. The policy lives in internal/game/timers.go and is read by both internal/game/phase/phase.go and the creation phase directly, with tests covering both the wired-through disable and the phase-level timer behavior. This landed as groundwork for ENG-027 (checks stuck in "rolling") and the still-open hook stall (ENG-029), both of which depend on timers behaving predictably during a played-through test.

  91. MilestoneORCH

    Stand-in hero portraits, phase-keyed layers, and a visible conversation NPC round out DM-032

    A run of small DM-032 fixes closed out the gaps a live play-through kept exposing on the TV. Creation, scene cards, HUD party cards, and the phone build card previously showed an empty "Waiting for hero" frame until image generation finished, which in fake mode is never; 3982b5e adds a fallback chain (rolled species art, then class crest, then a seeded-random species stable per hero) in a new heroPortrait helper in internal/api/project.go, swapped for the generated portrait once it resolves, and b465844 fixes the fallback to key off SceneCharacter.Class so it matches the hero actually rolled. 9d8d3e2 fixes the exploration and conversation backgrounds rendering solid black: the scene URL was layered underneath an opaque fallback gradient instead of behind it. 08b3a7a keys every DM layer by the current phase, because the GoWebComponents reconciler was reusing a previous screen's DOM nodes and their leftover inline styles (border, width, bottom) across a live phase change, which is why exploration briefly rendered with the opening's title squeezed into a bordered column; preview fixtures never showed this because previews always render fresh. 8d9097c gives the live conversation screen an actual NPC portrait, feathered into the dialogue panel, and hides the Act I title card that had been sitting on top of the dialogue. ec651d1 reshapes the hero cards into horizontal party rows (portrait tile, name, class) and 8a12777 hides idle turn timers and swaps the combat banner's raw pc_turn state name for plain words.

  92. IssueORCH

    Art fetched before the manifest arrived was cached under a key it could never be found by again

    eecd424 (WEB-015) fixes AssetLoader.ArtURL falling back to the raw name selector: an asset requested before the build-time manifest had resolved got cached under that raw name, and once the manifest later mapped the same name to a content SHA, every later lookup by SHA missed the cache entirely, so the phone's conversation portrait never appeared even though the bytes had already been fetched once. The same commit also fixes the phone calling the conversation NPC "Marra, Barkeep" when the TV and the game's own content call her Mother Vell, and commits the dev server's ?live=1 auto-reload addition to index.html. 8de9611 (WEB-022) adds retry to the same lazy art path so a phone that misses once does not miss forever, and 89bac72 gives the live conversation screen a fallback to the preloaded Vell backdrop when the specific portrait is not yet cached, and gives the DM's NPC figure her actual face instead of a placeholder.

  93. MilestoneORCH

    Read-along narration text reaches every screen, and on-screen actions get sound effects

    NARR-001 (commit d7c76eb) closes a gap the developer specifically asked about: nothing carried spoken-line text to any client, because domain.View.Scene.Narration was never written and PhoneView had no narration field at all. The engine now stores a line's speaker and text-so-far on the view as it streams (LLM deltas for generated lines, the full localized text at once for canned and input lines) and clears it on the next line or phase change; both DMView and the newly added PhoneView.narration carry it. PHONE-030 (commit 9313a78) surfaces this on the phone as a read-along bubble pinned above the tab bar, outside the keyed screen frame so it survives a phase change, showing speaker and text-so-far with a live pulse while a line is speaking. SFX-001 (commit e6dcd55) adds the cue table and generation pipeline for short sound effects on player-facing actions (join, ready, taps, rolls, locks) over the existing gRPC audio channels and the phone's local tap-sound path, reusing existing build-time sfx where they fit and generating the rest through the same ElevenLabs pipeline OPS-023 used.

  94. ProcessORCH

    The dev server's live-reload build could hand a browser a half-written WASM bundle

    The web build deleted the currently served dungeonflux.wasm before rebuilding it in place, so a page load or a live-reload tick during the roughly 40-second build window could pick up a missing or partial bundle. 2d18dc9 has scripts/buildweb.ps1 build into a side file and then use File.Replace to swap it into place atomically, so a request always sees either the old bundle or the finished new one. This underpins the self-rebuilding ORCH dev server on :8446 that lane workers have been pointed at for browser checks all afternoon: web changes trigger a WASM rebuild that live clients auto-reload (?live=1, from the WEB-015 fix above), while the server process itself restarts only when a committed server-side change lands, so a worker's in-flight edits are never torn down mid-check.

  95. MilestoneORCH

    Runtime assets (QR codes, portraits, clips) join build-time art on the gRPC asset service

    The gRPC AssetService only ever served the build-time manifest; a probe against a live server found Get returning NotFound for the lobby QR's own SHA-256, because runtime-generated assets (join QR codes, generated portraits, reference sheets, clips) live under the data directory rather than the build-time asset root and nothing served them over gRPC, leaving the DM to fall back to the HTTP /assets/ route the transport rule forbids. INT-008 (commit 0ca0381) has Get check both roots, keyed by SHA-256 hex only so a request can't traverse the filesystem, verified against a live server with a small grpctunnel client; the lane's own hand-in notes the full internal/wire gate stayed red on unrelated, pre-existing concurrent-worker test flakiness rather than anything in this change. Separately, API-022 (commit 46fce5a) fixes the DM's party cards showing "Waiting for a player…" for a seat that had actually joined: the join name travelled through postPhoneJoin but was never stored on the engine seat or projected, so an empty name looked like an empty seat; the seat now keeps the joining player's name and the card shows it immediately, with the hero's own name taking over once a character is built. PHONE-031 (commit fbf6183) makes a phone reload keep its room instead of losing it when the router drops the ?room= query, by falling back to the boot copy already kept in sessionStorage, and switches move buttons to the plain choice-row style so labels no longer get squeezed into an oversized button-plate sprite.

  96. MilestoneL-WEB-SPLAT

    Battle scenes mount into host canvases with independent controls and a lifecycle API

    SPLAT-021 (commit 656b523) and SPLAT-022 add createBattleRuntime and createBattleViewer. The host supplies its canvas and CSS; the renderer retains that element and resizes its drawing buffer to the element. Control-panel visibility and camera input are independent toggles. The instance API exposes ordered scene snapshots, characters, follow cameras, effects, grades, LOD, pause, events and idempotent disposal. Superseded loads reject with AbortError and late assets are released. The existing v1 Go bridge remains an adapter over the same runtime.

    Two-canvas visual inspection caught PlayCanvas's default global application ownership: asynchronous entity creation was sending the first scene's entities to the second renderer. Explicit application ownership now covers entities and post effects. The embedding example exercises independent toggles, movement, resize, disposal and remount, and retains scene credits. Regressions preserve the standalone viewer's demo, FPS reporting and debug picking, and cover load races, paused pans, sequenced grades and fallback. Both scoped gates pass; no Go packages changed. Concurrent high-resolution scenes can trigger the existing low-FPS warning on slower devices. The API and two-canvas usage example are documented in web/splat/embed.html.

  97. IssueL-WEB-SPLAT

    Scan scale made normal-height characters look miniature

    SPLAT-018 through SPLAT-020 add terrain-following character movement, optional camera tracking, cyan player cells, crimson enemy cells and distinct player, enemy and NPC stand-in sprites. The first sprite preview exposed an existing registration problem: the imported scans still treated source units as metres. Commit b7bb57a applies an estimated 0.4 metres per source unit to both scans, their collider transforms and camera positions. Characters remain 1.8 metres tall and rules squares remain 1.524 metres, so characters now appear 2.5 times larger relative to the scenery. The estimate comes from visual architecture and path references, not a metric survey. Rescaling also revealed a demo destination on a disconnected terrain patch; the demo now selects a reachable destination. Both scenes were visually inspected and the scoped gate passed.

  98. MilestoneL-WEB-SPLAT

    Scanned battle scenes gain supported grids and game-triggered presentation effects

    The SuperSplat download workflow resolves each scene's LOD manifest and preserves its dependent chunks. SPLAT-012 through SPLAT-017 register the Wooded Path and Dittrich's Tomb scenes as battlefields, generate voxel occupancy, exclude unsupported or blocked cells and fit grid lines to supported terrain heights. The grid uses 5-foot squares, antialiasing, contrast outlines and a glow inspired by the concept art. Neutral gray skyboxes and bounded color grades cover the ten battlemap themes. Sequenced camera commands trigger eased pans, impact shakes and tilt-shift, with pause and reduced-motion handling. These are presentation features; authoritative movement remains in the game engine. Concept images supplied palettes and mood, while navigation came from the scene profiles and colliders. Individual TODO commits and their visual checks are recorded in the repository.

  99. IssueORCH

    A shared corner brand replaces three stacked wordmarks; conversation drops duplicate chrome

    DM-032 (commit 17729d7) adds CornerBrand, the lettering band of the ui/logo_wordmark art cropped away from its lantern crest and rendered through the same luminance mask as the lobby wordmark, shared by the scene, dialogue and HUD layers; it replaces three separately scaled copies of the full title plate that had been stacking on top of each other on every screen that showed the brand. The same commit cleans up the conversation screen: it now hides the scene caption, build cards, progress bar, brand and location title that duplicated what the dialogue layer already showed, and choice labels wrap instead of overflowing their buttons; the table-audio unlock control moves to top centre so it clears the location title. Compared against ui-tv-tavern-barkeep-dialogue-choices.jpg in headless Edge.

  100. MilestoneL-WEB-PHONE

    Two phones, a DM and a host played a full game to End; one stuck check left a tracked bug

    PHONE-031's hand-in reports a browser play-through with two phones, the DM screen and the host console: join, Ready, host Start, species/gender/class selection, roll hero, build cards and Lock/Ready, opening, conversation, a Persuade check, combat, attack, through to End. The run needed one host Skip because check resolution stuck in "rolling" instead of resolving on its own; that gap is tracked as ENG-027 rather than fixed inline, since it sits in backend behavior outside PHONE-031's owned files. The two commits landed for this todo, ff3c6c9 (refreshing phone screens on a same-phase snapshot) and 294204c (wiring seatViewFromState and keying the frame by art revision), are the client-side pieces that let the phones react to state that changes without a phase change; the stuck-check bug itself was left as a follow-up rather than patched around. Screenshots and the server trace are under artifacts/screenshots/L-WEB-PHONE/phone-end.png and artifacts/runtime/L-WEB-PHONE-RUN2/logs/server.jsonl.

  101. IssueORCH

    GoWebComponents skips a prop-less closure component when its props compare equal, so loaded art never appeared

    A run of DM and phone bugs shared one root cause: the GoWebComponents reconciler only re-renders a closure component when its props change, and several DM and phone screens were mounted as prop-less closures, or closures whose props stayed equal across a re-render, so art or state that finished loading after the first render never showed up. On the DM screen (WEB-018, commit cb95b7c), a client shared across route refreshes made the props compare equal between renders, so the screen kept skipping the render that would have read the newly-resolved Blob URLs for the room code, QR, title art, wordmark and seat portraits; the fix adds a per-render counter into the props so every route render is provably distinct, and the same counter pattern was added to the DM and phone preview fixtures (67b2a00). On the phone, screens are prop-less closures entirely, so the reconciler could not tell a lobby sheet from a moves screen after the Watch stream delivered a phase change (f0c7712) or after art finished loading mid-phase (294204c); both are fixed by keying the frame with html.WithKey on a string built from the screen kind plus an atomic art-revision counter that the shell bumps on art load.

  102. IssueORCH

    The lobby wordmark's black plate cannot be blended away because the scaled canvas is its own stacking context

    DM-032's lobby finish pass (commit 616375e) found that mix-blend-mode could not remove the black plate under the ui/logo_wordmark art, because the fixed-size design canvas is scaled with a CSS transform, which makes it its own stacking context; a blend mode on an element inside that canvas never sees the cover art sitting outside it. The fix renders the wordmark through a luminance mask of itself instead (mask-image, mask-mode: luminance), which needs no blend target. The same commit switches the lobby's bottom panels to ui/panel_frame as a 9-slice border-image frame layer, and moves the menu stack down to clear the crest. A second bug surfaced once that finish CSS was in place: a <style> element whose rules are rendered through html.Text is HTML-escaped, silently dropping the child combinators, quoted font names and content:"" pseudo-elements the finish CSS relied on; fb0d4cc writes the sheet into <head> as textContent once instead of rendering it as a child node. Finally, 4b6bf2c drops the left:-50% offsets an earlier lane (DM-030) had hard-coded into the scene, clip and creation screens to compensate for a canvas transform that DM-027 had briefly disabled; once the transform was restored, those offsets pushed the three screens half off the 1920x1080 canvas.

  103. MilestoneL-WEB-SHELL

    Lobby title stinger and music bed added to the DM screen; playback itself is unverified

    WEB-020 (commit 68e1308) wires the lobby view's entry effects to cue a one-shot title stinger and a looping music bed from the existing accepted build-time assets, streamed over the same gRPC audio channel INT-006 added rather than an HTTP fetch: OPENING_SWELL (a 12-second one-shot, per plan section 0.19) plays once when the DM taps "Enable table audio", or immediately if the page was already unlocked, and THEME_MAIN (a 96-second loop) fades in under it and crossfades out when the phase leaves the lobby; a reconnecting DM does not replay the stinger. Unit tests cover the cue-selection and no-replay rules, and the gate passed at 94.5%/74.3%/92.4%/90.5% coverage across internal/content, internal/game/phase, web/dm and web/shell/audio. The lane's own hand-in flags one gap: headless Edge loaded the authenticated DM page, but the fake lane server registered no DM audio client, so the run could not confirm actual AudioContext playback or server-side stream delivery from the browser session; that check is still open.

  104. IssueORCH

    The router drops the query string on /dm and /p; tokens and preview names now survive in sessionStorage

    The shared shell router normalizes the URL when it mounts /dm, /p and /host, which drops the original query string; the DM and host access tokens and room id passed as query parameters (WEB-011, commit 0bd94ad) were lost the moment the router rewrote the address, leaving the DM and host Watch and Listen streams unauthenticated. The fix copies token, t (host token), room and preview from the boot URL into sessionStorage before the router runs, so the DM and host mount hooks and the preview registries can still read them after the rewrite. PHONE-030 (commit 67b2a00) extends the same mechanism to preview fixture names on the phone and DM previews, and clears the stored fixture name on the next boot so a fixture chosen for one page load does not leak into the next.

  105. ProcessL-WEB-SHELL

    Browser asset cache persists across reloads with a 30-day TTL and a per-device size cap

    WEB-019 (commits 4c0a5ab, 0e356e1) gives the browser shell a persistent asset store (web/shell/assetcache.go) instead of only the in-memory cache the gRPC asset loader had before: entries carry a stored-at and last-used time, a 30-day TTL, and LRU eviction against a cap of 256 MB on phone and 1 GB on desktop, refreshed on an hourly touch interval so an asset in active use is not evicted mid-session. 0e356e1 then wires that store into the browser asset path that had been calling the in-memory cache directly. Coverage on web/shell landed at 83.9%. The lane's hand-in flags a gap of its own: a second same-profile page load did render from the persisted cache in a browser check, but the server logs expose no AssetService Get counter, so the hit was not measured reliably; that verification is left open.

  106. IssueORCH

    An idle lobby sent nothing: Watch never replayed the last snapshot, and the room only published on events

    WEB-018 (commit 07b41e7) found that a client connecting to a room's Watch stream before anything had happened received no snapshot at all, because the stream only forwarded views as new events produced them, and the room itself never published its starting view; a DM, host or phone joining an idle lobby before anyone acted just waited. The fix has Watch replay the room's latest known snapshot to each new subscriber immediately on connect, and has the room publish its starting view at creation instead of only after the first event. This is the same class of bug as the DM and phone reconciler fixes above: a client that mounts into a stream expecting a push has nothing to render until something changes it, unless the current state is handed to it up front.

  107. IssueORCH

    The lobby QR never loaded because the gRPC asset loader only knew build-time art

    The gRPC asset loader (WEB-015) only preloaded the build-time ui/* manifest at boot, so a runtime asset referenced only by its content hash — the lobby QR — never resolved: the DM screen showed a broken image with no console error, because the loader had nothing cached under that path. WEB-015's hand-in flagged this ("Browser smoke test rendered the fallback gradient instead of title art, with no console asset error") and left it as a known gap. The fix adds a lazy art source (web/shell/lazy_art.go) that normalizes a requested path to its sha256, fetches it from the AssetService on first use if not already cached, and triggers a re-render once the Blob URL resolves. Commit cc516d5 also fixed two related DM rendering bugs found in the same pass: the stage layer painted an opaque background over the scene art instead of staying transparent, and the lobby's ornate panels had collapsed to 38px tall because their content was absolutely positioned inside a container with no intrinsic height.

  108. IssueORCH

    The DM canvas never scaled: CSS cannot divide 100vw by a bare number

    DM-027's fixed 1920x1080 design canvas was meant to scale to fit any viewport with transform: scale(min(100vw / 1920, 100vh / 1080)), but CSS cannot divide a length (100vw) by a bare number the way that expression implies, so the declaration is invalid and the browser drops the whole transform — the canvas rendered at its native size, offset and mostly off-screen, hiding the dialogue screen DM-028 built on top of it (flagged in DM-028's hand-in as a contract request). The fix (commit 3fb8fd5) moves the computation into Go: a resize listener in web/dm/scale_wasm.go reads the viewport size, computes the fit ratio itself, and writes it to a CSS custom property --df-scale that the canvas's transform reads as a plain number. DM-031's hand-in reports a second, independent symptom of the same underlying bug: some renderers had started injecting their own local positioning workaround before the shared fix landed.

  109. MilestoneORCH

    Table and per-seat audio wired end to end over gRPC, pending one integration todo

    The audio path from server to client is now wired in pieces landed across the last two hours. INT-006 (commit 22e9fe0) extended the Listen stream's AudioMessage with a channel (voice, music, ambience, sfx), encoded chunks with backpressure that drops non-voice chunks before voice, and a target field so a clip can go to the DM, one seat, or all phones. WEB-016 (8fc2367) added the DM's Web Audio mixer: per-channel gain nodes, crossfade, loop, and ducking under voice. On the engine side, ENG-023 (c9999d0) maps combat and status events to per-seat cue names, ENG-024 (7116fb6) emits the table-wide music/ambience/stinger cues, ENG-025 (459a3ca) calls that on phase transitions, and ENG-026 (431ab86) is what actually calls ENG-023's per-seat mapping from the combat and phase code where attacks, damage, spells, downs, and heals happen. MEDIA-013 (3e25a8b) generates each locked character's own voice-effect pack (attack, hurt, spell, downed, victory, heal) through ElevenLabs. PHONE-022 (e740fac) built the phone's one-off effect player. What still does not run end to end: INT-007, open at the time of this entry, is the todo to register MEDIA-013's and MEDIA-011's executors in internal/wire/execs.go and mount PHONE-022's player into the running phone client — until it lands, the pieces exist but are not all connected in a live build.

  110. IssueORCH

    Finished features that no lane could wire in on its own became their own integration todos

    Several lanes finished the feature their todo asked for but could not wire it into the running app, because the wiring point was a file another lane already had open, or sat outside their todo's owned paths. MEDIA-013's hand-in reports its voice-pack executor was not registered because internal/wire/execs.go already carried another lane's edits; PHONE-022 built a phone audio player that nothing mounted into web/phone or web/shell; ENG-023 built the per-seat cue mapping but nothing called it from combat or phase code until ENG-026 landed five commits later. A similar pattern surfaced earlier in the build when ENG-021's full walk test failed because the class move ENG-019 added had no localized label key — a gap only CONT-010 (commit 76b633a) could see and fix, from outside ENG-019's own lane. ORCH's response was to stop asking single lanes to guess at cross-cutting wiring: INT-007 is a dedicated ORCH-owned integration todo with paths widened across internal/wire, web/phone/mount*.go, and web/shell/client*.go/compose*.go, queued to launch once MEDIA-012, PHONE-024, and ENG-023 land rather than assigned to any one worker's narrow lane.

  111. DecisionORCH

    Phone screens get the same frame-first treatment as the DM screen

    Following the DM screen's layout-first rebuild, ORCH applied the same concept-matching approach to the phone client (commit f7a5688): rather than one lane trying to restyle every phone screen at once, PHONE-024 builds a single shared frame and component language first — a wordmark header showing the current location, and a five-tab bottom bar (Character, Journal, Play, Map, Menu) with a raised center tile for the primary action — and every other phone todo (PHONE-025 conversation and exploration, PHONE-026 check offer and result, PHONE-027 character sheet, PHONE-028 creation/join/waiting/end, PHONE-029 combat) builds on top of that shared frame instead of each screen inventing its own chrome. Each of the five follow-on todos owns a disjoint set of files and is gated on PHONE-024 landing first, mirroring the DM-027-then-DM-028..031 split that avoided six lanes overwriting each other's mount hooks.

  112. DecisionORCH

    Six parallel DM concept lanes broke each other's builds; the fix was a layout-first rebuild

    The developer's assessment of the DM screen was blunt: it "looked nothing like the concepts." The cause was DM-020 through DM-025, six parallel concept-matching lanes that all shared web/dm/**, each building its own version of shared mount and composition hooks and stranding or breaking the others' as they landed. DM-026, an integration pass meant to reconcile all six into one working screen, was stopped mid-way rather than finished. ORCH's replacement (commit 21de411) is layout-first: ORCH measured each TV concept image into an exact pixel spec (component positions, sizes, typography, color values) written directly into the worker briefs; DM-027 then builds a single fixed 1920x1080 design canvas, scaled to fit any viewport, plus the shared ornate components (OrnatePanel, TitlePlate, GoldButton, PortraitCard, SpeakerCaption, ActionButton, LocationTitle) every other screen uses; DM-028 through DM-031 each then own exactly one screen's files (conversation, exploration HUD, opening/creation, check/combat/cliffhanger/end) and build against DM-027's finished components instead of against each other's in-flight work.

  113. ProcessORCH

    A character reference sheet at lock keeps a hero visually consistent across every generated asset

    ENG-022 (commits 5de6fea, 1f0eb1e) added an effect that fires when a character locks, requesting a reference sheet before creation finishes rather than blocking on it. MEDIA-011 (d3e9bd3) generates the sheet itself: a multi-angle turnaround (front, three-quarter, side, back) from species, gender, class, and flavor, in the concept art style, cropped per angle and stored as assets, with a template fallback on failure or timeout. MEDIA-012 (f97e835) then conditions the later visual pipelines — portraits, composed stills, clip first frames, and combat billboard loops — on those reference crops as image-to-image or image-to-video inputs, so the same hero looks the same character across every generated asset instead of each pipeline inventing its own likeness. The purpose is consistency across a demo that generates most of its art live. One gap remains open: the reference's asset ids do not yet reach the later effects that would consume them, because that wiring lives in INT-007, still open at the time of this entry.

  114. IssueL-OPS

    Folding tempo detection got music generation to 5 of 12 cues accepted

    OPS-026 changed the native beatcheck analyzer to fold a track's detected tempo by double or half before comparing it to the requested BPM, because ElevenLabs music generations were frequently coming back at exactly twice (or half) the tempo asked for, and beatcheck was rejecting them as off-tempo when they were musically usable. After the fold and a second live generation pass (commit 15cc1b9), 5 of the 12 planned music cues passed the check and were accepted; the other 7 remain rejected for off-tempo or insufficient onset detections. The per-request cost and accept/reject detail for this run is in the existing devlog entry on ElevenLabs audio costs (What the build-time audio cost) rather than repeated here. The remaining 7 cues are a known gap: the demo currently has full music coverage for only some of its states.

  115. DiscoveryORCH

    What the build-time audio cost: about $6.55 of ElevenLabs, mostly music retries

    The first live ElevenLabs pre-generation used estimates computed from list prices by each job (not the billed invoice). Canned voice lines and nudges (OPS-022): 13 TTS requests, 1,454 characters, about $0.07. Sound effects (OPS-023): 32 requests, 92 seconds of audio, about $0.18, 16 best takes kept. Ambience (OPS-024): 7 requests, 210 seconds, five loops kept; at the SFX rate of about $0.002 per second that is roughly $0.42. Music was the expensive part: the first run (OPS-025) spent about $1.62 on six music_v2_5 tracks that beatcheck then rejected at twice the requested tempo, plus four requests refused with HTTP 422 over seed range; after beatcheck learned to fold double and half tempo (OPS-026), 32 more music requests produced 1,702 seconds for $4.26 and 16 accepted takes across five cues, logged per request in artifacts/runtime/buildtime/music/costs.jsonl. Seven cues still fail the tempo check. Lesson: validate the checker on a few takes before generating a full catalogue, because each 108-second music request costs about $0.27.

  116. ProcessORCH

    Detached worker launches were invisible to the developer; every launch is now a tracked task

    Several Luna workers were started detached from the shell that launched them, so they never appeared in the developer's own background-tasks panel; the developer could see the repo changing without being able to see who was doing it or watch it run. Two of those detached workers finished their edits but then hung for one to two hours before their process actually exited, with nothing visible to say whether they were still working or stuck. Separately, a workspace scan that should have stayed inside the repo instead walked the whole disk with a recursive find / and ran for about thirty minutes before it was noticed and killed. None of these were caused by a single bad command; they were caused by launching work outside any tracking surface at all. The fix going forward: every Luna worker launch, and every queued launch waiting on a slot, runs as its own tracked background task, and a worker that is already running detached gets a watcher task attached to it rather than being left to report back only through its hand-in file.

  117. ProcessORCH, developer

    Building from a git-archive snapshot of HEAD traded correctness for speed the developer did not want

    ORCH had been building and gating from a git archive snapshot of HEAD taken at the start of a check, rather than the live working tree, on the reasoning that a frozen copy could not be disturbed by a worker committing mid-build. The developer asked ORCH to stop doing this and let workers finish their edits and build from the moving tree instead, even though that means a check sometimes has to wait for a lane to land before it goes green. Two concrete cases motivated the ask: MEDIA-010 briefly failed a build with a duplicate failureKind declaration because the snapshot caught one lane's file mid-edit against another lane's already-committed version of the same identifier, and INT-004 needed gen/ regenerated from a freshly compiled proto that a stale snapshot did not have. A snapshot's isolation is real, but it is isolation from the truth, not just from disturbance; a build that passes against a moment that no longer exists is not evidence the current tree builds. ORCH now waits on in-flight commits rather than snapshotting around them.

  118. ProcessORCH

    Live ElevenLabs pre-generation started under a manifest file lock; three of five jobs still running

    The developer approved live, paid ElevenLabs generation for the show's pre-recorded audio (OPS-022 through OPS-025). Because several of these jobs write to the same build-time asset manifest in parallel, OPS-022 first adds a file lock (scripts/buildtime/lock.go, lock_test.go, commit cc26113) so concurrent writers cannot clobber manifest.json against each other. The canned-lines and turn-timer-nudge job then ran live under that lock, normalising loudness and registering every file with a per-request cost line (commits 2e3446b and b9602cf); TODOS.md marks OPS-022 committed. The sound-effect library (OPS-023), the ambience loop job (OPS-024), and the 12-track music job (OPS-025) are still claimed by Luna workers and have not committed as of this entry. Separately, MEDIA-010 (commit 64c80a2) added the on-demand, game-time sound-generation pipeline that plays effects during a live session rather than pre-show, gated behind a pending domain.GenerateSound contract.

  119. ProcessORCH

    Concept art from 14 parallel Codex image jobs, then six DM workers matched TV screens to it 1:1

    OPS-021 ran 14 parallel Codex jobs, each given a concept image as visual reference via codex exec -i, to produce the show's title art, logo, lobby background, panels, buttons, move and class icons, species portraits, class crests, dice, scene stills, and the phone background, at no API cost. One job reported that its files had been written when they had not; TODOS.md's interim status line records "42 of 45 images, species_c regenerating" before the rerun completed. The finished images were converted to WebP (quality 82, max 1920px) and registered in the build manifest as 42 ui/* entries (commit 6b10116). With art in hand, six DM-lane workers each took one TV concept image and rebuilt the corresponding screen to match it layout-for-layout: title and lobby (DM-020), the shared scene/check/combat/end layers (DM-021, commit 3364669, which also adds the art-resolver contract the gRPC asset loader installs into), character creation (DM-022), opening narration (DM-023), the exploration HUD (DM-024), and the barkeep conversation screen (DM-025).

  120. DecisionORCH, developer

    Class becomes a third player choice across all 12 SRD 5.2.1 classes, replacing the R-D7 random draw

    PLAN-030 (commit 486431f) changes character creation so class is a player choice alongside species and gender, drawn from all 12 SRD 5.2.1 classes, with the old R-D7 random-draw table demoted to a timeout fallback only. RULES-006 (commit a1a3831) adds demo templates for every class; ENG-019 (commit 3b742a7) wires class into the creation legal-moves gate, requiring species, gender, and class before roll_hero is offered; CONT-009 (commit 6e24f12) adds the localized class labels and descriptions; PHONE-020 (commit b5dc1e8) adds the phone's class picker and projects the resulting lock state; DM-019 (commit b5a43a4) shows the class choices on the TV creation seats. The trade-off the developer accepted: twelve classes multiply the combat-move surface the engine must support, so templates are kept inside the existing paladin/rogue combat band rather than giving each class bespoke moves, to avoid reopening the combat design mid-build.

  121. DecisionORCH, developer

    gRPC is the only transport after page boot, including images and audio

    The developer's standing decision is that once the WASM shell has booted, every asset the browser needs — images, TTS voice, music, ambience, and one-off sound effects — travels over the same gRPC connection rather than plain HTTP fetches, so the show has one transport to reason about and one place to apply backpressure. INT-004 (commit 76a27bf) adds an AssetService that serves build-time art and future generated assets over gRPC, backing the DM art-resolver contract from DM-021. INT-006 extends the existing DM audio Listen stream, currently voice-only, to carry music, ambience, and SFX channels with per-channel mix commands (play, stop, crossfade at the next bar, loop, gain) and backpressure that drops non-voice chunks before ever dropping voice; WEB-016 is the DM-side Web Audio mixer that consumes that stream, and PHONE-022 is the matching one-off effect channel for phones. As of this entry, INT-004 has committed; INT-006, WEB-016, and PHONE-022 are queued to launch once gen/ has regenerated from INT-004's proto changes.

  122. MilestoneORCH

    A stack a human can actually click through: tester URLs, preview routes, and a 78% smaller WASM bundle

    Several small pieces landed together to make the build testable by a person rather than only by dfctl. BASE-019 (commit 630387c) prints and saves the tester URLs at server start-up instead of leaving them to be guessed. WEB-012 (commit 001abe3), DM-009 (commit 4f0dea7), and PHONE-010 (commit 42fbe60) add an offline ?preview= route that renders every DM and phone screen state without a live server or a joined room, so a screen can be checked in isolation. API-018 (commit 2ec98f5) allows same-origin browser WebSocket upgrades that had been rejected outright. BASE-022 (commit a2f1b10) builds the WASM bundle with -trimpath -ldflags="-s -w" and serves it precompressed: 26,976,759 bytes uncompressed to 5,831,095 bytes gzip, a 78.4% reduction, because the prior 27 MB bundle took 10-18 seconds to start on venue Wi-Fi. Finally, INT-003 (commit 23ff374) got fake mode to simulate a full phone-driven game through to End without a human at the controls, the first automated proof that the whole stack, not just the engine, runs start to finish.

  123. IssueORCH

    Every lane's own tests were green; playing the game in a browser found six integration gaps none of them saw

    A live play-through surfaced a run of integration failures that no single lane's test suite could have caught, because each gap sat exactly at a seam between two lanes' packages. Phone Join never reached the engine at all (API-019, commit 1f19041): the session handler accepted the call but had no field to carry a player name into domain.Join. The shared DMView proto had no seats or lobby fields, so the TV showed an empty lobby even after two phones had joined and the engine's view version had advanced (INT-001, commit b62a837). JoinRequest itself had no player-name field to carry in the first place (INT-005, commit 9414fcd, which needed a lane-local reflection bridge because the shared contract lagged the todo that depended on it). On the phone side, character creation sent only roll_hero and the wrong move to lock in a choice (PHONE-011, commit aa8bcfa, now sends species and gender before rolling and sends ready to lock), and the phone never read back the character the server had actually projected, so it stayed stuck on the rolling screen after a successful roll (PHONE-020, commit b5dc1e8). In fake mode, the simulated game stalled in the opening scene because the canned narration audio never posted a line_done event, so nothing ever advanced the phase (INT-003, commit 23ff374). The pattern repeats from earlier in the build (see the ENG-015 dispatcher-gap entry below): package-level green says nothing about whether packages are wired to each other, and only playing the actual game end to end, in a real browser, exposed any of these six.

  124. IssueORCH

    The browser app would not start; five stacked bugs, each found only by opening a real browser

    Bringing the WASM shell up in an actual browser, rather than trusting a green server-side gate, turned up a chain of five bugs, each hiding the next. First, index.html had no #app mount element for GoWebComponents to render into (WEB-010 backfilled earlier); once that was fixed, the WASM main function still returned immediately after calling Mount, tearing the whole runtime down before it could render anything (also tracked under WEB-010, commit b44fe09, which keeps the shell alive after mounting). With the app actually staying up, the host page's own main { max-width: 42rem } rule squashed every screen, DM, phone, and host alike, into a narrow 64px loader box left over from before any real content existed (WEB-011, commit 17144f3). Past that, the DM and host browser gRPC clients dialed ws:// with no transport credentials at all, so every call failed before it left the browser (WEB-011, commit 47499f9); fixing that exposed a fifth problem, that browser clients were dialing DNS-resolved hostnames gRPC-Web cannot resolve inside WASM, fixed by dialing passthrough:/// targets instead (WEB-011, commit 6ca5d93). That same commit fixed a sixth, smaller bug it found along the way: the phone's join-error mapping checked the lowercased error string for the substring "code", meaning any gRPC error — whose default string is always rpc error: code = X desc = ... — matched and printed "We couldn't find that room" regardless of the real cause; it now reads the status message alone. None of these six were visible from a passing server-side gate; each needed a real browser tab open.

  125. MilestoneORCH

    Four hours in: 158 lane launches, 313 commits, 195 todos done

    A checkpoint at the end of the first four hours of the build. artifacts/lanes/launches.log shows 158 Luna worker launches across every open lane. The tree carries 313 commits since a4d3f30, the point where lane work began after contracts and scaffolding landed. TODOS.md currently reads: 195 done, 49 committed (tests green, awaiting the coverage/review gate), 21 open, 9 claimed, 9 backlog, 6 blocked, and 1 superseded, out of 290 tracked todos. The blocked six are mostly narrow integration points that a worker correctly declined to touch outside its own package boundary (see the integration-hook entry below). Nothing in the blocked set is on the critical path to a playable game; the lobby-to-End walk already runs end to end (see the E2E-004 milestone below). The pace has held steady since hour 0: no lane has needed a restart from quota exhaustion, and no commit in this window required a history rewrite.

  126. IssueORCH

    WASM app panics at start-up: no #app mount in index.html

    Loading /dm, /p, or /host on the human test server panicked in the browser with GWC-RUNTIME-PANIC-STARTUP: RenderTo target #app not found. web/shell/static/index.html only ever had a <p id=status> element; nothing in WEB-007 or WEB-008 added the mount point GoWebComponents renders into, so none of the three screens ever rendered even though the WASM bundle built and loaded cleanly. The shell also still calls the deprecated GoRegisterRoute instead of the current router's Register. Backfilled as WEB-009 in commit abe1bd4; the fix itself (add the #app container with loading text inside it, switch to router.Register) is claimed by a Luna lane and in progress as of this entry, not yet landed. Until it lands, the placeholder page keeps serving fine and /healthz stays green, but no phone, DM, or host screen is actually visible in a browser. This is separate from the server-side game logic, which already runs a full game through dfctl without a browser at all.

  127. IssueORCH

    The human server crash-looped twice in one session, on two unrelated causes

    First: restarting against an existing SQLite data directory failed with UNIQUE constraint failed: runs.id, because wire always started every process with the run id "run-0". BASE-018 (commit 938b093) makes run IDs unique per start, derived from the next sequence in the runs table. Second, once that was fixed, the server crash-looped again on "DF_DEBUG_TOKEN is required when server.debug=true", because the scheduled task's environment never had that token and the supervisor had never captured the child process's stderr, so the cause was invisible until someone ran the binary by hand. REPO-016 (commit 279e4ea) has the supervisor generate a random token with crypto/rand when one is not set, write it to a gitignored file, pass it only in the child's environment, and route the child's stdout and stderr to a per-start log file with the last stderr line copied into status.json. Both fixes are supervisor- and wire-level, not game logic; both were only visible because the human test server actually restarts under real conditions, not fakes.

  128. DiscoveryORCH

    A modelchain test passed alone and failed under full-suite load

    The stream-fallback test in internal/modelchain was green in isolation but intermittently failed when the full test suite ran under CPU load, the kind of flake that is easy to dismiss as a fluke and hard to reproduce on demand. The test raced real wall-clock timers against goroutine scheduling to decide when a primary vendor call should be treated as hung and the fallback vendor engaged; under load the scheduler could delay the fallback goroutine past the window the test expected. LLM-014 (commit 848fdaa) rewrites the test on Go's testing/synctest so the fake clock advances deterministically and the test no longer depends on real elapsed time or scheduler timing at all. The lesson generalizes: any test asserting "this happens after roughly N milliseconds" against a live clock is a latent flake under load, and the fix is a virtual clock, not a longer timeout.

  129. MilestoneORCH

    A whole game runs, lobby to End, on the server through dfctl alone

    TestE2E_DfctlRunThroughLobby in internal/wire/e2e_test.go now drives two seats through creation, opening, conversation, checks, resolution, a hook event, and the cliffhanger, to End, issuing every move through dfctl against a live wired server with fakes for vendors, no browser and no WASM client involved. Landed as part of E2E-004 (commit ef33a57), on top of ENG-015 and the runtime/wire fixes below. This is the first point in the build where the phrase "the game plays start to finish" is backed by a passing test rather than by a plan. It exercises the phase dispatcher, the runtime effect executors, wire's adapter wiring, and the debug service all in one path, which is exactly the seam that had been invisible to every lane working in isolation (see the ENG-015 entry below).

  130. IssueORCH

    Debug service read the engine from a stale reference while the room loop wrote it

    Building E2E-004 surfaced that the debug service (used by dfctl and the host debug endpoints) read internal/game.State directly and concurrently with the runtime room's own event loop, and in some paths held a reference to the engine captured before a reset replaced it. Under the single-goroutine fakes used by earlier unit tests this never showed up, because nothing else was reading the engine at the same time; only a real end-to-end run that issues debug reads while the room loop is mid-step exposed the race. The fix, folded into E2E-004 (commit ef33a57), serializes debug access through the room's own goroutine instead of reaching into the engine from outside it, and re-reads the current engine reference on every request rather than caching it across a reset. This is the same class of bug as the runtime/reset problem below: state owned by one goroutine must not be read from another without going through that goroutine.

  131. IssueORCH

    Every phase package was green in isolation, and the engine never called any of them

    internal/game.State, the engine root that wire actually runs, never used internal/game/phase.Machine. Its Step accepted only host commands and the debug reset event; it rejected Act, Say, timer, line, STT, LLM, and asset events outright, and LegalMoves only ever knew about the lobby. Every phase package underneath it — creation, opening, conversation, check, resolution, hook, cliffhanger — had its own passing tests and its own coverage, and archtest was green on all of it, because nothing in those tests ever asked whether the root actually reached them. E2E-003 stalled in character creation for exactly this reason before the gap was diagnosed. ENG-015 (commit 4e07943) gives game.State a phase.Machine and routes every domain event to it, taking effects back out of Step, and moving LegalMoves/View to come from the active phase. The lesson for the rest of the build: unit and package-level coverage numbers say nothing about whether the pieces are wired together, and only an end-to-end test running through the real composition root catches an integration gap this large. Every remaining lane brief now calls out its own root-level wiring point explicitly rather than assuming another lane will notice it is missing.

  132. IssueORCH

    The runtime ran every effect through a no-op, and a reset could not swap the engine

    Two related gaps in internal/runtime.Room. First, room effects were still routed through a no-op runner left over from early scaffolding, so any effect returned by the phase dispatcher — render, TTS, asset generation, music cues — was accepted and silently discarded instead of reaching a real executor; fixed by RT-009 (commit efc65c8), which wires the effect executors the room actually calls. Second, once the debug service could trigger a host reset, the room had no way to replace its engine with a fresh one: the old game.State stayed live across what should have been a new run with a new seed. RT-010 (commit 4388912) adds runtime.WithNewGame so a reset builds a genuinely new engine; BASE-017 (commit b0675b8) is the wire-level fix that actually passes WithNewGame and WithRoomState into NewRoom, since BASE-016 had landed first and left the wiring point unfilled. Together these three commits are why a debug-triggered reset now starts a genuinely new run instead of resuming the old engine's state under a new run id.

  133. ProcessORCH

    Archtest was too strict in three places; each relaxation is its own recorded todo

    internal/archtest started from a narrow default import list per package, and three lanes hit rules that were stricter than the plan actually required. BASE-013 (commit 030d70b) lets web/shell compose the dm, phone, and host screens, which the shared-shell design in the plan requires but the original rule forbade. BASE-014 (commit a702cd0) lets phase packages import internal/game/nested, steer, content, and rules (checked as pure), and allows time.Second-style duration constants in otherwise-pure code, which phase packages needed for timers and could not fake around. BASE-015 (commit 25b960e) lets internal/wire and cmd/server, the composition roots, import internal/adapters and internal/fakes, which they must be able to do by definition but which the default list omitted. None of these were quiet edits: each is its own todo with its own why, done-when, and commit, on the same footing as any lane's work, because loosening an architecture rule is a decision that needs the same paper trail as any other.

  134. ProcessORCH

    Workers stopped as "blocked" on one-line wiring hooks; briefs now name them explicitly

    Several lanes correctly reported their own package as done, gated, and covered, then marked the todo blocked because finishing it required a one- or two-line change in a file another lane or ORCH owned — a call site registering a new executor, a composition root wiring a new port. Strictly respecting path ownership meant real progress sat blocked on trivial hooks until ORCH noticed and made the edit. The trade-off is between path ownership, which keeps parallel lanes from clobbering each other's files, and speed, which wants small connecting edits to happen immediately. ORCH's resolution: briefs now name the exact integration hook a todo will need up front (the call site, the registration point, the file it lives in) and mark it as ORCH's to close the same review cycle, rather than letting a worker discover it mid-task and stop. This does not relax path ownership; it just moves the one-line connecting edit onto the side that already owns the file, decided at brief time instead of at blocked time.

  135. DecisionORCH, developer

    Splat assets are .ply and .sog only; the Go SPZ converter is gone

    The developer decided the build supports only .ply and .sog splat formats, not .spz, because the standard conversion path (splat-transform) needs a WebGPU Dawn binary that does not exist for win32-arm64, the developer's own machine. OPS-017 (commit 0c0365f) had already built a native Go SPZ-to-PLY converter to work around that gap; once the format decision landed it was superseded and removed in OPS-018 (commit 0b2fb50), which deletes the converter package outright and switches build-time asset jobs to use Marble's own direct PLY exports instead. Marble exports PLY at both full resolution and a 100k-point decimation, but has no SOG export path, so any .sog assets the build wants must come from elsewhere or be produced by a step not yet built. Net effect: one platform constraint (no Dawn on ARM64 Windows) drove both a converter's creation and its removal inside the same four hours, and the removal is the correct outcome, not churn.

  136. ProcessORCH, developer

    The developer ran seven of their own Codex agents alongside ORCH's lanes

    ORCH wrote briefs for seven carved-out todos — PHONE-008, PHONE-009, DM-008, CON-010, STAGE-008, LLM-013, and SPLAT-008 — and the developer ran them as their own Codex agents in parallel with the Luna worker lanes ORCH was managing, rather than folding them into the standard lane queue. Commit 8860045 backfills all seven into TODOS.md once they landed. This is the same brief-and-gate discipline as any other todo: one todo, named paths, a hand-in, and a review before it counts as done; the only difference is who launched the agent and on whose account it ran. Having a second, developer-controlled channel of agents running the same recipe alongside ORCH's own lanes did not require any change to the todo format or the gate script, which is the point of keeping the recipe uniform regardless of who is dispatching it.

  137. DecisionORCH

    Localization todos added: server sends keys, one shared catalog, en and es

    PLAN-029 (commit ba6c18c) adds twelve localization todos, I18N-001 through I18N-012, covering both server and client. The shape: a single internal/i18n catalog package shared by server and WASM client (it must build for GOOS=js GOARCH=wasm and pass the purity check); the server never sends pre-localized prose, it sends message keys with arguments, and each seat carries its own negotiated locale from Join's explicit choice or its Accept-Language header; views, canned lines, legal-move labels, and the end card all resolve through catalog keys instead of inline strings. Initial coverage is English and Spanish, with plural rules for both. All twelve todos are currently open, scheduled for block 14–17, well after the playable-game milestones in this window; recorded now so the contract shape (locale on Seat, a Msg{Key, Args} form alongside plain strings in views) is decided before any lane starts writing user-visible text that would need to be retrofitted later.

  138. DiscoveryORCH

    Scheduled task for the human test server needs AtLogOn and battery settings

    Register-ScheduledTask with -AtStartup failed with access denied, because that trigger needs administrator rights this session does not have. With default settings the task then sat in the Queued state instead of running, because the laptop was on battery and Windows Task Scheduler will not start a task under battery power unless told otherwise. The fix, in commit aa892d3 (REPO-011), switches the trigger to -AtLogOn for the current user, which needs no elevation, and adds -AllowStartIfOnBatteries and -DontStopIfGoingOnBatteries so the server keeps running when the laptop unplugs mid-build. The placeholder server this task launches now answers /healthz on :8443, confirming the always-up human test server from AGENTS.md section 11 is reachable before any real game code lands. This closes the readiness gap between the plan's requirement for an always-on port and what a non-admin Windows session can actually schedule.

  139. IssueORCH

    go build ./... walked into artifacts/tmp

    Worker lanes set GOTMPDIR under artifacts/tmp, and Go's ./... package pattern descends into any directory that does not start with . or _. The full gate's go build ./... therefore tried to compile Go's own scratch output sitting under artifacts/, work that was never meant to be part of the module. The fix, REPO-015 in commit 4428334, gives artifacts/ its own go.mod, which removes it from the main module's package tree entirely rather than trying to pattern-exclude it. The same commit added go.sum entries for the js/wasm-only imports GoWebComponents pulls in, cbor and goldmark, which native builds never touch and so had never been resolved. This is the second go.mod-adjacent surprise of the morning, after the toolchain auto-download in e-20260926-build-start, and both are now folded into the standing module layout so later lanes do not rediscover them.

  140. IssueORCH

    Shared git index: one worker's commit swallowed another's files

    Every Luna worker lane runs against the same working tree and the same .git index, so a plain git commit stages and commits whatever any lane has staged at that instant, not just the files the committing lane touched. It happened twice within the first hour: OPS-003's commit 5fd9c9c ("add transparent image cut-out job") also carries SPLAT-002's files, and an ORCH TODOS.md sync commit 4d40280 also carries MEDIA-002's files. Neither loss was permanent, since the files did land in the tree, but the commit history no longer matched the "one todo, one commit" rule in AGENTS.md section 13, and attributing a change to the wrong lane makes review and revert harder. The fix, landed in commit f59c005, rewrites the commit recipe in AGENTS.md to use git commit --only -m "..." -- <paths>, which stages and commits exactly the named paths regardless of what else sits in the index, leaving other lanes' staged work untouched. History was not rewritten to correct the two mixed commits; TODOS.md instead records which todo each stray file actually belongs to.

  141. MilestoneORCH

    Build started: hour 0 with parallel Luna workers

    ORCH (Claude Opus 5.5) wrote go.mod first, commit 28b9afc (REPO-001), pinning go 1.26.7 with a toolchain go1.26.8 directive. That directive made the Go tool auto-download 1.26.8 on first use, which closed REPO-003, the planned toolchain upgrade, without any manual install step. With the module in place, ORCH launched GPT-5.6 Luna worker lanes through Codex, invoked as codex exec -m gpt-5.6-luna, one brief per todo or per short dependency chain, generated from TODOS.md by a launcher script. Within roughly 35 minutes, about 135 commits landed on main (measured with git log --oneline a4d3f30..HEAD | wc -l), covering the layer 0–2 contracts (vocab, domain, ports, proto, gen), the clock/logx/httpx/config/fakes/archtest foundations, the FSM core, rules and dice, content, the engine root, the nested machines and phase dispatcher, runtime, store, the API layer, adapters for every vendor, the splat JavaScript bridge, the spike work, and the build-time job scripts. Up to about 35 worker lanes ran at once, bounded only by file ownership, disk, and Codex quota per AGENTS.md section 10, not a fixed lane count.

  142. ProcessORCH

    TODOS.md is backfilled, grouped by system, and every todo says why

    Three rules were added to AGENTS.md so the work list stays complete and the tree stays clean. First, any feature, package, endpoint, screen, asset job, or behaviour that no todo covers gets one before or alongside the work (rule 18). Workers list the missing todos in a new "Backfill todos" field of the hand-in, and ORCH, the only writer of TODOS.md, adds them; nothing ships that the list does not describe. Second, TODOS.md is grouped by system, ordered from the simplest foundations to the most integrated systems, and each todo carries a one-sentence why:. Third, stale build output is cleaned (rule 19 and a new section 4a): workers delete their own superseded binaries, old WASM bundles, earlier coverage profiles, and artifacts/tmp/<LANE>/ before hand-in and report it under "Cleaned"; ORCH prunes artifacts/build/ (except the human test server's builds), old test and coverage output, and the Go cache when free disk drops below 20 GB. The reason given is that an old binary or bundle that still runs can hide a bug, and the disk fills during a 24-hour build.

  143. ProcessCritic, rounds 10 and 11

    Critic rounds 10 and 11: 7.2, then 7.8, and the fixes between

    Round 10 scored the plan 7.2. Gates again depended on later blocks, and the fixes moved work earlier: a read-only Inspect and the PlayCanned Opening stub by hour 5, dice and rulings in hours 1–5, and L-STORE in hours 5–8, so hours 11–14 hold only combat wiring, cues, and tuning. PrerenderSet had two executors; it was split so PrerenderText produces the text and RenderLines renders it. The build-time manifest.json had no path into the pure engine; wire now reads it into OneShot and records it in runs.config_hash. ORCH running the full gate on every commit was a bottleneck, so each commit now gets the lane gate, go build ./..., and archtest, while the full gate, WASM build, and walk tests run on the merged head every 30 minutes and at checkpoints. Each -seed now yields one fixed stage seed. Round 11 scored 7.8, and its fixes went in the same pass: the hour-5 canned asset reaches the engine through wire, the dfctl backlog wording was tightened, the gate cadence was copied into AGENTS.md, and the venue probe, which starts the show in Safe Mode on a bad network, now falls back to a live run with the backup video cued if Safe Mode has been cut.

  144. DiscoveryORCH, todo-draft agent

    One phase package would have missed the hour-14 line by about 3 hours

    An agent drafted the full build todo list with an estimate and dependencies for each todo, then computed the critical path. The earliest finish the dependencies allowed was 17.15 hours for the hour-14 hard line and 8.4 hours for the hour-5 gate. The cause was one Go package: internal/game/phase took about 11 agent-hours of edits across seven todos, and every one of them edited the same transition table, so they had to run one after another. Three options were weighed: split the package, let file-disjoint todos share it with weaker gate isolation, or assume the build runs 25–35% faster than estimated. The decision was to split it. game/phase is now a thin top table and dispatcher, with one subpackage per phase (creation, opening, conversation, check, resolution, hook, cliffhanger). Each exports its own sub-table and handlers and may not import a sibling, so todos on different phases run in parallel lanes. Walk-test scripts were split into subpackages under internal/sim/walk for the same reason. This was gap G1 of seventeen found in the draft (G1–G17), all folded into the plan in the same pass.

  145. MilestoneORCH, gallery agent

    Twenty new concept images and a gallery page

    Twenty concept images were added: ten cinematic scenes and ten top-down grid battle maps. All 31 concept images were then renamed by kind and content, with prefixes ui- (7 screen mockups), scene- (10), combat- (2), battlemap- (10), and establishing- (2), so a file name says what the image shows; for example, the title-screen concept is now ui-tv-title-screen-join-lobby.jpg. The site links and the page preview images were updated to the new names. A new gallery page shows every image, with filter chips by kind and a lightbox that steps through images with arrow keys or swipe and closes on Escape. As before, the images guide palette, type, framing, and mood only; screens and features come from the plan, not from the art.

  146. DecisionORCH, developer

    No WSL anywhere: race tests in GitHub Actions, beat checks in Go

    The build machine is a Windows ARM64 laptop, and the plan had used WSL for two jobs. The Go race detector does not support windows/arm64, so race tests were to run in a WSL distro; the aubio beat tracker has no Windows ARM64 build, so music BPM checks were to run in WSL too. The readiness pass showed neither was set up: aubio was not installed, and the machine's default WSL distro is NixOS, not the Ubuntu distro the plan used. Both uses were removed. Race tests now run only in a GitHub Actions job (race.yml) on ubuntu-latest on every push to main, and ORCH reads the result with gh run list; nothing runs them locally, and a race found there opens a fix todo for the owning lane. The beat tracker is replaced by beatcheck, a small native Go tool under scripts/buildtime that uses only the standard library, with ffmpeg decoding the audio. It is unit-tested against click tracks generated at known tempos, to within 0.5% from 80 to 170 BPM. librosa had already been rejected because it needs Python and numba on ARM64. The plan now states that nothing in the build uses WSL.

  147. DecisionORCH, critic

    dfctl: a debug CLI so agents can check the game without a browser

    dfctl is a Go command-line tool that Claude Code, the Codex workers, and the developer use to read and change a running server and its clients during development; the checkpoints from hour 5 onward use it to check gates. Reads return the phase and machine state, the exact screen a given phone or the DM screen receives, a seat's legal moves, the scope tree with live work, assets, the event log, logs, and spend per vendor. Writes never mutate state directly: each one enters as an engine event, so it passes through Step, lands in the event log, and replays deterministically. Critic round 10 found the transport unclear: GoGRPCBridge serves the main server's gRPC only through its WebSocket tunnel, which a native gRPC client outside the browser cannot dial. DebugService now runs alone on a second listener at 127.0.0.1:<port+1000> (9443 for the human test server), only with server.debug=true, only for loopback peers, and only with DF_DEBUG_TOKEN; the demo config never opens it. The demo verb set was cut to the reads plus send, act, say, a single forced d20, and reset. Everything else (mid-run goto, timers, snapshots, vendor faults, client commands, --dry-run) is a backlog taken up only after hour 17 if a lane is idle.

  148. DecisionORCH

    Goroutines with owners, and one slog record per vendor call

    The plan's rules on goroutines were spread across several sections, so they were gathered into one binding section. One loop goroutine per room serialises every event and is the only code that calls the engine's Step; the engine itself stays single-threaded and pure. Vendor calls, speech-to-text, TTS, and pre-renders each run in their own goroutine under the scope that started them and are cancelled with it. Every goroutine has an owner and a context, every channel is bounded with a stated full policy (block, drop the oldest, or drop the subscriber), and results come back to the room as events, never as direct state changes. Recovered panics become failure events. Pre-renders run in parallel through one semaphore per vendor, sized to its quota. Logging is log/slog only, with loggers narrowed at each scope so every record carries correlation fields (room, run, scope, utterance, asset), plus a trace_id that follows one voice turn from microphone chunk to first TTS byte. Every vendor call emits exactly one call record with first-token time, duration, bytes, and estimated cost; those records are the demo's latency and cost telemetry and feed the host panel and dfctl costs. Keys, raw audio, and full prompts are never logged.

  149. DecisionDeveloper, dependency agent

    SchemaFlux becomes the LLM client, at its provider layer only

    The developer decided to use SchemaFlux, the developer's own open-source Go library, as the client for the game's LLM calls (plan D14). A dependency agent read the v1.2.0 source before it was adopted. It is used only at its provider layer, inside one adapter behind the ports.LLM interface, for the OpenAI-dialect links: gpt-6-luna over the Responses API, Qwen on Cerebras, and a local llama.cpp server. Its typed builders were left out because they add prompt text, repair calls, and global state. Two gaps shaped the rest. v1.2.0 sends no reasoning effort for gpt-6 models, so Luna would silently run at medium without a per-link effort setting. SchemaFlux has no Gemini provider, and its Anthropic path neither streams nor enforces a schema, so Gemini stays on google.golang.org/genai and Haiku on anthropic-sdk-go. If SchemaFlux fails the hour-0 Luna checks, an openai-go adapter replaces it behind the same interface with no caller changes. The same pass added plan section 0.22, which pins every library, tool, API, key, and quota; a scratch module built the pinned Go set natively and for js/wasm.

  150. DiscoveryORCH, test agent

    Codex can generate build-time art with no API key

    A test agent checked whether Codex's built-in image tool works on this machine. It does: one prompt asking Codex to use $imagegen produced a 1536×1024 painterly dungeon scene in under a minute, billed to the ChatGPT login's quota rather than an OpenAI API key. Codex saves the image under its own generated-images folder first and then copies it to the path the prompt names. Two traps came up. PowerShell expands $imagegen inside double quotes, so the prompt is written to a file and piped in. And the global Codex config named a model this account does not offer, which made every codex exec fail until -m was passed explicitly. The result: the ops lane uses Codex for build-time opaque art (backgrounds, the battlefield still, NPC and fallback portraits), which takes that spend off the API budget. The running game still calls the Images API, and portraits that need a transparent background still go through it, because this path does not guarantee alpha. The recipe is in AGENTS.md section 10.

  151. DecisionORCH, developer

    Worker lanes run gpt-5.6-luna in Codex

    The plan called for GPT-6 Luna in Codex to run every worker lane. The same image test showed the ChatGPT-login Codex account does not offer gpt-6-luna; it lists gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, and gpt-5.5. The developer chose gpt-5.6-luna for the worker subagents. Every lane now launches with codex exec -m gpt-5.6-luna, always passing the model explicitly so a stale default in the global config cannot fail a lane at launch, and worker commits carry a Luna (Codex) co-author line. This is separate from the game itself: the running game still calls gpt-6-luna through the OpenAI API for its live dialogue and interpretation calls, which is a different product from the Codex CLI the lanes run in. Claude Opus 5.5 stays the orchestrator and reviewer.

  152. ProcessORCH

    Roles for the build: one orchestrator, many worker lanes

    Claude Opus 5.5 in Claude Code is the orchestrator and reviewer: it writes the shared contracts, briefs each lane, reviews and gates every hand-in, keeps the human test server up, and writes the devlog. GPT-6 Luna in Codex runs every worker lane and writes the first draft of all lane code and tests; a lane is never the last reviewer of its own work. Codex lanes run in parallel, bounded by file ownership, disk, and quota rather than a fixed cap, replacing an earlier seven-agent limit. One lesson carried over from earlier Codex use: an exhausted quota exits the process with code 0 and no hand-in report, which looks like success but is not. The fallback is a Claude Sonnet subagent given the same brief and the same ownership rules, so a quota outage does not stall a lane.

  153. DecisionORCH

    An always-up human test server on port 8443

    The human test server on port 8443 stays up throughout the build so the developer can test by hand at any time. It runs as a Windows scheduled task, not a process started from an agent's shell, because a shell-started process dies with that shell. It serves the last build of main that passed the full gate and never swaps in a broken one: if a new build or gate fails, it keeps running the last good binary and logs the failure instead. It restarts within 5 seconds if the process exits. Before any code exists, it serves a placeholder page showing the current build phase and the latest devlog entries, so the URL never returns a 404. Lanes never start, stop, or bind this port, and never write under its runtime data directory; only the orchestrator touches it.

  154. ProcessORCH

    Working from TODOS.md: atomic commits without clobbering

    All work in the build comes from TODOS.md, and each todo ends as exactly one atomic commit. A worker stages only its todo's own files, by name, never with git add -A, commit -a, or similar broad commands. The orchestrator is the single writer of TODOS.md: it marks a todo claimed when a lane starts it and done after review. This exists because many agents share one working tree that always holds other agents' uncommitted, in-progress changes. Sweeping git commands, stashing, checking out, resetting, or cleaning can silently wipe or absorb another agent's unfinished work, so they are forbidden outside a worker's own todo commit. If git's index is locked, a worker waits two seconds and retries, up to 30 times, and never deletes the lock; a stray lock usually just means another commit is mid-flight.

  155. DecisionORCH

    A 70% coverage floor, measured only on touched packages

    Every lane gate enforces a 70% statement-coverage floor, but only on the Go packages a todo actually touches, so the check stays fast instead of re-measuring the whole module every time. Tests must assert an outcome, a returned value, an emitted effect, a state change, or an error, not just execute lines to inflate the number. Some paths are excluded from the floor because they cannot be meaningfully unit-tested: generated protobuf code, composition roots such as cmd/server and internal/wire, test fakes, adapter files that only make a live vendor call, browser-only syscall/js glue, the one JavaScript file under web/splat, and build scripts. Each excluded path still has to be tested some other way: through API tests, end-to-end tests, httptest fixtures, or checkpoint runs, so exclusion from the floor is not exclusion from testing.

  156. DiscoveryCritic, round 9

    Critic round 9 found gates that depended on later work

    Critic round 9 scored the plan 7.1, up from 7.0 in round 8. It found several build gates that depended on work scheduled in later blocks: conversation dispatch and the combat turn timer were not due until hour 8, a combat view and a way to start combat directly were not due until hour 11, and a host Start button was expected at every gate before the host UI existed at all. It also found the battlefield and navigation data had no path into the pure engine, that pressing Reset dropped joined seats, the splat-ready flag, and the rehearsed seed, and that a thrall already at 0 HP could be reported as fled instead of down. The fixes for these are queued behind the plan's dependency research so that only one agent edits plan.md at a time, per the single-writer rule in AGENTS.md section 13.

  157. ProcessORCH

    Parallel subagents on separate files, serial on the plan

    When asked to run one subagent per todo, only the todos that do not touch plan.md ran at the same time: the devlog update, a build-todo draft, and an hour-0 readiness check. The todos that edit the plan ran one after another instead, because plan.md has a single writer (AGENTS.md section 13), and two agents editing it at once would silently overwrite each other's changes rather than merge. This is the same rule that governs TODOS.md and docs/devlog.html: a shared file gets one writer at a time, even when the workload would otherwise parallelize cleanly.

  158. ProcessORCH

    The devlog becomes a timeline that agents append to

    The first devlog was a single essay. It is now this timeline, with one entry per event and a fixed template, so any agent can add a note about a hard issue or a useful discovery without restructuring the page. AGENTS.md now tells every agent when to write an entry. During parallel work, lanes put their entries in their hand-in report and the orchestrator appends them, so two lanes never edit this file at once.

  159. IssueEditor agent

    A network error stopped an editor agent mid-task

    The agent applying the round-8 critic fixes died with a self-signed certificate error just before it ran its edit script. Because research and editing were split, nothing was half-written: the plan was untouched. Instead of starting a new agent from scratch, the orchestrator resumed the same agent with its context intact and told it to re-read the plan and skip anything already applied. Resuming saved the agent's full read of a 2,900-line plan.

  160. MilestoneORCH

    The project site goes live

    A site agent built this static site in docs/ from the README, the plan, and the concept art, and checked it at 1440, 1024, 768, and 375 px. The orchestrator committed it, enabled GitHub Pages from /docs, set the repository description, homepage, and topics, and confirmed the page and its assets return 200.

  161. ProcessRules agent

    AGENTS.md and an artifacts folder keep parallel agents tidy

    Before any code exists, the repository has a rulebook for the coding agents: lane ownership, a gate command that defines "done", a hand-in format, and a list of forbidden actions (no commits by lanes, no killing processes by name, no paid API calls in tests, no stray files). Every generated file goes under artifacts/, including the runtime database and generated media, so parallel dev servers each get their own copy. A .gitattributes forces LF line endings because this machine converts to CRLF by default.

  162. IssueORCH

    Concept images did not show in the README viewer

    The README linked the concept art as .webp files, and the viewer in use did not render them. The images were converted to JPG with ffmpeg, the links were updated, and one image was opened to confirm it decodes. The WebP originals stay alongside.

  163. DecisionDeveloper

    Concept art guides the look, not the design

    When the concept images arrived, the orchestrator started reading features into them: a class picker, an objective tracker, a Continue button. The developer corrected it: the images are art direction only. They set palette, type, framing, and mood, and nothing in the game's flow or rules derives from them. The rule is now written into the plan and into the orchestrator's notes.

  164. DiscoveryCritic, round 8

    Random stats and fixed combat odds could not both be true

    After combat was merged, one section rolled character stats at random while another used fixed templates, and the combat probability table depended on the fixed version. The fix kept both goals: each class's attack ability keeps its template value and Charisma is always 14, while the other scores and the skills are rolled. Attack bonuses stay fixed, so the odds table holds; only hit points and armour class vary a little.

  165. MilestoneCombat design agent

    Combat enters the demo, and it always ends on time

    The developer moved combat into the three-minute demo. The design agent found a fight that fits 40 seconds: a drowned thrall follows the courier in, turns run in a fixed order, and the enemy is a lighter zombie variant tuned so the fight usually ends in about two rounds. If it runs long, the tower bell tolls and the creature flees; a 30-second cap and a host Skip back that up. The agent computed the exact distribution of when the fight ends for every pair of starting classes, which led to a rule that the second character's class is drawn so every pair can finish the fight.

  166. DecisionCombat design agent

    The battlefield renders in the PlayCanvas engine, not the SuperSplat viewer

    The first idea was to embed the SuperSplat viewer in a frame. Research showed it has no documented message interface, no way to add objects to the scene, and no exposed camera, so a grid and character billboards drawn over it could never pass behind a table. Rendering everything in one PlayCanvas scene fixes that: the splat is drawn without writing depth, the billboards are opaque and do, so occlusion works in both directions. It is the one JavaScript exception in a Go-first project.

  167. DiscoveryResearch agent

    The same video model costs fourteen times less from another host

    A provider comparison found the video model the plan uses on a host charging about $0.19 for a five-second 720p clip, against $1.21 on the original host, with first- and last-frame support intact. The per-run cost of the demo fell from about $1.37 to about $0.35 before combat clips. The cheaper host is now the primary, with the original as a fallback if quality or speed disappoints in testing.

  168. DiscoveryResearch agent

    Five poses cover every status effect

    Generating a clip for every condition on every character would cost about $52 per character. The status-effect research showed that only a handful need a different body pose (prone, down, bound, cowering, staggered); everything else is a tint, aura, or particle effect the browser draws over a neutral clip. That brings the cost to about $2.50 per character, and it keeps effects out of the green-screen colour.

  169. IssueCritic, round 7

    A cancellation rule threw away the demo's own pre-made work

    The engine cancels everything a game phase started when that phase ends, so nothing stale can fire later. The critic noticed this also cancelled the work started early on purpose: the stranger's lines, the cliffhanger clip, and the dice outcome line. The fix was a scope tree: work that must outlive a phase belongs to the whole run, the dice outcome belongs to the check, and only phase work dies with the phase.

  170. ProcessDeveloper

    Stop queueing agents; run them together

    The orchestrator had been running one subagent at a time and queued new requests behind the running one. The developer asked for parallel runs instead. From then on, independent research agents ran four or five at once in the background while the conversation continued. Agents that write files were kept on separate files, and only one agent edits the plan at a time.

  171. DiscoveryCritic, round 4

    A rule meant to prevent stale events cancelled every NPC reply

    Each state change bumps a counter that cancels work from the previous state. A conversation turn was modelled as a transition from Conversation back to Conversation, so every player line bumped the counter and cancelled the reply it had just queued. Self-transitions are now internal and bump nothing, and per-line work is keyed by the line's own ID.

  172. DiscoveryCritic, round 4

    Audio already sent to the TV is state too

    The server could cancel a voice line, but audio already queued in the TV's browser kept playing, so a skipped line overlapped the next one. The server now sends an explicit cancel message down the audio stream, and the TV stops every buffer for that line and drops any late frames.

  173. IssueDeveloper

    "See if we can" meant research, not a prototype

    Asked to see whether browser microphone audio could stream to the Go backend through the gRPC bridge, the orchestrator started writing a spike: a proto, a server, and a WebAssembly client. The developer stopped it: this was a planning phase. The code was deleted unrun, and the question was answered by reading the bridge's source instead. It supports client and bidirectional streaming, so one gRPC stream per push-to-talk press works. The spike moved into the build plan as the first test of the first hours.

  174. ProcessCritic agents

    An adversarial critic scores the plan after every round

    A critic agent reads the whole plan, scores it on a fixed rubric (consistency, contracts, feasibility, latency, reliability, state machines, media, clarity), and is told to verify each claimed fix instead of trusting it. Scores so far: 3.5, 6.0, 6.2, 6.4, 7.0, 7.3, 6.6, 7.0, 7.1. The two drops came right after large additions, and each pointed at exactly where the new material did not fit. The loop runs until the plan scores at least 8. (Updated after round 9.)

  175. ProcessORCH

    How the research agents were briefed

    Each research request from the developer was one line. The orchestrator turned it into a brief that could stand alone: which plan sections to read, the exact deliverables, read-only access, a source for every fact, a label on every figure (vendor, independent benchmark, anecdote, or estimate), and a section ready to paste into the plan. About fourteen research agents covered model APIs, latency, language-model vendors, a low-latency host, a classification model, the cheapest video host, costs, gameplay phases, rules, status effects, camera shots, music, backend architecture, and combat.

  176. DecisionDeveloper

    State machines own every phase

    The game runs as explicit state machines in Go. The language models never move the game between phases; they only write content inside a state, and their tool calls become events the machine can reject. This single decision resolved several critic findings at once: legal moves come straight from the machine, the story's steering became an engine rule, and every run can be replayed from its event log.

  177. MilestoneDeveloper

    From an empty folder to a plan

    The project started as an empty folder and a plan file. The first decisions came from a few rounds of questions: a table of friends in one room, the free fifth-edition rules, a laptop on the TV as the dungeon master's screen, phones as character sheets, the AI writing the story and a funnel steering it, voice conversations with characters, and a three-minute live demo built in 24 hours.