Repository navigation
Make the dance a conversation with call and answer - #46
Merged
Merged
Conversation
All 23 card illustrations are new renders from gpt-image-2.5-sunburst. The Open Position anchor was painted first; every other card was painted with that anchor attached as a reference image, so the same couple, clothes, palette, and painted floor carry across the set. The figures move more, both hand connections read at thumbnail size, faces stay apart in a social frame, and the Follow wears low practical shoes instead of heels. Cross Body Lead — Closed (mov_013) gets its own Closed-frame composition instead of a copy of mov_005. The prompts, model, endpoint, and reference for every card are in docs/art/card-art-prompts-v2.jsonl. Checks: npm run check:art.
When a dance step ends, the dancer either takes the song card's bonus or calls a figure for the partner to answer next 8-count. Answering pays the call's Style, plus afinque (a chain bonus for consecutive answers), plus 1 if it was the song's favoured call. Dropping a call costs 1 Connection unless a Recovery card covers it. A Position played before any other card takes or adjusts the hold, and every 8-count now ends on one rung of a five-step ladder (Perfect, Clean, Awkward, Recovered, Break). Content: seven calls in rules.call, favoured calls on the song deck, the answeredCall bonus op, Hand Twist in the starter instead of Lead Left Turn, private lessons to the top of the draw pile, Ponche (flo_005, a Beginner Pose worth no Style, painted with the same GPT Image 2.5 anchor as the repaint), a louder montuno, and the rating ladder re-fit against measured games. Engine version 5 -> 6. Every new state, checkpoint and content field has a default, so a stored engine-5 room resumes and plays on, and history entries from before the ladder take their rung from their own outcome. Goldens are regenerated with a sixth, call-aware strategy that calls, answers, covers and adjusts (`npm run golden:write`). Also: a clamped addStyle, one shared Connection-loss ending, an exhaustive bonus-condition switch, and a song-bonus preview that sees the answer to a standing call. The rulebook, 14 dated rulings, the card list, CONTEXT.md, the plan's post-v1 amendment and the README carry the new rules, with the measured numbers they were tuned against.
Three GPT Image 2.5 illustrations in the card art's style, made from the same anchor render: the couple dancing in front of a six-piece band for the home screen, a joyful finish for a completed song, and the couple laughing and reaching for each other again for a lost one. The art direction doc records the prompts, the candidates, and why each was chosen.
The static file server answered .webp with application/octet-stream, so every card painting reached the browser untyped. It now names image/webp, the audio types a soundtrack would need, and the other files Vite emits.
Ending a dance step now offers "Take the song · +N" beside a "Make a call" sheet: the seven calls with their asks and Style, the song's favoured call with its in-the-pocket bonus, and what the afinque chain would add. A banner keeps the standing call and its live answered or not-answered state in front of both players, the hot-seat hand-off leads with the call, a Recovery card covers it from the dance row, and a Position played before any other card takes or adjusts the hold. The HUD gains an afinque meter, and every danced 8-count shows its rung in the phrase strip. A pure snapshot-events differ replaces the HUD's score differ and drives synthesized cues and short toasts on both devices for calls, answers, drops, covers, afinque, the montuno and the cierre. It stays silent across a reset turn, a reload and a hot-seat hand-off. The song ends on a recap: the end-of-song illustration, the rung counts, both phrases, the calls, and the biggest 8-count. The rules dialog generates its call, afinque, hold and ladder sections from the game's own rules, and the home screen leads with the house-band illustration. Everything is read from the snapshot. No balance number is written into the client.
New specs make a call through the UI from a fresh hot-seat game, hand over, answer it, and see it paid out; cover a call with a Recovery card; adjust the hold; carry a call across two online devices; end a song and a lost dance on their recaps; and work the call sheet with the keyboard alone. Seeded states come from real engine games, so a content retune cannot leave a test checking a state the rules no longer reach. Existing specs follow the new starter deck and the new end-dance button. The sixteen-count game is marked slow, since it needs more than the default 30 seconds on the phone profile. The e2e server now uses an in-memory room database, so repeated local runs no longer fill the 50-room floor.
A second attempt at an original house-band soundtrack, this time with Google's Lyria 3.5. The takes had the right tempo and length but failed the same quality gate as the ElevenLabs attempt: two independent listening models scored all seven between 1 and 4 out of 10 for sounding like a band in a room, and one take leaked eight seconds of speech. Nothing ships; the record keeps the prompts, measurements and verdicts for the next try.
The rulings already said a stumble turns neither face of the song card, but the engine still paid one whose condition a stumble happened to meet: on 8-count 5 a stumble collected +1 Connection, and with Reconnect in hand it came out ahead. The song bonus guard now covers the stumble, so the preview and the payout cannot disagree. The order a dropped call resolves in is now a ruling rather than an accident: the 8-count's own song card pays first, then the call over it is settled. Each danced 8-count now records the call it made, how the call over it resolved (a stumble's drop counts as a drop), the song bonus it landed, and whether it took or adjusted the hold. The snapshot projects what ending the 8-count now would pay (song card and standing call together) and what an answer to a call made now would earn from the afinque chain. It also publishes the stumble cost and where each lesson tier lands, so the client reads these numbers instead of repeating them, and a finished game carries its whole log.
The gold end-of-dance control now reads what ending the 8-count really pays, so it can no longer promise a Connection that a dropped call takes straight back. The banner and the hand-off print what a drop costs. The call sheet's afinque figure comes from the engine's projection, the cover control stops offering to throw away a call that is already answered, and keyboard focus lands on the next step's control instead of the page body. On desktop the table scrolls instead of clipping, so the hand stays reachable with a call standing at 1280x720 and 1366x768. All seven calls fit the sheet, and the shut-call reason travels with the control. The cue layer outlives the swap to the end screen, so the cierre and a final dropped call are seen and heard. A cue no longer reports a multi-step jump after a reconnect as a single step. The recap counts calls, drops, covers and song bonuses from each 8-count's own record instead of the log, which missed stumble drops and fell out of its window on long games. Also: the rules dialog shows the live game's numbers, a private lesson flies to the draw pile it lands on, the phrase strip names the hold, the music drawer no longer reports a failure that never happened, phones fetch the smaller hero, and the Break rung passes contrast. New browser tests cover a dropped call, the cierre over the end screen, a drop that ends the dance, a 1366x768 laptop answering from the hand, and focus after choosing a call.
npm run typecheck covered only src/, so 53 type errors had piled up in the test suites, most of them END_DANCE actions written without their callId. Each workspace now has a tsconfig.tests.json, and a root tsconfig.tools.json covers the e2e specs, scripts and test configs. The errors are fixed.
…reen At 1280px wide the opening step's dance row wrapped onto a third line and pushed "Dance the basic" out of its band, on the first 8-count of every game. The shut-call label is shorter, the opening guide leaves the row on short desktops (screen readers still hear it), and the row's grid track now grows to the height it wraps to. That also clears a tablet overflow and some older 2px bleeds. The drop price stops promising a clean bonus that an earlier dropped call already took: the snapshot now publishes the phrase's dropped call. The call sheet shows the same price as the banner. End-screen toasts sit clear of the score and the headline, and on short screens such as landscape phones they stay inside the viewport. Also: the rules dialog's stumble sentence reads the live game's cost, the hot-seat hand-off attributes the call's words to the caller, and a new game opens scrolled to the top.
The two-device call test read the active screen's hand the moment a turn changed hands. Locally the next snapshot is already there; against the deployed staging site it arrives a network round trip later, so the test danced the basic four times and gave up. It now waits for the screen to announce its own 8-count first.
A third attempt at an original house-band soundtrack, with Mureka's mureka-9.5 instrumental model. Its three takes were the best behaved of any attempt: on tempo, free of voices, and lossless. They still failed the same gate, with every listening pass scoring 1 to 4 out of 10 for sounding like a band in a room. One listener reported a glitch every ten seconds that measurement shows is not in the file, so this record also points at the places where the two listeners disagree, for a human ear to settle. Nothing ships; the README and the plan amendment now count three attempts.
A fourth attempt, with Suno's newest model (V5_5; there is no v6) through the reseller APIFRAME. Its ten takes were the first with a clave and a bass tumbao both listeners agreed on, but they still sounded programmed: one take reached 6/10 and the identical request then gave 4 and 4. They could not ship regardless. Suno has no public API, assigns output rights only to its own Pro and Premier subscribers, and its terms bar obtaining output outside its own channels, so audio bought through a reseller carries no rights to pass on. The README and the plan amendment now count four attempts.
The Suno attempt's record, and the commit that added it, said there is no Suno v6. Suno released v6 on 2026-09-09 and retired its earlier models the same day. What is true is narrower: APIFRAME's model list still stops at V5_5, so it does not offer v6, and which model actually answered these V5_5 requests cannot be confirmed from outside.
The repository is published under all rights reserved, for reference and portfolio purposes, so two sentences in the licensing notes that called Clave open source were wrong.
Jacob made four takes with Suno v6 in Suno's own app on his own Pro account on 2026-10-24, the one route on which the output is his. Cuerpo B and Montuno A, the steadier groove of each pair, are cut into bar-exact loops: 40 bars (51.56 s) for the cuerpo and 56 bars (73.04 s) for the montuno, each placed where the take's tempo drift leaves its two ends within about 1% of each other. Each file is a 0.75 s pre-roll, the loop body and a 0.75 s post-roll, joined with an 80 ms equal-power crossfade, so a decoder that trims or keeps a few samples of its own still lands on matching audio. The loop points are in house-band-loops.json. Both files are 44.1 kHz stereo 192 kbps MP3 at -16.0 LUFS, and the seams were measured clean in ffmpeg, in Chromium's decodeAudioData and in a looping AudioBufferSourceNode.
The music drawer gains a house band switch, off by default and remembered per device. Switched on, the band plays the cuerpo through the first phrase and crossfades to the montuno when the song card's section turns to Montuno, read from the snapshot like everything else, then fades out when the dance ends. Each device plays its own copy; nothing is synced. The band builds its own AudioContext inside the gesture that turns it on, fetches the cuerpo first and the montuno behind it, loops each between its loop points so no seam is ever played, and pauses with a hidden tab. A preference saved as on starts on the next gesture, and every gesture retries, because a phone lets the end of a touch start audio but not its start. Teardown stops the voice without writing its gain, which a browser refuses in the middle of a fade. The test fake now enforces that rule. The drawer still links to the real Llorarás on Spotify, and its button now reads "Music".
The soundtrack record gains a fifth attempt, the one that shipped: why Suno's own app on the owner's Pro account is the route on which the output is his, the model and the exact settings as Suno recorded them, the condensed style text v6 generated from, all four takes measured, and the loop engineering with the candidates it rejected. The montuno's remaining 1.5 dB step at its seam is noted, with a tighter 24-bar alternative. The plan's §9 amends the music non-goal in its first two clauses only (synchronization is still a non-goal) and D7, and takes "a bundled or original soundtrack" off the Phase 6 menu as delivered. The README, CONTEXT.md and the task log say what ships.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Clave becomes a conversation. When your 8-count ends you either take the song card, or call a figure your partner must answer on theirs. Answered calls chain into afinque. A dropped call costs Connection unless a Recovery card covers it. Every 8-count lands on a rung, the montuno gets louder, every painting was redone with GPT Image 2.5, and an original house band can play under the dance.
v1 could not be lost. Simulated pairs never stumbled, never lost (0.0%), and three quarters of games got the same rating: a turn's best play was worth 1.90 points over its worst. The call gives a pair something to risk, something to read in each other's hands, and a crescendo to build toward.
Also in the rules: the hold (a Position played before any other card takes or adjusts the floor, giving up the song bonus), Ponche (
flo_005, a Beginner Pose), Hand Twist in the starter instead of Lead Left Turn, private lessons on top of the draw pile, a retuned song deck, and a re-fit rating ladder. The rulebook, 15 dated rulings, the card list andCONTEXT.mdcarry all of it.Commits, in order: the card repaint · the rules, engine and content (
ENGINE_VERSION5 → 6) · the illustrations · server media types · the client · the browser tests · the soundtrack record · four commits of review fixes · the task log · an online-test fix found on staging · the Mureka and Suno soundtrack records · the house band's loops, player and record.The house band
Two original instrumental loops, made with Suno v6 in Suno's own app on Jacob's Pro account. That is the one route on which Suno's terms assign the output to him; a reseller's API cannot. The Cuerpo loop plays through phrase 1 and crossfades into the Montuno when the song card turns to Montuno. Both are cut from the takes to whole bars and loop without a seam.
Each file is a 0.75 s pre-roll, the loop body and a 0.75 s post-roll, so a browser that trims MP3 padding differently still lands on matching audio. Both are 44.1 kHz, 192 kbps, −16.0 LUFS, 1.3 MB and 1.8 MB, fetched only after the band is switched on. The drawer still links to the real Llorarás, which is never hosted.
docs/audio/soundtrack.mdhas the provenance, settings and loop engineering.Staging: https://staging.clave.dance runs this branch at
eaf6fc1, before the house band. At Jacob's request, production gets the merge without a staging pass. Staging is a separate service, database and Apache site beside production.Evidence
Balance, measured on the real engine (400 simulated games per policy; v1 baseline from the same harness):
Misses, kept honest: a sensible pair loses 0.5% of games, against a 5–20% target. A Recovery card covers a 1-Connection drop exactly, and raising the drop cost to 2 sends bold play to 76% losses. Song cards 4 and 9 still fire in only 2% and 13–14% of games whatever they pay; the fix there is the condition or the starter, not the number. A pair that obeys the ★ blindly scores below a pair that never calls (60.4 vs 64.3). The call rewards reading your partner, not obedience.
Tests:
[mobile-chrome] plays a complete sixteen-count hot-seat gametiming out at 30 s on master.npm run typechecksaw onlysrc/.npm run typechecknow covers tests, browser tests and scripts too, which turned up 53 type errors, all fixed. Lint, format, build and the art check pass; the bundle is 125 KB gzipped (limit 500).Production serves the card art untyped today, fixed here:
Merge Danger
Door: Two-way for the code. One-way for games saved while it is live.
Engine-5 rooms and hot-seat saves resume under engine 6 on defaults (server test included). Pre-ladder history entries take their rung from their own outcome. Games saved under engine 6 carry state that engine 5 was never tested against, so a rollback may strand rooms that are mid-dance.
Blast Radius: Everything.
Every turn's ending, scoring, balance, ratings, all card art, and the phone, tablet and desktop layouts change. The app grows by about 3 MB of audio, which nobody downloads unless they switch the band on. Merging to
masterruns the production deploy. Its last runs were held by the server's active-room guard, because rooms had moved within 48 hours; it was not an SSH fault. The guard passes now. Issue #45 (in progress) requiresENGINE_VERSIONto stay at 5 and the goldens to stay put; whichever lands second rebases and runsnpm run golden:write. No overlap with #39.Left for later:
Predates this branch, found along the way.
/api/roomsand the hot-seat completion beacon (rate-limited in Rate-limit the hot-seat beacon and cap its stored rows #35) draw on one 10-per-minute, per-IP budget inpackages/server/src/app.ts, so ten finished hot-seat songs from one IP within a minute block room creation. Real play won't get there, but the e2e suite did, and it now stubs the beacon. A separate limiter for the beacon would close it. Static files are served withoutCache-ControlorETag. At 1280×800 the song band clips a two-line song-card description.Balance misses (measured; the rulings record the levers): the sensible-pair loss rate, song cards 4 and 9, shared flair as a near-automatic bonus for pairs that never call, and the ★ obedience trap.
For Jacob's ear: the Montuno loop arrives at its seam about 1.5 dB hotter than it leaves, so the band steps back a notch each time round. A 24-bar alternative measures much tighter. Switching to it means changing the loop points in
house-band-loops.jsonand swapping in the re-cut file.Not in this PR: shines as a card type, telegraphing from the watcher's hand, an AI partner for solo play, a dealt song deck. Before the house band, four soundtrack attempts failed: ElevenLabs Music, Google Lyria 3.5, Mureka
mureka-9.5, and SunoV5_5through the reseller APIFRAME. None sounded like a band playing, and audio bought through a Suno reseller carries no rights to ship. A public-domain search found only 1909–1925 Cuban danzón and son, which predates salsa's sound.API access injected by vaulted-agent
Names only. No value was printed, logged or committed. Claude Code's permission classifier blocked reading vaulted-agent's own configuration as credential exploration, so this is what the session's environment exposed, not a confirmed manifest. The push and this PR used the account's own
ghkeyring login, not an injected token.Used
OPENAI_API_KEY: GPT Image 2.5 (gpt-image-2.5-sunburst, high quality, 1024×1536). About 100 renders: 84 attempts across the 22 repainted cards, 4 model-comparison probes, and 12 for Ponche and the three illustrations. Prompts are indocs/art/card-art-prompts-v2.jsonlanddocs/art/card-art-direction.md.ELEVENLABS_API_KEY: ElevenLabs Music (music_v2_5), 4 soundtrack generations, 5,469 credits. Failed verification.GEMINI_API_KEY: Lyria 3.5 and Lyria 3 Clip, 7 soundtrack generations, which failed verification. Gemini 3.1 Pro and 2.5 Pro as listening judges, about 24 audio calls across the three attempts. Model listing.MUREKA_API_KEY: Murekamureka-9.5instrumentals, 2 requests and 3 takes, 45 of the account's 51 credits. Failed verification.Added to the vault mid-session and used through
va bash:APIFRAME_API_KEY: SunoV5_5through APIFRAME, 5 requests, 10 takes, 55 of 100 free-plan credits. Not shippable; see the soundtrack record.No key at all: the house band. Its takes were made in Suno's web app on Jacob's own Pro account, through his Chrome session.
Present, not used:
ANTHROPIC_API_KEY,ASSEMBLY_AI_API_KEY,BASETEN_API_KEY,BLACK_FOREST_LABS_API_KEY,CARTESIA_API_KEY,DEEPINFRA_API_KEY,FAL_AI_API_KEY,FIREWORKS_API_KEY,GRANOLA_API_KEY,GROK_API_KEY,GROQ_API_KEY,HUME_API_KEY,HUME_SECRET_KEY,IDEOGRAM_API_KEY,KEEPLORE_API_KEY,META_AI_API_KEY,RECRAFT_AI_API_KEY,SAMBA_NOVA_API_KEY,TOGETHER_AI_API_KEYInfrastructure credentials, also present and not used:
AWS_SES_ACCESS_KEY,AWS_SES_SECRET_ACCESS_KEY,CLOUDFLARE_API_TOKEN,CLOUDFLARE_EDIT_ZONES_TOKEN,DIGITALOCEAN_TOKEN,DOCKER_READ_ONLY_PAT,PORKBUN_API_KEY,PORKBUN_SECRET_KEY,TRACEWAKE_CAREER_GITHUB_TOKEN,TRACEWAKE_DROME_PAT,TRACEWAKE_PASSWORD,TRACEWAKE_SMTP_PASSWORD,TRACEWAKE_VAULTED_AGENT_GITHUB_TOKEN,TRACEWAKE_VOICE_AGENT_PATThis task needed five of the 38:
OPENAI_API_KEY,ELEVENLABS_API_KEY,GEMINI_API_KEY,MUREKA_API_KEYandAPIFRAME_API_KEY. The DNS, cloud, email and GitHub tokens let a coding agent reach well beyond this repo if it misbehaved or were prompt-injected. A per-task manifest would narrow that.