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.
-
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 sentlast_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. -
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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. -
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
2412be2and939dc25synchronize 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. -
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.
-
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.
-
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-usdceiling (default $5), stores each clip by SHA-256 content hash in the build-time manifest, and has a-cache-onlymode 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. -
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,devand dot directories) and rebuilds only when that fingerprint changes. A failed build leaves the running child in place. The WASM bundle builds to a.nextfile in the runtime's own directory and is renamed into place. The file server prefers precompressed.gzand.brsiblings, 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. -
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.jsoninto 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 inartifacts/test/PR-REVIEW/. -
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.Waithad 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. -
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 directionis 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. -
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/watchcontroller 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. -
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
533e4b5gives 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 owndmEndCardCSSsheet 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. -
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 ingit logfollowed by several todos landing close together once a blocking signature change resolves. -
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), andR1-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 againstassets/concepton 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. -
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/conceptreference. 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.fontscame 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.starliterally 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. -
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
55a9758adds a validatedserver.public_urlconfig 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.c01c135backfills the todo intoTODOS.mdalongside it. -
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/conceptthe way the earlier live-demo review did (commit4787ff6), 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. -
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
:8446at SPLAT-029 (commit184e224) 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(commit8a25fd8) 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 loggedidle_hooktimer, on by default), and fixes the root cause blocking HookEvent's advance to Combat. A runtime trace confirmed Resolution → Exploration now startsidle_hookand 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.c9a160eis 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. -
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
4fcd2b4falls the card back to thethrall_stillbuild-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. -
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.127f280moves 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.184e224follows 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:8446with 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. -
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 withconfig/fake.jsonexited 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 pointeddfctlat port 19446 instead of the correct 9446.internal/wirenow generates a random token withcrypto/randwhenDF_DEBUG_TOKENis unset, writes it to<data-dir>/debug.token(0600, never logged), and logs only the path, matching the REPO-016 supervisor pattern; a setDF_DEBUG_TOKENis used unchanged and writes no file.WEB-024(c6cd079): GoWebComponents v6 wraps every route render indocument.startViewTransition, and Watch updates re-render faster than a transition completes, so/dmand/plogged 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. -
MilestoneORCH
BB-001: live green-screen billboard loops from fal, cached and budget-capped
Commit
b7b5555wires 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_loopson, 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).4787ff6lands 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 againstassets/concept. -
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. Commit572ee5fhas the TV'sdf-splatfetch the scene profile itself, replace its grid with the engine grid carried in the init message, and setkeep_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, anddfSplat.state()now exposes the runtime snapshot and last scene for debugging. Verified in the Claude browser pane on the live:8446room (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. -
ProcessORCH
The Wooded Path battle splat, all LODs and colliders, goes into Git LFS; Dittrich's Tomb stays local
Commit
c957bc2commits the Wooded Path combat scene (id64bb46d5, 107 MB) — a streamed SOG in splat-transform's LOD layout,lod-meta.jsonplus LOD chunk folders for levels 0-3 — together with the voxel colliders theweb/splat/scenesprofiles reference, so a fresh clone has the scene the engine and TV both depend on. Dittrich's Tomb (idcb2fddd6, 508 MB) stays local only, kept out of the repo for its size; collider experiments and screenshots stay gitignored as before. -
MilestoneL-E2E, Codex
E2E-008 restores the simulated walks and i18n guards to green
Commit
3ea84e9gets the simulator walks and i18n coverage passing again acrossinternal/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/i18n98.1%,walk/basic100%,walk/story100%,walk/full71.4%,web/dm90.4%,web/phone88.2%. -
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_BEDmeasured at 141.68 BPM against a requested 60 (beatcheck's double/half-time fold did not cover it),END_CARD_THEMEmeasured at 152.82 against 80, andSTING_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 ranbeatcheckagainst every track regardless of shape.AUD-002(commitcff94aa) 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 by616e9abbelow. -
ProcessORCH
All paid build-time media goes into Git LFS so a fresh clone doesn't have to pay the vendors again
Commit
616e9abun-ignoresartifacts/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. -
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
54c8750addsdomain.BuildStats(six abilities, save and skill proficiencies, HP, max HP, AC, attack name/dice/damage type/bonus) and threads it from the rules build throughinternal/game/view.goandinternal/api/project.gointo 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 omittedinternal/game/phase/view.go, the only function wiring the build intodomain.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 protobufCharacterBuildstill 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 fordfctland the TV build card. -
ProcessL-AUDIO, Codex
Adaptive music cues and the TV's Listen reconnect get their follow-up pass
AUD-002(commitcff94aa) 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.goandinternal/game/audio_table.gonow distinguish looping beds from one-shot stingers explicitly rather than inferring it from channel,internal/content/sound_cues.goadds the missing cliffhanger and end cues, andscripts/buildtime/music_job.go/music_process.gogenerate and register them through the same build-time pipeline as the rest of the score.web/dm/mount_wasm.gogives 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 gape4a0d98found on the voice side earlier in the day. -
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
93438d9has 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 playssfx_readyandsfx_phone_tickwithout touching the pill first. -
MilestoneORCH subagent
The engine now owns movement, and the phone gets a top-down combat map
COMBAT-MOVE(commite9458d4) 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, rulingR-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.CombatMapcarries the map per seat, and the API projects it intoPhoneView.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_mscarries the same walk pace to the TV forENG-032's rerun. -
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.
e4a0d98found the cause:Frame()queued every voice frame onto the legacyFrames()channel for every subscriber, but the registeredAudioServiceonly ever readsMessages(), 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 toSubscribe()listeners. Separately,b590f57found why no music, ambience, or stinger ever played after the lobby:decodeEncodedreleased itsdecodeAudioDatacallbacks 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.
9cecf79narrows when Listen audio is cancelled to lines actually interrupted (host Skip, phase change) rather than every line, so a line that reachesline_donealways finishes playing its queued tail, ande4a0d98's same commit has Cancel send the DM anAudioCancelso an interrupted line fades rather than cuts.cf22ceafixed a debug-only but confusing symptom of the same area:DebugGotowalked a fixture through every phase before the target and returned all of their effects, so jumping straight to combat withdfctl goto combatstarted 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. -
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. Commita8729d8givesRunSFXBuildanOnlyfilter (DF_SFX_ONLYin 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. -
IssueL-SPLAT / L-WEB-SPLAT, Codex & ORCH
The battle splat never showed on the TV, then kept fading, for five unrelated reasons
SPLAT-023(commit325b269) did the initial wiring: onecreateBattleViewerinstance 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(commit81ff970) tracked the failure to five separate causes rather than one: the server never served the scene's LOD chunks or voxel collider underartifacts/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'sopacity:0and fixed1920x1080attributes, which is why the canvas kept going blank on each snapshot; and aLOW_FPSwarning was treated as fatal and hid the canvas outright. The fix serves/artifacts/media/supersplat/(the stated HTTP exception for splat streaming) withCache-Control: no-cacheso stale cached modules stop masking fixes, fetches the profile from its ownscenes/<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 treatsLOW_FPSas 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;bindnow retries every 50 ms for up to 2 seconds. All of this was verified live in the Claude browser pane against the running:8446room in combat, not by reading logs. -
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-001throughBL-004(commits0552514,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(commit703b5ab) wires the engine side:DebugGotosynthesizes 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.debugon,dfctl goto combat --turn pc1against the live:8446room still returned{"reason":"unaccepted_event"}, becauseENG-033added the engine's handling butinternal/wirestill constructed the room with plaingame.New, which never registers debug events as acceptable.7e95f42switches the wiring to the debug-aware constructor whenserver.debugis on, and keeps debug on across aResetwithout needing a freshdebug_start.dfctl goto <phase>against a running room now works end to end, closing out the developer's request and unblockingENG-032and lane testing that had been working around the missing control by restarting servers. -
MilestoneORCH
The TV gets per-phase transition choreography instead of hard cuts
Commit
85255b7keeps the previous phase's layers mounted under an exit class for a short window, using atransitionTrackerand the same layer keys so the DOM is reused rather than torn down, and plays a per-pair choreography keyed off adf-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.TransitionVeiland thedf-tx-enter-*classes are built as the reusable primitiveSPLAT-023needed 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 exposewindow.dfPreview(name)so a specific transition can be swapped in and inspected without playing a whole scene to reach it. -
DecisionL-ENGINE, Codex
Engine decides, renderer draws: one shared grid, and an 8-connected mismatch found along the way
ENG-030(commit4a2d7f7) writes down the combat view contract the PlayCanvas renderer needs and has the engine own it: proto fields for a token'skind,path,anim, andanim_seq, highlighted cells, contact events, camera shake, and camera follow/duration, all computed once ininternal/game/combatrather than re-derived on the client. Building it surfaced a mismatch that had been silently in the codebase: the engine's own pathing ininternal/game/combat/navigation.goused 4-connected movement (no diagonals) whileweb/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-030aligns the engine side to 8-connected.ENG-031(commit6f625c3) carries the new contract the rest of the way:ENG-030had added the fields tointernal/gameand the proto, but the sharedinternal/domainview and the API projection didn't carry them yet, soSPLAT-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, andweb/dm/battle_stage*.gonow map the full set through 1:1, withgrid_math.go's path and distance functions kept in sync with the engine's. -
MilestoneL-RULES, Codex
All 12 classes get complete seeded builds and an exported combat surface
RULES-007(commit3603c22) 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.goreorganizes the twelve class templates,build.goextends the seeded-build path to fill them all in consistently (ability scores, saves, skills, HP, AC), andattack.goexports the attack resolution the combat engine calls into rather than duplicating it locally. A newrulingspackage 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 isRULES-008, tracked separately below. -
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. -
IssueORCH
What's still open going into the evening
Four things stay open rather than fixed inline. The
internal/simwalk 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), soI18N-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 byE2E-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. -
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-026records this process. -
DecisionL-WEB-SPLAT, Codex
Frame the scanned locations as battle stages
SPLAT-025curates 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. -
DiscoveryL-WEB-SPLAT, Codex
Remove idle work before trading away scene quality
SPLAT-024profiled 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 underartifacts/test/L-WEB-SPLAT/perf-024*. -
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.
-
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-023is open and in progress, mounting onecreateBattleViewerinstance 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/splatroute rather than gRPC, an explicit, flagged exception to the gRPC-only transport rule (the same ruleINT-004/INT-006otherwise 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. -
MilestoneL-E2E
A full game played lobby to End in the Codex browser, with a defect list to work from
E2E-007drove the dev server at:8446from real browser tabs (a TV, a host, and two phones on different origins so they keep separate seats) rather thandfctl: 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 asENG-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, sinceE2E-007is a report-only todo. -
IssueORCH
The hook glue fix landed, but a live run still stalls at the same spot
a03d795fixed one clear bug in the hook: the arrival clip'sline_donewas discarding the stranger'sStartLineand re-feeding the event instead of using it to start the stranger's own line, which stalled the phase. The fix makesline_donestart 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-029is now open to find the remaining root cause from the event trail rather than guessing again: it ownsinternal/game/phase/hook, hook routing in the top-level phase files, the voice-out executors, and hook-only dispatch ininternal/wire/execs.go, and depends onENG-027(checks resolving without a host Skip) andNARR-001landing 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. -
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;
d397236switches lines whose role has a build-time recording (the OPS-022 through OPS-026 ElevenLabs assets) to play the canned recording instead, and459af32fixes 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 ownline_done, but nothing then spoke it.a03d795addsspeakGeneratedandcannedWhenEmpty(internal/wire/speak.go): the text executor'sline_doneis 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.9bf6622paces 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 freshSubscribeTargetcall always evicts the previous same-target listener (internal/api/listen.go, from the existingAPI-017"newest DM Listen wins" behavior), a duplicate tab silently stole the stream from the tab actually being watched. -
IssueORCH
Narration played choppy and cut off once real chunked audio was flowing
With
9bf6622now 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, butweb/shell/audio/player_js_wasm.gowas adding that offset toAudioContext.currentTimeat 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.8d7c930anchors each line to theAudioContexttime of its first chunk in alineBasemap 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. -
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 freshWatchalready had.ae5064ccaps 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. -
ProcessL-ENGINE
A host TIMERS_OFF command actually disables the 30-second creation timeout during tests
ENG-028(commit041de8c) wires the timer-disable policy the host console's TIMERS_OFF button and thefeatures.turn_timersconfig 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 ininternal/game/timers.goand is read by bothinternal/game/phase/phase.goand the creation phase directly, with tests covering both the wired-through disable and the phase-level timer behavior. This landed as groundwork forENG-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. -
MilestoneORCH
Stand-in hero portraits, phase-keyed layers, and a visible conversation NPC round out DM-032
A run of small
DM-032fixes 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;3982b5eadds a fallback chain (rolled species art, then class crest, then a seeded-random species stable per hero) in a newheroPortraithelper ininternal/api/project.go, swapped for the generated portrait once it resolves, andb465844fixes the fallback to key offSceneCharacter.Classso it matches the hero actually rolled.9d8d3e2fixes the exploration and conversation backgrounds rendering solid black: the scene URL was layered underneath an opaque fallback gradient instead of behind it.08b3a7akeys 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.8d9097cgives 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.ec651d1reshapes the hero cards into horizontal party rows (portrait tile, name, class) and8a12777hides idle turn timers and swaps the combat banner's rawpc_turnstate name for plain words. -
IssueORCH
Art fetched before the manifest arrived was cached under a key it could never be found by again
eecd424(WEB-015) fixesAssetLoader.ArtURLfalling 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=1auto-reload addition toindex.html.8de9611(WEB-022) adds retry to the same lazy art path so a phone that misses once does not miss forever, and89bac72gives 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. -
MilestoneORCH
Read-along narration text reaches every screen, and on-screen actions get sound effects
NARR-001(commitd7c76eb) closes a gap the developer specifically asked about: nothing carried spoken-line text to any client, becausedomain.View.Scene.Narrationwas never written andPhoneViewhad 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; bothDMViewand the newly addedPhoneView.narrationcarry it.PHONE-030(commit9313a78) 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(commite6dcd55) 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. -
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.wasmbefore 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.2d18dc9hasscripts/buildweb.ps1build into a side file and then useFile.Replaceto 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:8446that 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. -
MilestoneORCH
Runtime assets (QR codes, portraits, clips) join build-time art on the gRPC asset service
The gRPC
AssetServiceonly ever served the build-time manifest; a probe against a live server foundGetreturningNotFoundfor 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(commit0ca0381) hasGetcheck 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 fullinternal/wiregate stayed red on unrelated, pre-existing concurrent-worker test flakiness rather than anything in this change. Separately,API-022(commit46fce5a) fixes the DM's party cards showing "Waiting for a player…" for a seat that had actually joined: the join name travelled throughpostPhoneJoinbut 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(commitfbf6183) 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 insessionStorage, and switches move buttons to the plain choice-row style so labels no longer get squeezed into an oversized button-plate sprite. -
MilestoneL-WEB-SPLAT
Battle scenes mount into host canvases with independent controls and a lifecycle API
SPLAT-021 (commit
656b523) and SPLAT-022 addcreateBattleRuntimeandcreateBattleViewer. 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 withAbortErrorand 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. -
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
b7bb57aapplies 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. -
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.
-
IssueORCH
A shared corner brand replaces three stacked wordmarks; conversation drops duplicate chrome
DM-032 (commit
17729d7) addsCornerBrand, the lettering band of theui/logo_wordmarkart 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 againstui-tv-tavern-barkeep-dialogue-choices.jpgin headless Edge. -
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-027rather 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) and294204c(wiringseatViewFromStateand 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 underartifacts/screenshots/L-WEB-PHONE/phone-end.pngandartifacts/runtime/L-WEB-PHONE-RUN2/logs/server.jsonl. -
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 withhtml.WithKeyon a string built from the screen kind plus an atomic art-revision counter that the shell bumps on art load. -
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 thatmix-blend-modecould not remove the black plate under theui/logo_wordmarkart, 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 toui/panel_frameas 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 throughhtml.Textis HTML-escaped, silently dropping the child combinators, quoted font names andcontent:""pseudo-elements the finish CSS relied on;fb0d4ccwrites the sheet into<head>astextContentonce instead of rendering it as a child node. Finally,4b6bf2cdrops theleft:-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. -
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, andTHEME_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 acrossinternal/content,internal/game/phase,web/dmandweb/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 actualAudioContextplayback or server-side stream delivery from the browser session; that check is still open. -
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,/pand/host, which drops the original query string; the DM and host access tokens and room id passed as query parameters (WEB-011, commit0bd94ad) were lost the moment the router rewrote the address, leaving the DM and host Watch and Listen streams unauthenticated. The fix copiestoken,t(host token),roomandpreviewfrom the boot URL intosessionStoragebefore the router runs, so the DM and host mount hooks and the preview registries can still read them after the rewrite. PHONE-030 (commit67b2a00) 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. -
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.0e356e1then wires that store into the browser asset path that had been calling the in-memory cache directly. Coverage onweb/shelllanded 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. -
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. -
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. Commitcc516d5also 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. -
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 (commit3fb8fd5) moves the computation into Go: a resize listener inweb/dm/scale_wasm.goreads the viewport size, computes the fit ratio itself, and writes it to a CSS custom property--df-scalethat 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. -
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'sAudioMessagewith 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 ininternal/wire/execs.goand 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. -
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.goalready carried another lane's edits; PHONE-022 built a phone audio player that nothing mounted intoweb/phoneorweb/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 (commit76b633a) 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 acrossinternal/wire,web/phone/mount*.go, andweb/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. -
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. -
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 (commit21de411) 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. -
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. -
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. -
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.
-
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. -
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 archivesnapshot ofHEADtaken 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-010briefly failed a build with a duplicatefailureKinddeclaration because the snapshot caught one lane's file mid-edit against another lane's already-committed version of the same identifier, andINT-004neededgen/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. -
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-022throughOPS-025). Because several of these jobs write to the same build-time asset manifest in parallel,OPS-022first adds a file lock (scripts/buildtime/lock.go,lock_test.go, commitcc26113) so concurrent writers cannot clobbermanifest.jsonagainst 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 (commits2e3446bandb9602cf);TODOS.mdmarksOPS-022committed. 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(commit64c80a2) added the on-demand, game-time sound-generation pipeline that plays effects during a live session rather than pre-show, gated behind a pendingdomain.GenerateSoundcontract. -
ProcessORCH
Concept art from 14 parallel Codex image jobs, then six DM workers matched TV screens to it 1:1
OPS-021ran 14 parallel Codex jobs, each given a concept image as visual reference viacodex 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 42ui/*entries (commit6b10116). 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, commit3364669, 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). -
DecisionORCH, developer
Class becomes a third player choice across all 12 SRD 5.2.1 classes, replacing the R-D7 random draw
PLAN-030(commit486431f) changes character creation so class is a player choice alongside species and gender, drawn from all 12 SRD 5.2.1 classes, with the oldR-D7random-draw table demoted to a timeout fallback only.RULES-006(commita1a3831) adds demo templates for every class;ENG-019(commit3b742a7) wires class into the creation legal-moves gate, requiring species, gender, and class beforeroll_herois offered;CONT-009(commit6e24f12) adds the localized class labels and descriptions;PHONE-020(commitb5dc1e8) adds the phone's class picker and projects the resulting lock state;DM-019(commitb5a43a4) 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. -
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(commit76a27bf) adds anAssetServicethat serves build-time art and future generated assets over gRPC, backing the DM art-resolver contract fromDM-021.INT-006extends 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-016is the DM-side Web Audio mixer that consumes that stream, andPHONE-022is the matching one-off effect channel for phones. As of this entry,INT-004has committed;INT-006,WEB-016, andPHONE-022are queued to launch oncegen/has regenerated fromINT-004's proto changes. -
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(commit630387c) prints and saves the tester URLs at server start-up instead of leaving them to be guessed.WEB-012(commit001abe3),DM-009(commit4f0dea7), andPHONE-010(commit42fbe60) 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(commit2ec98f5) allows same-origin browser WebSocket upgrades that had been rejected outright.BASE-022(commita2f1b10) 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(commit23ff374) 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. -
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
Joinnever reached the engine at all (API-019, commit1f19041): the session handler accepted the call but had no field to carry a player name intodomain.Join. The sharedDMViewproto 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, commitb62a837).JoinRequestitself had no player-name field to carry in the first place (INT-005, commit9414fcd, 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 onlyroll_heroand the wrong move to lock in a choice (PHONE-011, commitaa8bcfa, now sends species and gender before rolling and sendsreadyto 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, commitb5dc1e8). In fake mode, the simulated game stalled in the opening scene because the canned narration audio never posted aline_doneevent, so nothing ever advanced the phase (INT-003, commit23ff374). The pattern repeats from earlier in the build (see theENG-015dispatcher-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. -
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.htmlhad no#appmount element for GoWebComponents to render into (WEB-010backfilled earlier); once that was fixed, the WASMmainfunction still returned immediately after callingMount, tearing the whole runtime down before it could render anything (also tracked underWEB-010, commitb44fe09, which keeps the shell alive after mounting). With the app actually staying up, the host page's ownmain { 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, commit17144f3). Past that, the DM and host browser gRPC clients dialedws://with no transport credentials at all, so every call failed before it left the browser (WEB-011, commit47499f9); fixing that exposed a fifth problem, that browser clients were dialing DNS-resolved hostnames gRPC-Web cannot resolve inside WASM, fixed by dialingpassthrough:///targets instead (WEB-011, commit6ca5d93). 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 alwaysrpc 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. -
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.logshows 158 Luna worker launches across every open lane. The tree carries 313 commits sincea4d3f30, the point where lane work began after contracts and scaffolding landed.TODOS.mdcurrently 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 theE2E-004milestone 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. -
IssueORCH
WASM app panics at start-up: no #app mount in index.html
Loading
/dm,/p, or/hoston the human test server panicked in the browser withGWC-RUNTIME-PANIC-STARTUP: RenderTo target #app not found.web/shell/static/index.htmlonly ever had a<p id=status>element; nothing inWEB-007orWEB-008added 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 deprecatedGoRegisterRouteinstead of the current router'sRegister. Backfilled asWEB-009in commitabe1bd4; the fix itself (add the#appcontainer with loading text inside it, switch torouter.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/healthzstays 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 throughdfctlwithout a browser at all. -
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(commit938b093) makes run IDs unique per start, derived from the next sequence in therunstable. 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(commit279e4ea) has the supervisor generate a random token withcrypto/randwhen 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 intostatus.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. -
DiscoveryORCH
A modelchain test passed alone and failed under full-suite load
The stream-fallback test in
internal/modelchainwas 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(commit848fdaa) rewrites the test on Go'stesting/synctestso 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. -
MilestoneORCH
A whole game runs, lobby to End, on the server through dfctl alone
TestE2E_DfctlRunThroughLobbyininternal/wire/e2e_test.gonow drives two seats through creation, opening, conversation, checks, resolution, a hook event, and the cliffhanger, toEnd, issuing every move throughdfctlagainst a live wired server with fakes for vendors, no browser and no WASM client involved. Landed as part ofE2E-004(commitef33a57), on top ofENG-015and 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 theENG-015entry below). -
IssueORCH
Debug service read the engine from a stale reference while the room loop wrote it
Building
E2E-004surfaced that the debug service (used bydfctland the host debug endpoints) readinternal/game.Statedirectly 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 intoE2E-004(commitef33a57), 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. -
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 usedinternal/game/phase.Machine. ItsStepaccepted only host commands and the debug reset event; it rejected Act, Say, timer, line, STT, LLM, and asset events outright, andLegalMovesonly 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-003stalled in character creation for exactly this reason before the gap was diagnosed.ENG-015(commit4e07943) givesgame.Stateaphase.Machineand routes every domain event to it, taking effects back out ofStep, and movingLegalMoves/Viewto 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. -
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 byRT-009(commitefc65c8), 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 oldgame.Statestayed live across what should have been a new run with a new seed.RT-010(commit4388912) addsruntime.WithNewGameso a reset builds a genuinely new engine;BASE-017(commitb0675b8) is the wire-level fix that actually passesWithNewGameandWithRoomStateintoNewRoom, sinceBASE-016had 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. -
ProcessORCH
Archtest was too strict in three places; each relaxation is its own recorded todo
internal/archteststarted from a narrow default import list per package, and three lanes hit rules that were stricter than the plan actually required.BASE-013(commit030d70b) letsweb/shellcompose the dm, phone, and host screens, which the shared-shell design in the plan requires but the original rule forbade.BASE-014(commita702cd0) lets phase packages importinternal/game/nested,steer,content, andrules(checked as pure), and allowstime.Second-style duration constants in otherwise-pure code, which phase packages needed for timers and could not fake around.BASE-015(commit25b960e) letsinternal/wireandcmd/server, the composition roots, importinternal/adaptersandinternal/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. -
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.
-
DecisionORCH, developer
Splat assets are .ply and .sog only; the Go SPZ converter is gone
The developer decided the build supports only
.plyand.sogsplat 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(commit0c0365f) 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 inOPS-018(commit0b2fb50), 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.sogassets 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. -
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, andSPLAT-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. Commit8860045backfills all seven intoTODOS.mdonce 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. -
DecisionORCH
Localization todos added: server sends keys, one shared catalog, en and es
PLAN-029(commitba6c18c) adds twelve localization todos,I18N-001throughI18N-012, covering both server and client. The shape: a singleinternal/i18ncatalog package shared by server and WASM client (it must build forGOOS=js GOARCH=wasmand 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 itsAccept-Languageheader; 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 currentlyopen, scheduled for block 14–17, well after the playable-game milestones in this window; recorded now so the contract shape (locale onSeat, aMsg{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. -
DiscoveryORCH
Scheduled task for the human test server needs AtLogOn and battery settings
Register-ScheduledTaskwith-AtStartupfailed 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 commitaa892d3(REPO-011), switches the trigger to-AtLogOnfor the current user, which needs no elevation, and adds-AllowStartIfOnBatteriesand-DontStopIfGoingOnBatteriesso the server keeps running when the laptop unplugs mid-build. The placeholder server this task launches now answers/healthzon: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. -
IssueORCH
go build ./... walked into artifacts/tmp
Worker lanes set
GOTMPDIRunderartifacts/tmp, and Go's./...package pattern descends into any directory that does not start with.or_. The full gate'sgo build ./...therefore tried to compile Go's own scratch output sitting underartifacts/, work that was never meant to be part of the module. The fix, REPO-015 in commit4428334, givesartifacts/its owngo.mod, which removes it from the main module's package tree entirely rather than trying to pattern-exclude it. The same commit addedgo.sumentries for the js/wasm-only imports GoWebComponents pulls in,cborandgoldmark, 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 ine-20260926-build-start, and both are now folded into the standing module layout so later lanes do not rediscover them. -
MilestoneORCH
Build started: hour 0 with parallel Luna workers
ORCH (Claude Opus 5.5) wrote
go.modfirst, commit28b9afc(REPO-001), pinninggo 1.26.7with atoolchain go1.26.8directive. 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 ascodex 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 withgit 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. -
ProcessORCH
TODOS.md is backfilled, grouped by system, and every todo says why
Three rules were added to
AGENTS.mdso 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 ofTODOS.md, adds them; nothing ships that the list does not describe. Second,TODOS.mdis grouped by system, ordered from the simplest foundations to the most integrated systems, and each todo carries a one-sentencewhy:. 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, andartifacts/tmp/<LANE>/before hand-in and report it under "Cleaned"; ORCH prunesartifacts/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. -
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
Inspectand thePlayCannedOpening 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.PrerenderSethad two executors; it was split soPrerenderTextproduces the text andRenderLinesrenders it. The build-timemanifest.jsonhad no path into the pure engine;wirenow reads it intoOneShotand records it inruns.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-seednow 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 throughwire, thedfctlbacklog wording was tightened, the gate cadence was copied intoAGENTS.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. -
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/phasetook 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/phaseis 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 underinternal/sim/walkfor the same reason. This was gap G1 of seventeen found in the draft (G1–G17), all folded into the plan in the same pass. -
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), andestablishing-(2), so a file name says what the image shows; for example, the title-screen concept is nowui-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. -
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) onubuntu-lateston every push to main, and ORCH reads the result withgh run list; nothing runs them locally, and a race found there opens a fix todo for the owning lane. The beat tracker is replaced bybeatcheck, a small native Go tool underscripts/buildtimethat 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. -
DecisionORCH, critic
dfctl: a debug CLI so agents can check the game without a browser
dfctlis 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 throughStep, 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.DebugServicenow runs alone on a second listener at127.0.0.1:<port+1000>(9443 for the human test server), only withserver.debug=true, only for loopback peers, and only withDF_DEBUG_TOKEN; the demo config never opens it. The demo verb set was cut to the reads plussend,act,say, a single forced d20, andreset. Everything else (mid-rungoto, timers, snapshots, vendor faults, client commands,--dry-run) is a backlog taken up only after hour 17 if a lane is idle. -
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 islog/slogonly, with loggers narrowed at each scope so every record carries correlation fields (room, run, scope, utterance, asset), plus atrace_idthat follows one voice turn from microphone chunk to first TTS byte. Every vendor call emits exactly onecallrecord with first-token time, duration, bytes, and estimated cost; those records are the demo's latency and cost telemetry and feed the host panel anddfctl costs. Keys, raw audio, and full prompts are never logged. -
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.0source before it was adopted. It is used only at its provider layer, inside one adapter behind theports.LLMinterface, for the OpenAI-dialect links:gpt-6-lunaover 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.0sends no reasoning effort forgpt-6models, so Luna would silently run atmediumwithout a per-link effort setting. SchemaFlux has no Gemini provider, and its Anthropic path neither streams nor enforces a schema, so Gemini stays ongoogle.golang.org/genaiand Haiku onanthropic-sdk-go. If SchemaFlux fails the hour-0 Luna checks, anopenai-goadapter 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 forjs/wasm. -
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
$imagegenproduced 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$imagegeninside 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 everycodex execfail until-mwas 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. -
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 listsgpt-5.6-luna,gpt-5.6-sol,gpt-5.6-terra, andgpt-5.5. The developer chosegpt-5.6-lunafor the worker subagents. Every lane now launches withcodex 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 callsgpt-6-lunathrough 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. -
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.mdnow 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. -
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.
-
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. -
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.gitattributesforces LF line endings because this machine converts to CRLF by default. -
IssueORCH
Concept images did not show in the README viewer
The README linked the concept art as
.webpfiles, 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. -
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.)
-
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.
-
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.
-
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.