commits
Store the starting FEN in the PGN [SetUp] and [FEN] headers rather
than as a separate lexicon field. chess.js reads these headers on
loadPgn() automatically. Add daily challenge button with bot handle
via URL param, fix waitForOpponent re-init losing custom position,
and add variant rules doc for the bot.
Randomized piece compositions with standard movement rules. Each side
gets a random army balanced by material value (target 45-68 points,
+/- 5 variance). Black gets 0-13% fewer points than white. Kings stay
on e1/e8, pawns restricted to ranks 2/7.
- Board generation with seeded PRNG for future daily challenges
- Lexicon: variant and startingFen fields on game and challenge records
- Game store initializes chess.js from custom FEN when present
- PGN includes SetUp/FEN/Variant headers for non-standard games
- Variant threading through join, rematch, reconciliation, and PGN calls
- Shared CreateGameForm component used by /play and /play/really-bad-chess
- Spec at specs/really-bad-chess.md
- Add variant field to both game and challenge lexicons/types
- Remove redundant challengerColor from challenge (color is already
determined by the game record white/black fields)
- Enforce opponent check on challenge acceptance so directed challenges
can only be accepted by the intended player
- Sync SPEC.md lexicon blocks with actual lexicon files (add lastMoveAt,
variant; fix resultReason values)
Replace API-based Bluesky posting with bsky.app/intent/compose links,
removing the need for app.bsky.feed.post write access. Drop the blanket
transition:generic OAuth scope in favor of a custom permission set
(blue.checkmate.authFullAccess) scoped to game and challenge records.
Add logo_uri to client metadata for branded consent screen.
Replace API-based Bluesky posting with bsky.app/intent/compose links,
removing the need for app.bsky.feed.post write access. Drop the blanket
transition:generic OAuth scope in favor of a custom permission set
(blue.checkmate.authFullAccess) that grants access only to game and
challenge records. The consent screen now shows a single friendly label
instead of broad scary permissions.
- Remove buildFacets, postToBluesky and all facet/embed machinery
- Add openBlueskyCompose() using bsky.app/intent/compose?text=
- Add blue.checkmate.authFullAccess permission set Lexicon
- Publish schema to @checkmate.blue PDS via com.atproto.lexicon.schema
- DNS TXT record at _lexicon.checkmate.blue points to the publishing DID
- Deduplicate SCOPE const between oauth.ts and auth.svelte.ts
Move social/source links from header to a new footer. Includes Bluesky
butterfly and Tangled dolly icons linking to the Bluesky profile and
Tangled repo, plus an AT Protocol proof-of-concept tagline.
The winning player's result banner (You win!) was not rendering after
delivering checkmate. The game.result getter relied on gameResult(chess)
which reads from a chess.js class instance mutated in place -- Svelte 5
runes don't track mutations on class instances, so the template never
re-evaluated. Fix by explicitly calling game.setResult() in writeMove()
which sets the storedResult $state variable, triggering reactivity.
- Replace "Bluesky handle" with "ATmosphere username" in UI labels and placeholders
- Add autocapitalize=none, autocorrect=off, spellcheck=false to handle inputs
- Fix "Atmosphere" capitalization to "ATmosphere" across nav, homepage, game page
- Restructure roadmap into high/medium/lower priority tiers with new features:
time controls, integrity checks, push notifications, chess variants, and more
Adopt ATmosphere as the ecosystem name (AT Protocol for the protocol,
Bluesky for the app). Update README with full feature list, SPEC with
current color palette and implemented features, ROADMAP condensed to
remaining work with completed phases collapsed. Add architecture doc
explaining the move flow through AT Protocol.
Phase 5: editable share-to-Bluesky textarea with grapheme counting
after game completion. Rematch now navigates correctly by tracking
route param changes, and the opponent receives a live rematch
notification via Jetstream. Fixed stale drawOffered on resignation,
draw acceptance, and abandonment.
- Add skip navigation link, nav aria-label, main landmark
- Add aria-labels to form inputs, board, move list, sound toggle
- Add role="dialog" and aria-modal to promotion modal
- Add role="alert" to game result, draw offer, check, and error notices
- Add aria-hidden to decorative status dot
- Add global :focus-visible outline and remove conflicting focus:outline-none
- Add prefers-reduced-motion media query to disable animations
- Add per-route page titles via svelte:head
- Add sr-only h1 to homepage for heading hierarchy
- Update text-secondary color to #8b9098 for AA contrast compliance
- Document accessibility standards in CLAUDE.md
- #checkmate wordmark with blue # logo, converted to SVG paths
- Favicon as path-based SVG (no font dependency)
- PWA icons (192px, 512px PNG) and updated manifest
- OG meta tags with composite image (board + wordmark)
- Move branding to nav header with slogan, simplify homepage
- Background color to #0B0F14, text-secondary to #8b9098 (WCAG AA)
- Font family standardized to Helvetica Neue
Draw offers: player writes drawOffered to their record, opponent sees
accept/decline banner via Jetstream. Accepting writes draw result,
declining just dismisses. Making a move implicitly declines. State
persists across page reloads.
Also fixes: waitForOpponent ignoring events with status != active
(prevents premature transition when creating game as black),
connectJetstream called before PDS write (prevents dead Jetstream if
write fails), game controls hidden until first move played, explicit
result tracking for resignation and draw by agreement.
- Make login box prominent at top of page when opponent visits a game link
- Simplify firehose logger to append bare DIDs instead of full records
- Remove console.log/error statements from jetstream and atproto modules
- Profile page now uses GameCard component instead of raw rkey strings
- Add try-catch to findGameRecordByParent for consistent error handling
- Fix opponent handle check on waiting screen to use DID (resolves immediately)
- Homepage live feed empty state links to /play as CTA
- Sound effects for moves, captures, game end, and opponent notifications
- Rematch button after game completion (swaps colors)
- PGN download and Lichess analysis integration
- Abandoned game detection with 7-day inactivity threshold
- PWA manifest with theme color
- Draw offer state restored on page reload
- Fix waitForOpponent false activation when creator is black
- Lexicon: add lastMoveAt field and abandonment result reason
- Add project roadmap, spec, and implementation plans
When the opponent ends a game (checkmate, resignation), persist the
result to the current player's own PDS record. Previously only the
opponent's record was updated, leaving the other player's record
stuck as 'active'.
Verify opponent-claimed results before trusting them:
- Checkmate/stalemate/etc: verified from PGN via chess.js
- Resignation: only accepted if opponent claims they lost
- Draw agreement: not accepted from opponent's record alone
Add storedResult to game store so resignation/draw results display
in the result panel (chess.js only detects positional endings).
Reconciliation runs in three places:
- Real-time via Jetstream handler
- On page load when opponent's record shows completed
- Catch-all: if chess.js detects game over but status is stale
Also fix: only enter waitForOpponent when game is actually waiting,
and hide connection indicator on completed games.
- Add initialCursor option to JetstreamConnection to prevent missed
events when opponent joins during WebSocket connection setup
- Show login prompt to unauthenticated users on game pages so they
can sign in to join or play
- Load user games from PDS directly instead of Constellation to
avoid indexing delay on pending games
- Add listGamesForDid helper for direct PDS game listing
Redesign homepage with rich game cards showing player handles, status,
move count, and relative time. User's games fetched from Constellation
and deduplicated (challenger's record only. Split into active and
completed sections.
Live feed connects to Jetstream with 2-hour historical cursor to show
active games across the network, visible to all visitors.
Add standalone firehose logger script (npm run firehose) that writes
all game and challenge events to data/firehose.jsonl for future stats.
EOF
)
- Prevent White from moving before opponent joins (isMyTurn requires active status)
- Auto-complete game on checkmate instead of leaving resign available
- Add check indicator banner during active games
- Show personal win/lose/draw result with human-readable reason labels
- Filter noisy non-commit Jetstream events from console logs
- Post challenge to Bluesky with @mention facet and game link
- Send via DM option (copies link, opens bsky.app/messages)
- Color selection on /play (White, Black, Random)
- Game page handles owner-as-either-color (decoupled from record ownership)
- Spectator mode: non-participants see read-only board with flip button
- Unauthenticated users can view any game via public PDS reads
- Dual Jetstream connections for spectators (one per player)
- Check-before-join guard to reduce race condition on open games
- Public agent helpers (getGamePublic, findGameRecordByParentPublic)
- challengerColor field added to challenge lexicon
- Fixed all pre-existing type errors (oauth, Board, route params)
- 9 new tests for bluesky facet construction
- Fix polling fallback: pass agent to JetstreamConnection so polling
works when WebSocket disconnects
- Add error handling to writeMove: catch failures, roll back local
chess state, show error banner
- Fix challenge acceptance duplicates: check for existing game record
before creating a new one
- Document firehose limitation in waitForOpponent
- Resolve player handles via Slingshot after game load
- Add Vitest with 29 tests covering game-logic and atproto modules
Store the starting FEN in the PGN [SetUp] and [FEN] headers rather
than as a separate lexicon field. chess.js reads these headers on
loadPgn() automatically. Add daily challenge button with bot handle
via URL param, fix waitForOpponent re-init losing custom position,
and add variant rules doc for the bot.
Randomized piece compositions with standard movement rules. Each side
gets a random army balanced by material value (target 45-68 points,
+/- 5 variance). Black gets 0-13% fewer points than white. Kings stay
on e1/e8, pawns restricted to ranks 2/7.
- Board generation with seeded PRNG for future daily challenges
- Lexicon: variant and startingFen fields on game and challenge records
- Game store initializes chess.js from custom FEN when present
- PGN includes SetUp/FEN/Variant headers for non-standard games
- Variant threading through join, rematch, reconciliation, and PGN calls
- Shared CreateGameForm component used by /play and /play/really-bad-chess
- Spec at specs/really-bad-chess.md
- Add variant field to both game and challenge lexicons/types
- Remove redundant challengerColor from challenge (color is already
determined by the game record white/black fields)
- Enforce opponent check on challenge acceptance so directed challenges
can only be accepted by the intended player
- Sync SPEC.md lexicon blocks with actual lexicon files (add lastMoveAt,
variant; fix resultReason values)
Replace API-based Bluesky posting with bsky.app/intent/compose links,
removing the need for app.bsky.feed.post write access. Drop the blanket
transition:generic OAuth scope in favor of a custom permission set
(blue.checkmate.authFullAccess) scoped to game and challenge records.
Add logo_uri to client metadata for branded consent screen.
Replace API-based Bluesky posting with bsky.app/intent/compose links,
removing the need for app.bsky.feed.post write access. Drop the blanket
transition:generic OAuth scope in favor of a custom permission set
(blue.checkmate.authFullAccess) that grants access only to game and
challenge records. The consent screen now shows a single friendly label
instead of broad scary permissions.
- Remove buildFacets, postToBluesky and all facet/embed machinery
- Add openBlueskyCompose() using bsky.app/intent/compose?text=
- Add blue.checkmate.authFullAccess permission set Lexicon
- Publish schema to @checkmate.blue PDS via com.atproto.lexicon.schema
- DNS TXT record at _lexicon.checkmate.blue points to the publishing DID
- Deduplicate SCOPE const between oauth.ts and auth.svelte.ts
The winning player's result banner (You win!) was not rendering after
delivering checkmate. The game.result getter relied on gameResult(chess)
which reads from a chess.js class instance mutated in place -- Svelte 5
runes don't track mutations on class instances, so the template never
re-evaluated. Fix by explicitly calling game.setResult() in writeMove()
which sets the storedResult $state variable, triggering reactivity.
- Replace "Bluesky handle" with "ATmosphere username" in UI labels and placeholders
- Add autocapitalize=none, autocorrect=off, spellcheck=false to handle inputs
- Fix "Atmosphere" capitalization to "ATmosphere" across nav, homepage, game page
- Restructure roadmap into high/medium/lower priority tiers with new features:
time controls, integrity checks, push notifications, chess variants, and more
Adopt ATmosphere as the ecosystem name (AT Protocol for the protocol,
Bluesky for the app). Update README with full feature list, SPEC with
current color palette and implemented features, ROADMAP condensed to
remaining work with completed phases collapsed. Add architecture doc
explaining the move flow through AT Protocol.
- Add skip navigation link, nav aria-label, main landmark
- Add aria-labels to form inputs, board, move list, sound toggle
- Add role="dialog" and aria-modal to promotion modal
- Add role="alert" to game result, draw offer, check, and error notices
- Add aria-hidden to decorative status dot
- Add global :focus-visible outline and remove conflicting focus:outline-none
- Add prefers-reduced-motion media query to disable animations
- Add per-route page titles via svelte:head
- Add sr-only h1 to homepage for heading hierarchy
- Update text-secondary color to #8b9098 for AA contrast compliance
- Document accessibility standards in CLAUDE.md
- #checkmate wordmark with blue # logo, converted to SVG paths
- Favicon as path-based SVG (no font dependency)
- PWA icons (192px, 512px PNG) and updated manifest
- OG meta tags with composite image (board + wordmark)
- Move branding to nav header with slogan, simplify homepage
- Background color to #0B0F14, text-secondary to #8b9098 (WCAG AA)
- Font family standardized to Helvetica Neue
Draw offers: player writes drawOffered to their record, opponent sees
accept/decline banner via Jetstream. Accepting writes draw result,
declining just dismisses. Making a move implicitly declines. State
persists across page reloads.
Also fixes: waitForOpponent ignoring events with status != active
(prevents premature transition when creating game as black),
connectJetstream called before PDS write (prevents dead Jetstream if
write fails), game controls hidden until first move played, explicit
result tracking for resignation and draw by agreement.
- Make login box prominent at top of page when opponent visits a game link
- Simplify firehose logger to append bare DIDs instead of full records
- Remove console.log/error statements from jetstream and atproto modules
- Profile page now uses GameCard component instead of raw rkey strings
- Add try-catch to findGameRecordByParent for consistent error handling
- Fix opponent handle check on waiting screen to use DID (resolves immediately)
- Homepage live feed empty state links to /play as CTA
- Sound effects for moves, captures, game end, and opponent notifications
- Rematch button after game completion (swaps colors)
- PGN download and Lichess analysis integration
- Abandoned game detection with 7-day inactivity threshold
- PWA manifest with theme color
- Draw offer state restored on page reload
- Fix waitForOpponent false activation when creator is black
- Lexicon: add lastMoveAt field and abandonment result reason
- Add project roadmap, spec, and implementation plans
When the opponent ends a game (checkmate, resignation), persist the
result to the current player's own PDS record. Previously only the
opponent's record was updated, leaving the other player's record
stuck as 'active'.
Verify opponent-claimed results before trusting them:
- Checkmate/stalemate/etc: verified from PGN via chess.js
- Resignation: only accepted if opponent claims they lost
- Draw agreement: not accepted from opponent's record alone
Add storedResult to game store so resignation/draw results display
in the result panel (chess.js only detects positional endings).
Reconciliation runs in three places:
- Real-time via Jetstream handler
- On page load when opponent's record shows completed
- Catch-all: if chess.js detects game over but status is stale
Also fix: only enter waitForOpponent when game is actually waiting,
and hide connection indicator on completed games.
- Add initialCursor option to JetstreamConnection to prevent missed
events when opponent joins during WebSocket connection setup
- Show login prompt to unauthenticated users on game pages so they
can sign in to join or play
- Load user games from PDS directly instead of Constellation to
avoid indexing delay on pending games
- Add listGamesForDid helper for direct PDS game listing
Redesign homepage with rich game cards showing player handles, status,
move count, and relative time. User's games fetched from Constellation
and deduplicated (challenger's record only. Split into active and
completed sections.
Live feed connects to Jetstream with 2-hour historical cursor to show
active games across the network, visible to all visitors.
Add standalone firehose logger script (npm run firehose) that writes
all game and challenge events to data/firehose.jsonl for future stats.
EOF
)
- Prevent White from moving before opponent joins (isMyTurn requires active status)
- Auto-complete game on checkmate instead of leaving resign available
- Add check indicator banner during active games
- Show personal win/lose/draw result with human-readable reason labels
- Filter noisy non-commit Jetstream events from console logs
- Post challenge to Bluesky with @mention facet and game link
- Send via DM option (copies link, opens bsky.app/messages)
- Color selection on /play (White, Black, Random)
- Game page handles owner-as-either-color (decoupled from record ownership)
- Spectator mode: non-participants see read-only board with flip button
- Unauthenticated users can view any game via public PDS reads
- Dual Jetstream connections for spectators (one per player)
- Check-before-join guard to reduce race condition on open games
- Public agent helpers (getGamePublic, findGameRecordByParentPublic)
- challengerColor field added to challenge lexicon
- Fixed all pre-existing type errors (oauth, Board, route params)
- 9 new tests for bluesky facet construction
- Fix polling fallback: pass agent to JetstreamConnection so polling
works when WebSocket disconnects
- Add error handling to writeMove: catch failures, roll back local
chess state, show error banner
- Fix challenge acceptance duplicates: check for existing game record
before creating a new one
- Document firehose limitation in waitForOpponent
- Resolve player handles via Slingshot after game load
- Add Vitest with 29 tests covering game-logic and atproto modules