Process spine: generate assets per milestone, and two trogdor profiles (quality / speed) #35
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
cmoriarty/trog#35
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two changes with one motive: iteration on trog is currently slower than the bugs are, so every mistake costs a full drain to find. Filed together because they compound — per-milestone generation shortens the feedback loop, the speed profile shortens every step inside it.
Part 2 landed (2026-07-27):
trogdor-quality/trogdor-speed, the resident brain,trog profileto switch. Part 1 below was rewritten after it, because Part 2 changed what Part 1 should be.Part 1 — assets are a tool, not a phase
What happens now
One
assetsphase writes a spec for the ENTIRE game, submits it as one batch, anddraingenerates all of it before the first line of code exists. Observed on the first real run (#11): 27 assets, ~2 hours of GPU, all of it spent before the build walk started — and then the run was abandoned because the art was unusable (#34).Why it's wrong here
The shape (revised)
The original filing proposed per-milestone batching:
assets(M1) -> drain -> build(M1). That is the same spec-then-drain machinery with a smaller batch, and it would be built only to be removed. The essence of a playful production process is not "batch later", it is make the thing when you find out you need it.So: generation becomes a tool the brain and the code agent call, and the asset phase goes away. The milestone still declares what it expects to need — that is what keeps the walk reviewable rather than improvised — but declaring is planning, not generating.
The plumbing for this already landed (2026-07-28, part of #35 since it is the resident brain that makes it possible):
generate_asset(kind, prompt, …)returns a handle in ~0.5s;await_asset(job_id)collects it. Measured live: a 64px sprite is ~7s warm, ~25s cold, an animation ~82s — all of it time the caller can spend working.seats.brain_yields()is True, because there it would stop the brain the agent is talking to.What remains is the graph.
What the graph should become
assetsphase. The vertical slice generates what the slice needs, when it needs it, and is judged by playing it.Constraints that are not negotiable
These come from things already learned the hard way; a redesign that ignores them re-earns the lesson.
critics.py+ the ledger. The brain decides when to generate; the critic decides whether it passed. That is where commit+target keying, temp 0.2 (0.7 flip-flopped, live-caught), never-rejudge-unchanged, and the bounded regen cap live. A brain looping "generate, look, try again" in its own context has none of them.BUILD_FIX_RETRIES) — that cap is the only thing guaranteeing an unattended run ends. An on-demand generation tool is a new unbounded GPU spend and needs its own budget, or that guarantee is gone.drain.py's batch lanes retire;seats.pydoes not. Undertrogdor-speedimage and animate still share GPU3, so seat lifecycle,runnable(), and keep-warm stay load-bearing — the queue is what makes that contention invisible to the caller.Done when
trog test asset --mockstill covers the whole path.Related: #27 (hardware profiles), #34 (asset pipeline has no alpha), #11 (where this was found), #36 (switching profiles from the studio).
Implementation context (written for a session starting cold)
Everything below was learned live on trogdor while building #11. It is the stuff that isn't in the repo and isn't derivable from reading it.
State as of filing
mainis at5f8f351, deployed to the trog stack, 220 tests green, lint clean.trog-games/a-cozy-fishing-game-2607272154— useful as a fixture: real docs, a real 4-milestone plan, a real 27-asset spec, and 27 real (broken) assets.Deploying — the part that is easy to get wrong
The Portainer webhook does not pull images.
make redeployfires the webhook, so aftermake build pushit will redeploy the stack with the OLD images and everything will look mysteriously unchanged. Redeploy through the API instead:Pass
Envback explicitly. Omitting it clears the stack environment, which is wherePOSTGRES_PASSWORD,FORGEJO_TOKEN,FORGEJO_URL,REGISTRY,COMFY_IMAGEandWEBSEARCH_MCP_URLlive. Verifyenv kept: 6in the response.The stack deploys from Forgejo main, not from the working tree — commits must be pushed before a redeploy means anything. A redeploy restarts the orchestrator, which kills any in-flight drain or build (
assetq.fail_orphans()cleans up the rows on boot).Brain bracketing: the invariants Part 2 must not break
Making
resident: trueskip bracketing touches the one thing the orchestrator is careful about. The relevant code istrog_lib/seats.py:take_brain()— waits up to 300s forbrain_idle()(llama.cpp/slots), then stops the brain container, sets_brain_taken.restore_brain()— starts it; on failure it deliberately leaves_brain_takenset so a later release path retries. Clearing it there once left the brain down until the next boot (#26).release_idle(ttl, restore=True)andrelease()are the paths that bring the brain back;_post_drainand the worker's idle sweep both rely on them.reconcile()at boot removes orphan seats and starts the brain if nothing holds the GPUs.For a resident brain the cleanest shape is for
take_brain/restore_brainto become no-ops and_brain_takento stay false — but checkreconcile()too: withresident: trueit must NOT treat a deliberately-stopped brain as an orphan-recovery case, and it must not start a brain whose cards a seat is currently using if the profile ever overlaps them. A profile whosebrain.gpusintersect anyseats.*.gpuswhileresident: trueis a configuration error and should fail fast at load, not at render time —profiles.pyalready refuses unknown seat names, so validation belongs there.Testing without a rig
make test— 220 unit tests, no services needed.trog test asset --mock— the whole drain path on stub seats, zero GPUs. PointTROG_URL=http://trogdor:8200.SEAT_MODE=mock/CODESEAT_MODE=mock— the code agent seat writes a placeholder Phaser page instead of raising a container.tests/test_drain.py::_lane_harnessdrives_laneover a fake job with fake seats, which is how the progress and early-audio tests work. Reuse it.Inspect the artifacts, not the verdicts
The single most useful lesson from #11: I reported "4 pass / 10 fail" for some time without opening an image, and the defect (#34) was one header read away. Concretely:
The ledger carries the critic's full reasoning, which is often more informative than the verdict:
(Note:
trog_ledgerhas nocreated_atcolumn — order byid.)Traps already paid for
seats._wait_healthyaccepts any non-5xx. With an authenticated seat a 401 reads as "up", and the failure surfaces much later as something unrelated.codeseat._wait_healthyis a separate implementation for exactly this reason; anything else that gains auth needs the same treatment.Image.getcolors()returns(count, value), not(value, count). Getting it backwards makes a half-transparent sprite read as 0% transparent. Cost me a test cycle incritics.transparency.BG_JOB_TIMEOUT_SECS(was 3600, now 86400 in compose). The symptom isexecution_seconds=3600.01 Worker job cancelled status=interruptedin the aegra logs and a run that stops mid-phase for no visible reason.trog runnow resumes from the checkpoint./runs/streamcloses cleanly during a long node with nothing to emit.POST /drainandPOST /buildmark themselves running synchronously; otherwise a caller polling a second later reads the PREVIOUS run's terminal status. A build node skipping an hour of work because it saw a staledoneis a silent failure.nvidia-smi topo -mover ssh is the fastest way to confirm the NVLink/NUMA layout; don't infer it from GPU indices.Measuring whether the speed profile was worth it
Run the same brief through both profiles and compare two things, not one:
trog_asset_jobs.timingsnow carries{boot, generate, deliver, warm, kind}per job (the drain records them as ofd030fce), so the corpus is queryable rather than anecdotal.Baseline from the failed run, for comparison: 14 images, 14 sounds, 3 animations, one full regen pass, roughly 2 hours wall clock, and 4/14 images passing on the first pass — though that pass rate is not a fair baseline while #34 stands, since the critic was judging assets that were unusable for a reason it could not see.
Progress: Part 2 done, Part 1's tool half done, first graph step landed
Part 2 (2026-07-27) —
trogdor-quality/trogdor-speed,brain: {gpus, resident, cpuset},trog profileto switch. Verified on the rig: brain resident on GPU0+1 across a whole session, brain and a generator seat resident simultaneously for the first time, audio ∥ animate preserved, image↔animate serialising on GPU3 as designed.Three things the first live run corrected, all of them readouts rather than the mechanism:
Swapping brain for image stackprinted on every job under a resident brain — the one line that would have made the win invisible.The 5B animate lane was not broken, it was mis-sized. At the 14B graph's 512² the subject dissolves by frame 2 and clips degenerate into streak noise; at 704² with 30 steps the background stays clean and the subject holds across all 8 frames, reproduced across seeds, ~82s a clip. Identity is still softer than the 14B's — pix3lwalk was trained for exactly this and no such LoRA exists for the 5B. Bench sheets in
/oneoffs/bench/.Part 1, tool half (2026-07-28).
generate_assetreturns a handle in ~0.5s;await_assetcollects it. Verified against a live build session: the asset landed in the session's workspace, was 404 in the repo the whole time the session ran, and came out byte-identical in the session's own single commit besideindex.html.Its four constraints are implemented, not just written down:
run_jobhad never calledcritics, so everything the tool made was committed unjudged.critics.judge_freshjudges the bytes (the file isn't in the repo during a session), records nothing to the ledger forprejudge_audio's reason, and the verdict travels back throughawait_asset. This immediately caught #34 — first judged asset came back "no alpha channel", and every sprite that session measured RGB/0.0% transparent. Fixed in2c1a9d1; same prompt now passes.ASSET_BUDGET(24) per build session. A human at the CLI spends none./assets/findfirst, workspace before repo, existing path returned rather than regenerated..trog.jsonsidecar per asset (prompt, verdict, critique, timestamp) riding the same delivery, so one commit carries the asset and its account.Also: the OpenCode seat got an MCP block for the first time, handed over only where
seats.brain_yields()is False — still no credential in that container, because the asset lands in its workspace rather than being committed by the tool.Graph, first step.
slice_buildsits betweenvertical_sliceandgate_bar: the slice is now built before the bar gate judges it, with its art made on demand where the brain is resident. The brief adapts to the rig — generate-as-you-go on a resident brain, flat rectangles plus an## Assets neededlist where the brain yields (a tool that refuses every call produces worse code than an honest instruction to draw rectangles).Remaining
production -> assets -> drain -> build. Moving it onto per-item generation is the rest of this ticket.trog runyet —slice_buildis covered by graph tests and deployed, but nothing has played a slice it built.Related: #34 (fixed, found by closing the critic gap here), #36 (switching profiles from the studio).
Handoff: seven headless runs, ten fixes, no working game yet
Session of 2026-07-28. Everything below is pushed, CI green, deployed. Stack is idle on
trogdor-speed. Read the "what to do next" section first — the current blocker is small and specific.What I was doing
Running the full production graph headless (
trog run), monitoring, interrupting on bugs, fixing, redeploying, repeating. Seven runs. Each one got further than the last; none produced a playable game.Landed (oldest first)
0fc0414,4a6df1f.venv. Its key only changed whenrequirements-dev.txtdid, so a bad venv stayed bad forever. Proved the code innocent first (clean linux/py3.14 container: ruff clean, 259 passed), then bumped the key. Green since.2e99307BUILD_STALL_SECS, 600) aborts a session that goes silent instead of burning the fullBUILD_TIMEOUThour. Also: the fun gate never passed — 4/4 FAIL across two runs, every critique fair and none decisive, because the prompt said "judge harshly" and never said what PASS means. Now states the bar. Passes first try on every run since.fa311b2index.html. Planning now requires every milestone to end playable; both build briefs sayindex.htmlIS the game, never replace it.5fa81af+esmhas no default export;undefined.add). House rules now pin the exact script tag and name both failures. No bootstrap errors since.b50bc4egate_slice— three runs committed a slice that never drew a frame, becausegate_barjudges the spec document. The slice is now played before it is judged, and failures go back to their author with the console attached, bounded bySLICE_FIX_RETRIES.cbc69a7422was killing repair sessions with no reason given. The error now carries Forgejo's body — it found the next bug within one run.1df6e7c"a small orange fish, side view"→ PASS; against that +CRAFT_SPRITE_SUFFIX→ FAIL.build_specwas handing the craft-suffixed prompt to the critic as the brief, turning generation guidance into an acceptance test — and overriding the style-block craft opt-in. Spec now carriesbriefbesideprompt.188be3agenerate_assetnow says outright that it makes pixel art.a4b6705repository file already exists [path: game.js].commit_fileschecked existence on the branch, and Forgejo serves a stale contents read for a window after a fresh commit — the same windowread_binarydocuments. So a just-created file read as absent, we chosecreate, batch rejected. It threw away two repair sessions that had already fixed the game. Existence is now pinned tohead_commit.275 tests, all with regression cover for the above.
Where run 7 died — the current blocker
Commits succeeded (the 422 fix held), repairs ran and landed — but the slice build never produced
index.htmlat the repo root, so the playtest had nothing to open. It then burned all threegate_sliceattempts on the same report and advanced on its retry cap.This is much shallower than earlier failures and smells like the same class: the rule exists (AGENTS.md says the headless browser opens
index.htmlat the root) but nothing checks it, and the feedback is expensive and generic.What to do next
slice_build/build_phaseverifyindex.htmlexists at the repo root before the node returns, and re-prompt immediately if not.trog run "<brief>"again and watchgate_slice. That gate is the fast signal now — if the slice plays, the milestone walk is the next unknown.Practical notes for whoever picks this up
a one-screen arcade game where you catch falling fish in a bucket, score goes up, speed increases. Deliberately tight — one screen, few assets, obvious win/lose.trog runneedsTROG_URL=http://trogdor:8200 TROG_AEGRA_URL=http://trogdor:2026 TROG_FILES_URL=http://trogdor:3923.GET /orch/build(session events, the agent's own narration),GET /orch/health, thread state at/threads/{id}/state(phase, next, gates, fixes), andtrog playtest trog-games/<slug>— CPU-only, safe to run during a drain. Read the whole playtest output; the verdict line is at the top and atailwill hide it.trog_asset_items.verdict/verdict_note.trog-games/a-one-screen-arcade-*andcreate-a-cozy-fishing-*. Safe to delete; I don't do hard deletes.trog profileswitches rigs.trogdor-speed(resident 35B-A3B MoE, klein-4b art) is what all of this ran on;trogdor-qualityis the FLUX rig with a bracketed brain, whereslice_buildfalls back to placeholder art because the brain can't generate mid-session.Still open from this ticket
The milestone walk still batches (
production -> assets -> drain -> build). Only the vertical slice generates on demand. Moving the walk over is the rest of Part 1 and should probably wait until a slice reliably plays.The blocker was not the blocker: the gate was never playing the game
Session of 2026-07-28, picking up from the handoff above. Four fixes, all
pushed and CI-green; three deployed, one waiting on the current run.
The "missing index.html" was a symptom
Comment 430 named
422: no index.html found under /as the blocker andguessed the slice build was failing to write an entry point. It wasn't.
a-one-screen-arcade-2607281339walked ideation to ship without committingone line of code. Its
buildslist is seventeen identical entries:That run had been started while
...1313still held the build seat. Theorchestrator serves one build at a time and refuses the rest with 409, and
_hireread the refusal as a result — no commit, ledger fail, nodereturns. The gates then did exactly what they are designed to do with a
build that does not run: repair, repair, advance on the cap, for four
milestones plus alpha and beta, against an agent that had never been hired.
Every gate reported "no index.html" because there genuinely was no index.html,
because nothing had ever been built.
e56411b— a 409 waits for the seat now. Two concurrent runs serialize,which is what a single-seat orchestrator means. Any other non-200 still
fails fast: a 500 is an answer, not a queue.
The real one: a gate that never presses a key cannot tell a game from a poster
...1313did build a game, and its gates passed it. Milestone 1:PASS (59.5 fps; 1 canvas; 793 colors; motion 5.37). Milestone 2:PASS (59.5 fps; 1 canvas; 793 colors; motion 5.37). The same number to twodecimal places, twice. Milestone 3 as well.
The screenshots say why. Every one of them is this:
Nobody had clicked it.
playtest.runwas passive: load the page, screenshotevery two seconds, judge.
motion 5.37was the idle tween on the title text.Three milestone gates in a row photographed a title card and called it
playable.
Driving that same commit by hand, one run found:
In a keyboard handler — so it could only ever fire when something pressed a
key, and nothing ever did. A fourth invented API (
this.pauseMenuLine.linePath)turned up the same way, after
g.strokeArcandPhaser.Filter.DisplacementMap.Before changing the gate I wanted to know whether the input path worked at
all, or whether a click through the runner was a no-op everywhere. So:
trog-games/input-probe— a minimal Phaser 4 page whose only job is topaint a green box on
pointerdownand an orange one onkeydown. Throughthe playtest service it comes back
POINTERDOWN OK | KEYDOWN ArrowLeft.The harness was fine. The game's start handler was broken, and three gates
had no way to notice. Worth keeping — it answers "is it me or the game?" in
sixty seconds.
b8e5642—playtest.runandrun_zipdrive by default: wait out theCDN boot, click the middle of the canvas to get past a start screen, run the
arrows, then try space. Space comes last and after the measured screenshot,
because it is a start button in some games and a pause in others, and pausing
the game you are photographing is how you get another honest-looking PASS.
script=[]still means "watch it, do not touch it";fixture_baselineasksfor exactly that, so its two verdicts stay a fixed contract.
I looked at gating on
motionand decided against it: the title-card tweenscored 5.37 while the known-good fixture scores 1.34, so any threshold that
catches a dead game fails a live one. Motion stays reported, not gated —
page errors under real input are the honest hard gate, and those now fire.
The agent could not see its own bug either
Each repair session fixed the call it was shown and confidently wrote the
next one. What ran out was never the bugs; it was the retry cap. The agent
wrote code, its session ended, and only then did a browser open — so a made-up
API came back a whole session later, if at all.
1abeb83—playtest_build, an MCP tool that zips the LIVE workspace,posts it to the playtest service's existing
/playtest/upload, and hands theagent back its own console errors, failed requests and fps. Fifteen seconds,
no GPU, no commit, and it drives the game exactly like the gate. AGENTS.md
and every build brief now tell it to run that before finishing and after every
fix, and to believe the report over its reading of its own code.
Nothing is recorded. A ledger verdict is keyed to a commit and is the verdict
for that commit; a workspace has no commit, and writing one against HEAD would
let a build the agent has since changed read as already judged — the trap
critics.prejudge_audiodocuments.The MCP belt now rides on every rig. It was withheld wherever the brain
yields its cards so the agent would not be handed
generate_assetwhere itmust refuse — but that also withheld the one CPU-only tool that tells it
whether its code runs, on the exact rig whose slice falls back to placeholder
art.
generate_assetrefuses itself there, with the reason, and the briefsays so plainly.
One more, found by hitting it
f7351d6— an interrupted run has to give the build seat back. Asuperseded run resumed after the redeploy, hired a session, and took the seat;
the graph side had been cancelled, so nothing would ever read its commit, but
the session ran on and the only way to recover the seat was stopping the
agent's container over SSH by hand. Survivable only while a busy seat failed
the next run outright — now that a 409 waits, an orphaned session stalls the
next run for the full lock timeout.
POST /build/cancelcancels the task;codeseat.buildalready tears its container down in afinally, so that isthe whole job. Pushed and CI-green but NOT deployed — deploying would have
interrupted the run in flight.
State
292 tests, lint clean, playtest fixture baseline still passes both directions
(
goodPASS /brokenFAIL).playtest_buildis live on the MCP surface;the deployed gate completes its ten input steps.
Run 9 is in flight:
trog-games/a-one-screen-arcade-2607281432, thread125acbd6-0f9e-4af9-be61-1d36468d75d8. It is the first run where the gateactually plays the game and the agent can see its own build.
gate_sliceisthe signal to watch.
Notes for whoever picks this up
f7351d6:make build push, then thePortainer API
PUT /api/stacks/92/git/redeploy?endpointId=39withpullImage: trueand the existing Env — the webhook alone does not pull.trog runneedsTROG_URL=http://trogdor:8200 TROG_AEGRA_URL=http://trogdor:2026 TROG_FILES_URL=http://trogdor:3923.POST /orch/playtest {"game": ..., "record": false}— and look at thescreenshots, not the verdict line. Twice in the last two sessions the
verdict said one thing and the picture said another; both times the picture
was right. That is the whole reason this session found anything.
trog-games/a-one-screen-arcade-*andcreate-a-cozy-fishing-*are safe to delete. I don't do hard deletes.A playable vertical slice, first try, zero repairs
Run 9 (
trog-games/a-one-screen-arcade-2607281432, thread125acbd6-0f9e-4af9-be61-1d36468d75d8) clearedgate_fun,gate_sliceandgate_barwithfixes {}— nothing sent back to anybody. The slice is areal game.
Playtest run
2026-07-28/1785250572-4f9dd4, driven, three frames:Score: 0,Speed: 1x,Misses: 0/3), three generatedpixel-art salmon falling, a wooden bucket, ambient bubble particles
Score: 1,Misses: 2/3, the bucket moved left and tilted on ajuice tween, a
+1popup floating over itThe score going 0 → 1 is the part that matters: the drive script's arrow keys
moved the bucket and it caught a fish. Misses incremented, so the fail
state works too. "Catch falling fish in a bucket, score goes up, speed
increases" is implemented, running, and reachable by input.
gate_slicechecks:game_entryindex.html @1229f9d,assets_resolve7 references all resolve,
playtest_passedpass.What the self-playtest actually did
Nine
playtest_buildcalls in one 15-minute session, and the narration is aclean debugging arc rather than a loop:
generateCanvasdoesn't exist in Phaser 4. Let me fix the background andother API issues." — a fifth invented API, caught seconds after being
written instead of a graph repair later
I need to skip loading entirely and use placeholders." — this is the
missing-asset black screen that CLAUDE.md calls the most common way an
autonomous build "works", caught before the commit for the first time
them." — placeholders swapped for the real sprites once they finished
rendering
Meanwhile
/orch/healthshowedworking_seats ['audio','image']with thebrain up and the code agent writing — the on-demand generation #35 is about,
doing exactly what it says.
We never wired Phaser's own agent skills, and that is why it invents APIs
Raised by @cmoriarty, and correct.
docs/toolbelt-audit.mdsurveyed this inthe original toolbelt work and marked it keep for ticket 10:
Ticket 10 shipped the code seat. The skills were never wired — no mention
of them in
server/,opencode/, or either compose file, ever.Every API this pipeline has invented has a skill covering it:
Phaser.Input.Keyboard.JustPressedinput-keyboard-mouse-touch— documentsJustDown, the real callPhaser.Filter.DisplacementMapfilters-and-postfxg.strokeArc,line.linePathgraphics-and-shapesgenerateCanvasrender-texturesv3-to-v4-migrationThe audit even called the shot — it deferred a library-docs MCP because
"Phaser skills likely cover v1's API-hallucination risk for free. Adopt only
if ticket 10 shows the brain hallucinating APIs the skills don't catch." We
skipped the skills, hit the risk, and have been paying for a mitigation that
was designed and never installed.
6bd70e1bakes all 28SKILL.mdfiles (644K) into the code-seat image at/opt/phaser-skills, pinned to 4.2.1 — the version the games load from theCDN, because a skills copy that drifts from the runtime documents calls the
game does not have. Outside
/workspacedeliberately: the session commits itsworkspace diff, so skills unpacked in there would land in every game repo.
~2k words each is far too much to inject and exactly the right size to read
one of on demand, so AGENTS.md and all three build briefs now say to
ls $PHASER_SKILLSand read the topic before writing an unseen call.check-updates.pyreports drift against the upstream tag like every other pin.Worth being clear about the shape of the mistake: I spent the session building
detection (
playtest_build, driving gates) for a problem the design hadalready decided to prevent. Both earn their keep, but the audit should have
been read first.
Not candidates
builds in their private cloud, billed from $0.01/min. Fails the self-hosted /
no-phone-home bar the audit used to reject SaaS asset generators.
phaserjs/editor-mcp-serverdrives Phaser Editor v5. Wrong shape.Pushed, not yet deployed
f7351d6(interrupted run hands the build seat back) and6bd70e1(Phaserskills) are on main and CI-green, held back because redeploying would interrupt
run 9. Ship both after it finishes:
make build push, thenPUT /api/stacks/92/git/redeploy?endpointId=39withpullImage: trueand the existing Env.Then verify the skills actually get read: watch
GET /orch/buildfor the agentlisting
$PHASER_SKILLS/ reading aSKILL.mdbefore it writes Phaser calls.The success signal is fewer invented-API errors than run 9's nine self-playtest
iterations needed.
Still open on this ticket
The milestone walk still batches (
production → assets → drain → build). Onlythe vertical slice generates on demand. Moving the walk over is the rest of
Part 1 — and now that a slice reliably plays, that precondition is met.
Run 9 shipped — a complete walk, and a playable game
For contrast,
a-one-screen-arcade-2607281339— the run the last handoff waswritten from — was 0 of 17.
Verified at HEAD with a driven playtest, not from the summary line: all six
checks green, 58.7 fps, motion 24.68, zero page errors, zero failed requests.
The screenshots show
Score: 2,Misses: 2/3with the hearts depleting, a+2popup, and the bucket moved left by the gate's arrow keys. It is a game.Ideation → preproduction → slice → 3 milestones → alpha → beta → ship, with
one repair in the whole run (M2's
setLoopon a null sound, handed back withthe console and fixed on the first retry).
Three more bugs, all found by watching rather than by tests
84b7a16— the stall watchdog had never worked. A milestone session wentquiet; the watchdog fired on schedule, logged "STALLED — aborting the session",
and nothing happened. It posted to
/session/abortwith no id, which OpenCodeanswers 200 — it reads the path as a session named "abort" and dutifully
aborts that. So it got a success for a no-op and returned satisfied, while the
wedged session held the seat toward the full
BUILD_TIMEOUThour the watchdogexists to prevent. Proved on the live container: both URLs return 200, only
/session/{id}/abortreleased it, and the session then committed its work.The test was complicit — it asserted
"abort" in url, which the broken URLsatisfies.
b42f17a—playtest_build404'd on a file sitting in git. Mine, fromthe same session. The agent's playtest reported the music missing; it was in
the repo at 11.2MB and served fine by the gate, which clones from git. The zip
did not have it:
build_playtestsnapshotted withMAX_FILE_BYTES, whichanswers "is this committable" — the right question for a commit and the wrong
one for a browser, which only asks whether you can serve it the bytes. The tool
built to stop the agent chasing phantoms invented one, and it cost a repair
session. Playtest zips now carry 96MB. The other half of that confusion was
already there and silent — a file too big to commit was skipped with a log line
nobody reads — so the build now names it on the feed.
f7351d6— an interrupted run has to give the build seat back. A supersededrun resumed after a redeploy, took the seat, and the only recovery was stopping
the agent's container by hand.
Also worth recording: both wedges happened immediately after
todowrite.Two for two. If it recurs it is worth a ticket of its own.
The Phaser skills are live and being used
Verified end to end, not just in-image. Every build session since the wiring
has used them — six for six:
Used both before writing and as a self-audit afterwards. The alpha session read
v3-to-v4-migration/SKILL.md— the one the toolbelt audit singled out. Thecommitted code contains zero invented API, and it chose
.isDownoverJustDownfor held-key movement, which is applying the documentation ratherthan parroting it. M3 (Polish) is where the previous run died twice on
DisplacementMapandlinePath; this one readparticles,graphics-and-shapesandtweensfirst and passed.Deployed via an image swap with no stack redeploy —
opencode/is build-only,so a rebuilt
trog-opencode:latesttakes effect on the next build session. Theupdated AGENTS.md went into the live game repo the same way.
What is wrong with the shipped game
It runs. It is not good, and both defects are invisible to every gate.
1. The background is an illustration of the game.
assets/background/ocean-bg.pngis a large salmon, a purple trout, a woodenbucket and a crate painted into a seabed. Full-screen behind play, the player
sees big fish and a bucket that are not interactive, beside the tiny sprites
that are. The critic passed it correctly — it matches the brief. The brief is
the bug: the agent asked for "an underwater ocean background" and the generator
drew the subject. Same shape as the sfx-as-songs bug — the generator does what
it is asked, so fix the asking. A standing background suffix ("empty scenery
only, nothing the player could mistake for a gameplay object") is the obvious
move, mirroring
SFX_SUFFIX.2. The placeholder outlived the asset.
The real bucket loads and is never used; the player is still the flat rectangle
the slice drew while waiting. The guard at line 322 only avoids regenerating
the placeholder — nothing ever prefers the real texture. This is a direct
consequence of the #35 on-demand flow telling the agent to draw placeholders
and swap them later, and the swap silently not happening. Candidate fixes: a
house rule that a placeholder may only be created behind
if (!this.textures.exists(realKey)), and/or acheck_phaserule that anasset committed under
assets/and loaded but never referenced by anadd.sprite/add.imageis a failed check.Neither is a regression — they are the next layer, visible only now that games
survive long enough to be looked at.
State
Nine commits, all pushed, CI-green and deployed. 295 tests. The playtest
fixture baseline still passes both directions.
Still open on this ticket: the milestone walk continues to batch
(
production → assets → drain → build); only the vertical slice generates ondemand. That is the rest of Part 1, and its stated precondition — "wait until a
slice reliably plays" — is now met.