The problem: a cozy game that didn't feel cozy
The Kept Star is a small Godot cozy game inside the Starbound project. You plant star-seeds, they grow while you're away, you harvest them, a journal fills up. That's the whole loop. The v1 was functional but flat: exact countdown timers, simultaneous harvest effects, no audio, and on desktop it stretched into something ugly. Yesterday's session rebuilt it from the ground up as v2, and the interesting part is how the work was split between two AI agents and one human.
Research first, spec second, build third
Before any code shipped, the orchestrator agent ran two live-web research passes: one on cozy/idle design (Project Horseshoe's "Coziness in Games," Neko Atsume, Animal Crossing analyses) and one on game feel at a whisper register (Jonasson and Purho's "Juice It or Lose It," dialed way down). The output was a 41-line DESIGN-RULES.md with binding laws:
- Hide exact timers: fuzzy time words ("very soon," "a while yet"), never a countdown.
- Absence is a pause: ready crops wait forever, away time flattens, never punishes.
- Compress the first cycle to 20 seconds so the first session completes a full loop before real pacing kicks in.
- Tap-ack within 50ms, decoupled from the slower celebration animation. "I was heard" is separate from "it completed."
- No screenshake, ever. Hitstop reserved for milestones only.
Then the orchestrator distilled those rules into a dispatched spec and handed it to Codex (a second agent) to build the actual systems. The orchestrator's job was research, spec, and review. Codex's job was implementation. The human's job was loading it on desktop and saying "this looks terrible."
The three-cache bug
That human reaction triggered one of the cleanest debugging sequences I've seen in this project. Three stacked problems, peeled in order:
The real visual bug was trivial: stretch aspect was set to expand, so any non-9:16 window cropped the background and slid the hit targets off the painted beds. Switching to aspect=keep with a brand-violet clear color fixed it. The portrait garden now letterboxes cleanly at every size.
But the fix wouldn't land. Every re-export silently repacked day-one compiled scripts while the sources had moved on. A stale .godot editor cache was poisoning the export. Proven when nuking the cache changed the pck hash. The export script now deletes .godot before every export.
And the fix stayed invisible even after landing. Python's http.server sends no Cache-Control header, so Chrome heuristically reuses the pck and wasm files without revalidation. You'd fix the code, re-export, refresh, and see the old build. A custom serve.py with no-store, must-revalidate replaces it:
class NoCacheHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cache-Control", "no-store, must-revalidate")
self.send_header("Expires", "0")
super().end_headers()
That's a pattern anyone shipping Godot web builds will hit. The default dev server is not your friend.
Synthesized audio: no samples, no downloads
The soundscape is fully synthesized from pure tones, not sampled. A 137-line Python script generates six bell chimes and a 60-second continuous ambient pad from one A-major pentatonic set, so any overlap is consonant. The ambient bed compresses to a 285KB ogg. No downloads, brand-owned by construction.
A3 = 220.0
PENTA = [A3 * r for r in (1.0, 9/8, 5/4, 3/2, 5/3, 2.0)] # A B C# E F# A
def bell(freq, dur, amp=0.5, partials=((1.0, 1.0), (2.76, 0.35), (5.4, 0.12)), decay=4.0):
t = np.linspace(0.0, dur, int(SR * dur), endpoint=False)
sig = np.zeros_like(t)
for mult, pamp in partials:
sig += pamp * np.sin(2 * np.pi * freq * mult * t) * np.exp(-decay * mult * t / 2.0)
return amp * sig
The chimes map to game beats: plant thump, two-note rising harvest, picker glissandi, three-note milestone arpeggio, unlock pair. Web builds can't autoplay audio before a user gesture, so the ambient loop is gesture-gated: it starts on the first tap, then ducks under one-shots.
The juice: stagger, don't stack
The biggest feel improvement came from one rule: stagger, never stack. Harvest is now a choreography: press-ack (instant 0.95x scale) then pop (TRANS_BACK/EASE_OUT, ~1.15x) then soft motes (~10 particles, upward, 1.1s) then a float label (+N) then settle, each offset 50-100ms apart. Simultaneity reads flat. Sequence reads alive.
Ambient life is everywhere: 14 slow fireflies, foliage sway at +-1.2 degrees with phase offsets, star flicker on a 6-second cycle, background breathing at 1.008x over 9 seconds, two-layer parallax. None of it is loud. All of it adds up.
The review pass: trust but verify
The orchestrator checked Codex's diff against every section of the spec, confirmed nothing was out of scope, ran the 65-check verification suite independently (never trusting the "checks passed" claim at face value), and found one real bug: the new journal and mute HUD buttons overlapped the starlight label. Narrowed the TopBar panel, re-verified after the fix.
One gap is honestly flagged: interactive tap-through (plot to seed picker, HUD buttons to journal/mute) couldn't be confirmed in this session's browser automation. Raw JS-dispatched pointer events, a fresh tab, a fresh port, cleared IndexedDB, all inconclusive. The identical interaction worked before the letterbox fix, so a canvas-coordinate quirk in the automated tool is suspected over an actual game defect. But it's unconfirmed. It needs a real-device tap test before the interactive loop is trusted.
That's the part I appreciate most about this process. The agent didn't paper over the gap. It tried five things, documented each, and flagged it clearly for whoever picks up next. The handoff doc points to the durable context files in the repo rather than duplicating them. That's how you keep work resumable across agent sessions.