- pnpm workspace: shared/server/client packages - Server: Fastify+ws, chess.js, FSM (touch-move + hierarchy), per-player view filter, zod validation, rate limiting, grace-window disconnect handling - Client: Svelte 5 + Vite, click-to-move board, moderator panel, promotion/draw dialogs - Shared: protocol types, ModeratorText enum, geometricMoves helper (provably zero opponent-info leak) - 43 tests pass (21 shared, 22 server incl. 4 real-WS integration) - Deploy: CT 690 on node-241 (192.168.0.245), systemd-managed, Caddy block for chess.sethpc.xyz - Live at https://chess.sethpc.xyz Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
Handoff: Spec approved, ready for implementation plan
Session Metadata
- Created: 2026-04-28 10:43:44
- Project: /home/claude/bin/blind_chess
- Branch: not a git repo (yet — see Pending Work)
- Session duration: ~90 minutes (single brainstorming session)
- Recent commits: none — repo not initialized yet.
Handoff Chain
- Continues from: 2026-04-28-kickoff.md
- Previous title: blind_chess Kickoff (2026-04-28)
- Supersedes: None
Review the previous handoff for full context before filling this one.
Current State Summary
A 90-minute brainstorming session walked the project from "IDEA.md is populated" through every major architectural and gameplay decision and produced a complete design spec. Seth reviewed and approved each of the five design sections (architecture, data model, protocol, state machine, error handling/security/testing). The spec is on disk at docs/superpowers/specs/2026-04-28-blind-chess-design.md. Self-review applied four small fixes inline. Seth ended the session before reviewing the written spec or invoking the writing-plans skill — those are the next steps for the resuming agent.
Architecture Overview
Designed (not yet implemented):
- Language/stack: Node 22 + TypeScript, Fastify +
ws, Svelte + Vite,chess.js. - Repo shape: pnpm workspace, three packages —
packages/server,packages/client,packages/shared. Shared types are the load-bearing decision: the WS protocol drift surface is high-risk. - State: in-memory only (
Map<gameId, Game>). No DB. Server restart drops active games — acceptable for MVP. - Engine:
chess.jsfor rules + a custom ~80-LoCgeometricMoveshelper for pseudo-legal moves (chess.js doesn't expose those). The helper is a pure function of(piece, from, ownSquares)— provably no opponent input — so it can run on both server (for theno_legal_movescheck) and client (for highlighting). It lives inpackages/shared. - Deploy: new LXC on node-241, Caddy CT 600 routes
chess.sethpc.xyz→ port 3000, systemd-managed Node service. Single-port everything (HTTP + WS + static). - Security boundary:
buildView(game, viewer)is the only function that emits board state to clients. Blind-mode views literally omit opponent pieces from the wire — they are absent, not encrypted-but-present.
Critical Files
| File | Purpose | Relevance |
|---|---|---|
| IDEA.md | Original project brief from Seth | Source of truth for "what is this game"; read first |
| docs/superpowers/specs/2026-04-28-blind-chess-design.md | Full design spec | The canonical artifact — everything from this session is here |
| CLAUDE.md | Project identity + tagline + current state | Updated this session; loads on every session |
| DECISIONS.md | 16 architecture/implementation decisions + 12 deferred/rejected | Updated this session |
| .claude/handoffs/2026-04-28-kickoff.md | Previous handoff (project scaffold) | Read second |
| .gitignore | Excludes .superpowers/ and .backup/ |
Created this session |
| .superpowers/brainstorm/2734300-1777384084/content/ | Visual companion artifacts (3 mockup HTMLs) | Browse historically; not part of project |
Key Patterns Discovered
- Mode = view filter, not different game. Vanilla and blind share one
chess.jsinstance; the difference is whatbuildView()returns to each player. - Moderator vocabulary is an enum, never a string. Display text lives client-side. Tests assert against
ModeratorTextvalues like'wont_help'. - Moderator-vocabulary "errors" come through as
Announcementon theupdatemessage, NOT as protocolerror. They're game events.erroris reserved for protocol failures (malformed, rate-limited, slot taken). - Highlights in blind+ON are purely geometric. Function of
(piece type, position, own-piece set). Rays extend through unseen opponents. Stop at own pieces. Off-board excluded. Zero opponent info leak. - Touch-move FSM is server-side.
game.armed: { color, from }is the entire state.no_legal_movesandwont_helpchecks fire only on first commit with a piece; once committed, all subsequent failed attempts areillegal_movewith the touch staying. - Single
commit { from, to? }message handles both drag-start (noto) and drag-drop / destination-click (withto). Uniform server FSM through one handler.
Tasks Finished
- Read kickoff handoff and IDEA.md
- Offered visual companion, Seth accepted (URL was http://steel141.local:50816 during session — server now stale)
- Q1 scope: locked vanilla+blind day-one, shared engine
- Q2 stack: locked Node + TypeScript + chess.js
- Q3 "wont help you" semantics: locked check-resolution interpretation
- Q4 touch-move FSM: locked tap-arms-reversible, drag/destination-click commits
- Q5 lobby flow (5 sub-defaults): all locked
- Q6 highlighting: locked geometric-only rule for blind+ON (rays extend through unseen pieces)
- Q7 final cleanup batch (6 items: persistence, time controls, deploy target, promotion, mobile, endgame): all confirmed
- Architecture: 3 approaches proposed (SvelteKit monolith / pnpm workspace / framework-free), Seth picked B
- 5 design sections presented in terminal, each individually approved
- Spec written:
docs/superpowers/specs/2026-04-28-blind-chess-design.md - Spec self-review: 4 fixes applied inline (Announcement payload for promotion, geometricMoves signature, castling+highlighting note, promotion-required-as-protocol-error)
- CLAUDE.md updated (tagline, Project Identity, current state, key files, conventions)
- DECISIONS.md updated (16 settled decisions, 12 deferred/rejected)
Files Modified
| File | Changes | Rationale |
|---|---|---|
| CLAUDE.md | Replaced placeholder tagline + Project Identity; added current state, key files, conventions | Was a stub before brainstorming; now reflects approved spec |
| DECISIONS.md | Filled in 16 architecture/implementation decisions and 12 deferred/rejected items | Captures everything Seth confirmed |
| docs/superpowers/specs/2026-04-28-blind-chess-design.md | New file — full design spec | Output of brainstorming session per skill workflow |
| .gitignore | New file — excludes .superpowers/ and .backup/ |
.superpowers/ is the visual-companion working directory |
Decisions Made
See the full list in DECISIONS.md (16 architecture + implementation, 12 deferred/rejected). Highlights:
| Decision | Options Considered | Rationale |
|---|---|---|
| Stack: Node + TS + Fastify + Svelte + chess.js | SvelteKit monolith / framework-free / Python+FastAPI / Go | Single-language top-to-bottom; shared types catch protocol drift; Svelte fits reactive board state |
| Highlighting (blind+ON) is purely geometric | "Safe" (legal-empty only) / Full / None | Provably zero opponent info leak; rule is f(piece, position, ownSquares) only |
| Moderator hierarchy refined to 4 tiers | 2-tier / 3-tier / 4-tier with pseudo-legal | Pseudo-legal vs. legal distinction lets wont_help reveal "king is the problem" without revealing why |
Announcement is enum, not string |
Free-form strings / structured enum | Tests assert on enum, i18n trivial via translation table |
| In-memory state only | SQLite from day one / in-memory + later SQLite | Hobby-scale; restart drops games is acceptable; SQLite is a 1-day add later |
Single commit message for arm + move |
Two messages (arm + move) / three / one | Uniform server FSM; one handler covers all transitions |
| Castling NOT highlighted in blind+ON | Highlight always / partial highlight / never | Castling legality depends on opponent state; partial reveal would leak; player can still execute manually |
Immediate Next Steps
-
Have Seth review the written spec. It's at
docs/superpowers/specs/2026-04-28-blind-chess-design.md. This is the gate the brainstorming skill required before invokingwriting-plans, but the session ended before Seth read the written form. Ask: "I've got the spec written at<path>. Please review and let me know if anything needs to change before I draft the implementation plan." If Seth requests changes, edit inline and re-ask. -
Propose Gitea repo creation, with Seth's OK. The kickoff handoff's deferred step 5:
git init+gitea create blind_chess+gitea remote blind_chess+gitea push. The session ended before this happened. Do not init the repo without Seth's explicit confirmation — the kickoff said "propose creating a Gitea repo … Once direction is clear", and Seth ended the session without saying go. Use thegiteaCLI from~/bin/gitea. -
Invoke the
superpowers:writing-plansskill. This is the terminal state of the brainstorming process. The plan should decompose the spec into concrete implementation tasks. Skill location: per CLAUDE.md plugin list.
Blockers/Open Questions
- Spec review by Seth not yet done. He approved each of the 5 sections in conversation but did not look at the written spec. Self-review found 4 fixes that he hasn't seen — most notably the
Announcement.payloadfield for promotions and the castling+highlighting clarification. - No Gitea repo yet. Project is on disk only. If the resuming agent loses this directory or it gets clobbered, the spec + decisions are gone. Backup:
.backup/is gitignored but currently empty (no edits to backup).
Deferred Items
All deferred/rejected items are in DECISIONS.md "Deferred / Rejected" section. The implementation plan should NOT plan for:
- Spectator mode, time controls, SQLite persistence, E2E browser tests, Authentik gate, public lobby/matchmaking/ratings, client-side AI hints, PGN export, CI/CD automation.
- Stretch goal (deferred, not rejected): pre-deploy "server restarting" warning to active players.
Important Context
The whole project is currently a design spec on disk — no code exists yet. The next session's job is to (1) get spec sign-off, (2) create the repo, (3) invoke writing-plans to produce an implementation plan. Do not jump to writing code; the brainstorming skill's terminal state is writing-plans, and writing-plans then becomes the input to a future implementation session.
Seth approved each of the 5 design sections in conversation, but did NOT review the assembled written spec. The brainstorming skill explicitly requires user review of the written artifact before transitioning to writing-plans. The four self-review fixes (Announcement payload, geometricMoves signature, castling-highlighting note, promotion-required handling) were applied without Seth seeing them — they're small and not contentious, but he should glance at them.
The visual companion server (port 50816, http://steel141.local:50816) is no longer running. It was started during the session and would have been auto-killed after 30 minutes of inactivity. Three mockup HTMLs are persisted in .superpowers/brainstorm/2734300-1777384084/content/: welcome.html, highlighting.html (v1, superseded), highlighting-v2.html (the corrected geometric rule, the one Seth approved), waiting.html. Don't restart the server unless the next session does more visual work.
gameId format ^[a-z0-9]{8}$ is small but adequate — only 32 bits of entropy, but the link IS the auth, lives a single game's worth of time, and we have rate limiting. If at any point the link becomes long-lived (e.g., post-game replay URLs), bump to 12+ chars.
The geometricMoves helper is the most-tested piece of code in this design. Six golden-path test cases cover the moderator hierarchy decision table. Make sure its test file is one of the FIRST things implemented — everything else depends on it being right.
Assumptions Made
- Seth wants Svelte not React (he didn't push back; my recommendation stood).
- Seth wants Fastify not Express (same — accepted by silence).
- Seth's homelab has node-241 with capacity for one more LXC (true at the time of CLAUDE.md last-update; verify with
pct liston node-241 if uncertain). - "Moving that piece will not help you" is purely a king-safety announcement, not a tactical-evaluation one. Seth confirmed explicitly during the session.
- 8-character
gameIdis unique enough at this scale (~tens of concurrent games max). Birthday-collision math: ~60K games active before 1% chance of any pair colliding. Way more than this project will see.
Potential Gotchas
- chess.js does not expose pseudo-legal moves directly. Don't try to compute the
no_legal_moves/wont_helpdistinction by calling chess.js'smoves()alone — that returns legal moves only. You need both the geometric set and the legal set. The spec'sgeometricMoveshelper is the missing piece. - chess.js
inCheck()checks the side-to-move. After a move is applied, the side-to-move is the opponent. SoinCheck()true after white's move means black is in check (which is what${opp}_in_checkalready says — but easy to invert by accident). Announcement.payload.promotedTowas added in self-review. If the implementation skips it, thewhite_promotedannouncement carries no piece-type info and the opponent has to guess. Don't drop it.- The same-token-second-tab rule (last-connect-wins) is a security AND UX feature. Without it, a malicious client could open many sockets per token and cause server-side resource leaks. Don't be tempted to "be more permissive" here.
- Castling is NOT in
geometricMoves. This is intentional. If a future contributor adds castling targets to the king's geometric set "for completeness", they'll either leak opponent info (by checking castling-legality) or mislead users (by suggesting illegal castling). Comment in the helper explaining this. - The visual companion's
.superpowers/dir is gitignored. If you want to share mockups (e.g., commit them for posterity), copy them todocs/first. - Files referenced in this handoff that don't exist yet:
deploy/Caddyfile.snippetand the test files (e.g.,packages/server/test/unit/geometric.test.ts) are forward references — they'll be created during implementation. Don't be alarmed when the validator flags them as missing.
Environment State
Tools/Services Used
- chess.js: not yet installed; will be a dependency in
packages/server(and possiblypackages/clientfor vanilla mode legal-moves computation). Pin viapackage.jsononce init'd. - gitea CLI:
~/bin/gitea. Used for repo create + push. Token at~/.config/gitea/token. Documented in~/bin/CLAUDE.md. - pnpm: required for workspace. Version 9+ recommended.
- Node 22 LTS: deployment target. Local dev should match.
- Caddy CT 600 (192.168.0.185): existing. Add a
chess.sethpc.xyzblock to the Caddyfile when deploying. Spec includes adeploy/Caddyfile.snippetto be created during implementation.
Active Processes
- None. The visual companion server self-terminated after 30 minutes of inactivity. No long-running tasks were started.
Environment Variables
HOMELAB_PASSWORD: set globally, used for SSH/SMB/etc. Not needed for blind_chess work directly.- (No project-specific env vars yet.)
Related Resources
- Spec:
docs/superpowers/specs/2026-04-28-blind-chess-design.md - Project identity:
CLAUDE.md - Decisions:
DECISIONS.md - Original brief:
IDEA.md - Kickoff handoff:
.claude/handoffs/2026-04-28-kickoff.md - Visual companion mockups (gitignored):
.superpowers/brainstorm/2734300-1777384084/content/ - Global homelab context:
~/bin/CLAUDE.md - Project scaffold recipe:
~/bin/CREATE_PROJECT.md - Gitea CLI:
~/bin/gitea - chess.js (intended dep): https://github.com/jhlywa/chess.js
Security Reminder: Before finalizing, run validate_handoff.py to check for accidental secret exposure.