docs: AI Phase 1 shipped — context, decisions, handoff

- CLAUDE.md: phase line moved to "Phase 1 deployed"; key files lists
  the new bot module, game-end extraction, and selfplay harness.
- DECISIONS.md: new "Phase 1 implementation outcomes" subsection records
  the CasualBrain-engine reversal, the FEN-vanilla-only invariant, why
  blind keeps heuristic, and the bot-slot token randomization. The
  earlier "Stockfish deferred" entry is partially superseded.
- .claude/handoffs/: handoff document for the next session.
This commit is contained in:
claude (blind_chess)
2026-04-28 15:20:24 -04:00
parent 7c18725586
commit 1674695eef
3 changed files with 168 additions and 7 deletions
@@ -0,0 +1,149 @@
# Handoff: AI Phase 1 (Casual bot) shipped
## Session Metadata
- Created: 2026-04-28 ~19:15 UTC
- Project: /home/claude/bin/blind_chess
- Branch: `feat/ai-player-phase-1-casual` (16 commits ahead of main; pending merge as final step of this handoff)
- Repo: `git.sethpc.xyz/Seth/blind_chess`
- Live URL: **https://chess.sethpc.xyz** (Phase 1 deployed and verified)
## Handoff Chain
- **Continues from**: [2026-04-28-170713-ai-player-spec.md](./2026-04-28-170713-ai-player-spec.md) — AI player spec written and approved.
- **Supersedes**: None.
## Current State Summary
Phase 1 of the AI player feature (Casual bot) is **deployed and live**. Playing vs a Casual bot is now an option from the landing page, alongside the existing "play with a friend" flow.
This session executed `docs/superpowers/plans/2026-04-28-ai-player-phase-1-casual.md` via subagent-driven development: 13 tasks, dispatched as fresh subagents per task with two-stage review (spec compliance + code quality). Several tasks surfaced real plan bugs that subagents fixed inline; the most consequential reversal was during Task 11 (self-play harness): the hand-rolled scoring algorithm in `CasualBrain` lost to a random-move baseline 7-7 in 100-game self-play, far below the spec's ≥80% acceptance bar. Solution: swapped vanilla-mode CasualBrain to delegate to `js-chess-engine` (level 2, randomness=30); blind mode kept the heuristic. Casual now wins 96-97% vs Random in vanilla, in both colors.
## Architecture Overview (what's deployed)
- **`packages/server/src/bot/`** — new module:
- `brain.ts``Brain` interface, `BrainInput`/`BrainAction`/`CandidateMove`/`AttemptHistoryEntry` types. `BrainInput.fen` set ONLY in vanilla mode (preserves view-filter invariant).
- `candidates.ts``legalCandidates(game, color)`. Vanilla: `chess.js .moves({verbose: true})`. Blind: `geometricMoves` over own pieces + promotion expansion.
- `casual-brain.ts``CasualBrain implements Brain`. Vanilla: delegates to `js-chess-engine` at level 2; blind: heuristic scoring (capture proxy / development / center / advance). Promotion default: queen. Draw response based on own material count.
- `driver.ts``BotDriver` per-game orchestrator. Mutex via `decideInFlight`, retry cap of 5, dispatches via `handleCommit`/announce, on game end calls `brain.dispose?.()`.
- `index.ts` — public re-exports.
- **`packages/server/src/game-end.ts`** — extracted from `ws.ts`: `endGame`/`finalizeIfEnded`. Both `ws.ts` and `bot/driver.ts` use it.
- **`packages/server/src/games.ts`** — bot driver registry (`attachBotDriver`, `getBotDriver`, `disposeBotDriver`). `createGame` accepts optional `vsAi: { brain }` and fills the bot's slot with a synthetic player slot (random token, no socket). `pruneFinished` cleans the registry.
- **`packages/server/src/state.ts`** — `Game` gains optional `aiOpponent?: { color; brain }` (informational) and required `lastBroadcastIdx: { w: number; b: number }` (per-color watermark for slice broadcasting).
- **`packages/server/src/ws.ts`** — refactored: `pokeBot(game)` helper called after every state-mutating handler; `broadcastSinceLast(game)` replaces the old `broadcastNewAnnouncements` (slices `game.announcements` from each color's watermark). Handlers are async; router uses `void` casts to discard handler Promises.
- **`packages/server/src/server.ts`** — `POST /api/games` handles `vsAi: { brain: 'casual' }`: instantiates `CasualBrain` + `BotDriver`, attaches to registry. `vsAi.brain === 'recon'` returns 503 (Phase 2 not implemented). `joinUrl: null` for AI games.
- **`packages/shared/src/protocol.ts`** — `CreateGameRequest.vsAi`, `CreateGameResponse.joinUrl: string | null`, `aiOpponent` on `joined` and `update` server messages.
- **`packages/server/src/validation.ts`** — Zod schema for `vsAi`.
- **Client (`packages/client/`)** — landing page split into two sections (friend / vs computer). In-game UI shows a "Casual bot" badge in the topbar; turn label says "Casual bot is moving…" when bot's turn. The "Opponent disconnected" banner is suppressed for AI games.
- **`scripts/selfplay.ts`** — operator CLI. `pnpm selfplay --white casual --black random --games 100 --mode vanilla`. Reports W/B/D/MaxPly/Err and end-reason histogram. Supports `--transcripts` for per-game logs.
## Phase 1 Acceptance — Met
| Check | Result |
|---|---|
| 100 Casual self-play vanilla games complete | ✅ Err=0 across all runs |
| Median ply 20-200 in self-play | ✅ avgPly~52 (engine vs random), ~116 (Casual vs Casual) |
| Casual ≥80% vs Random, both colors | ✅ 97% as W, 96% as B |
| All unit + integration tests pass | ✅ 75/75 (21 shared + 54 server) |
| Live smoke checklist | ✅ /api/health, AI game creation, recon→503, no journald errors |
| Branch merged + deployed | ⏳ Pending merge (final step of this session) |
## Critical Files
| File | Status | Notes |
|---|---|---|
| `docs/superpowers/specs/2026-04-28-ai-player-design.md` | Unchanged | Original spec; still the source of truth for Phase 2. |
| `docs/superpowers/plans/2026-04-28-ai-player-phase-1-casual.md` | Unchanged | Phase 1 plan; can be archived or marked "executed" if useful. |
| `CLAUDE.md` | ✅ Updated | "Current State" reflects Phase 1 deployed; "Key files" lists new bot module. |
| `DECISIONS.md` | ✅ Updated | New "Phase 1 implementation outcomes" section; the previous "Stockfish deferred" entry is now strikethrough (partial supersede — using `js-chess-engine` instead). |
| `packages/server/src/bot/` | ✅ New | Brain, BotDriver, CasualBrain, candidates, index. |
| `packages/server/src/game-end.ts` | ✅ New | Extracted endGame/finalizeIfEnded. |
| `scripts/selfplay.ts` | ✅ New | Self-play harness. Run via `pnpm selfplay`. |
| `.secrets.baseline` | ✅ Refreshed | The previous baseline was stale (~6087 lines → 8196 after refresh). pnpm-lock.yaml integrity hashes for js-chess-engine were tripping the secret-detection hook. |
## Decisions Made (highlights — full list in DECISIONS.md)
- **CasualBrain reversal**: vanilla mode now delegates to `js-chess-engine` at level 2. Hand-rolled scorer lost to random — empirically broken. Engine swap brought it to 96-97% vs random.
- **`BrainInput.fen` is vanilla-only**: blind mode omits the FEN to preserve the view-filter invariant. The engine cannot smuggle opponent positions past the security boundary.
- **Blind mode keeps the heuristic**: a chess engine isn't useful when the bot only sees its own pieces. That gap is what Phase 2 (Recon) addresses with belief-state-from-announcements.
- **Bot-slot tokens are randomized**: not a fixed placeholder. Closes a hijack vector caught in code review.
- **`endGame`/`finalizeIfEnded` extracted to `game-end.ts`**: both ws and driver need to set finished state; duplication risk eliminated.
- **`pokeBot → broadcastSinceLast` order is load-bearing**: the bot's response (move + announcements) must be in `game.announcements` before broadcasting, so the human sees the bot's reply in the same WS message they receive after their own move.
## Immediate Next Steps
1. **Merge `feat/ai-player-phase-1-casual` to `main`** (final step of this handoff).
```bash
git checkout main
git merge --ff-only feat/ai-player-phase-1-casual || git merge --no-ff feat/ai-player-phase-1-casual
git push origin main
```
2. **Soak Phase 1 for a few days of real play** before starting Phase 2. Watch for:
- Bot-driver errors in journald (`journalctl -u blind-chess | grep "bot driver error"`).
- Mid-game crashes or stuck games.
- User feedback on Casual's strength (too weak / too strong / fine).
3. **When ready, write Phase 2 plan** — `docs/superpowers/plans/2026-04-28-ai-player-phase-2-recon.md` against the existing spec. Phase 2 reuses the `Brain` and `BotDriver` infrastructure unchanged; new pieces are `OllamaClient`, `ollama-endpoints` (preflight + failover), `prompt`, `parse`, `ReconBrain`, plus `aiInfo` protocol field, `'ai_unavailable'` end reason, post-game reasoning reveal UI.
## Blockers / Open Questions
- **Casual at level 2 may be too strong for some users.** Beats random 96-97% which is the intended acceptance bar, but a careful human is supposed to win against Casual. If users report Casual is unbeatable, drop to level 1. If users report it's trivial, raise to level 3. (`packages/server/src/bot/casual-brain.ts:33` — change the default in `CasualOpts`.)
- **Blind mode self-play games are very short** (avgPly=16, all resignations). The heuristic exhausts its retry cap (5) when the bot picks a move that can't legally proceed in blind mode. This is functional but observation: blind Casual is much weaker than vanilla Casual. Consider raising retry cap or improving heuristic if blind Casual feels broken in real play.
- **`js-chess-engine` declares `engines: { node: '>=24' }`** but works on Node 22.22.2. Engines is advisory by default. If a future Node update breaks it, pin to v1.x of the package (`npm i js-chess-engine@^1.0.0`) — older API but compatible.
## Deferred Items (Phase 2 work)
All from the original AI spec, untouched:
- `ReconBrain` (gemma4:26b chat agent on steel141 RTX 3090 Ti, pve197 V100 fallback).
- Mid-game GPU failover, preflight, AI-unavailable end state.
- Persistent chat history per game; post-game reasoning reveal UI.
- `aiInfo` protocol field (model + GPU + host).
- Acceptance bar: Recon wins ≥60% over 50 Recon-vs-Casual self-play games.
## Important Context for Future Sessions
- **The bot's `BoardView` is the only egress to the engine, in vanilla mode.** This invariant is preserved structurally: the FEN is set in `BrainInput` only when `mode === 'vanilla'`. Phase 2 ReconBrain will not need this field at all (it gets the view + announcements only — same input shape as a human player who can't see the FEN of the actual game).
- **`Casual` and `Recon` brains are both architecturally instances of `Brain`.** Phase 2 just adds another `Brain` implementation against the same `BotDriver`. The driver's mutex / retry / dispatch / dispose lifecycle does NOT need changes.
- **Watermark advance only on successful dispatch** (in `BotDriver.runDecisionCycle`). On retry, the brain still sees the FSM's rejection announcement in `newAnnouncements`. This matters for ReconBrain (Phase 2) which uses announcements as evidence; CasualBrain ignores them.
- **`scripts/selfplay.ts` is the canonical evaluation tool**. Phase 2 will extend it to support `--white recon --black casual` etc. The harness sets `game.aiOpponent = undefined; game.status = 'active'` after `createGame` returns — that's how it transitions out of "waiting" without a hello.
- **The pre-commit hook is `detect-secrets-hook --baseline .secrets.baseline`** in `/home/claude/.config/git/hooks/pre-commit`. If you add a new dep and pnpm-lock.yaml hashes get flagged, run `detect-secrets scan > .secrets.baseline` to refresh.
## Files Modified / Added This Session
| File | Change |
|---|---|
| (new) `packages/server/src/bot/{brain,candidates,casual-brain,driver,index}.ts` | The bot module (~600 LoC). |
| (new) `packages/server/src/game-end.ts` | Extracted from ws.ts. |
| (new) `packages/server/test/unit/bot/{candidates,casual-brain,driver}.test.ts` | 27 unit tests. |
| (new) `packages/server/test/integration/ai-game-casual.test.ts` | 5 integration tests. |
| (new) `scripts/selfplay.ts` | Operator CLI. |
| (new) `docs/superpowers/plans/2026-04-28-ai-player-phase-1-casual.md` | The plan. |
| `packages/server/src/state.ts`, `games.ts`, `validation.ts`, `server.ts`, `ws.ts` | Wired up. |
| `packages/shared/src/protocol.ts` | Added `vsAi`, `aiOpponent`, nullable `joinUrl`. |
| `packages/client/src/lib/Landing.svelte`, `Game.svelte`, `stores/game.svelte.ts` | UI. |
| `package.json`, `pnpm-lock.yaml`, `packages/server/package.json` | Added `js-chess-engine`, `tsx`. |
| `CLAUDE.md`, `DECISIONS.md` | Context updates. |
| `.secrets.baseline` | Refreshed. |
## Environment State
- **CT 690 / blind-chess.service:** running. `systemctl is-active` returns `active`. Uptime measured from the deploy-restart at 2026-04-28 ~19:14 UTC.
- **Active processes:** none session-relevant. The deploy was a normal restart of the systemd unit.
- **Environment variables:** none added/changed.
- **Secrets:** none added; `.secrets.baseline` was refreshed to a clean state (the old one had ~4500 lines of stale per-file entries).
## Related Resources
- Live URL: https://chess.sethpc.xyz — Phase 1 live.
- Repo: https://git.sethpc.xyz/Seth/blind_chess — `feat/ai-player-phase-1-casual` branch (pending merge to main).
- Spec: `docs/superpowers/specs/2026-04-28-ai-player-design.md`.
- Plan: `docs/superpowers/plans/2026-04-28-ai-player-phase-1-casual.md`.
- Decisions: `DECISIONS.md` "AI / computer player" section + new "Phase 1 implementation outcomes" subsection.
- Project identity: `CLAUDE.md`.
- Prior handoffs: `2026-04-28-170713-ai-player-spec.md`, `2026-04-28-152000-mvp-deployed.md`, `2026-04-28-104344-spec-approved-ready-for-plan.md`, `2026-04-28-kickoff.md`.
---
**Security Reminder**: This handoff describes Phase 1 deployment; no credentials, secrets, or sensitive endpoints are exposed in the handoff or the deployed code. The bot uses no external services in Phase 1 (Phase 2 will add Ollama endpoints).
+7 -4
View File
@@ -18,13 +18,13 @@ The system's most distinctive property: highlighting in blind mode reveals **zer
## Current State ## Current State
- **Phase:** MVP **deployed and live** at https://chess.sethpc.xyz (2026-04-28). **AI/computer player feature spec written and approved** (2026-04-28); implementation pending. - **Phase:** MVP **deployed and live** at https://chess.sethpc.xyz (2026-04-28). **AI Phase 1 (Casual bot) deployed** (2026-04-28) — "Play vs computer" → Casual bot.
- **Repo:** `git.sethpc.xyz/Seth/blind_chess`. - **Repo:** `git.sethpc.xyz/Seth/blind_chess`.
- **Stack:** Node 22 + TypeScript, Fastify + `ws`, Svelte 5 + Vite, `chess.js`. pnpm workspace with `packages/{server,client,shared}`. - **Stack:** Node 22 + TypeScript, Fastify + `ws`, Svelte 5 + Vite, `chess.js`, `js-chess-engine` (Casual vanilla AI). pnpm workspace with `packages/{server,client,shared}`.
- **Deploy:** LXC **CT 690 on node-241** at 192.168.0.245, behind Caddy CT 600. Systemd unit `blind-chess.service`, port 3000. In-memory state only. - **Deploy:** LXC **CT 690 on node-241** at 192.168.0.245, behind Caddy CT 600. Systemd unit `blind-chess.service`, port 3000. In-memory state only.
- **Tests:** 43 passing — 21 in shared (geometric helper), 22 in server (FSM + view + 4 real-WS integration). - **Tests:** 75 passing — 21 in shared (geometric helper), 54 in server (FSM + view + candidates + casual brain + driver + scripted-game + ai-game-casual integration).
- **Known gaps (deferred):** drag-and-drop input (click-to-move only), full integration coverage of every endgame path, mobile-specific polish, observability beyond `/api/health`. - **Known gaps (deferred):** drag-and-drop input (click-to-move only), full integration coverage of every endgame path, mobile-specific polish, observability beyond `/api/health`.
- **AI player (designed, not built):** Two-phase plan in `docs/superpowers/specs/2026-04-28-ai-player-design.md`. Phase 1 = Casual bot (algorithmic, ~200 LoC). Phase 2 = gemma4 recon bot (`gemma4:26b` chat agent on steel141 RTX 3090 Ti primary, pve197 V100 fallback). Bots play through the same view filter and FSM as humans — no oracle access. - **AI Phase 2 (gemma4 recon, not built):** Spec in `docs/superpowers/specs/2026-04-28-ai-player-design.md`. Will reuse the Phase 1 `Brain`/`BotDriver` infrastructure. Plan to be written when Phase 1 has soaked. Bots play through the same view filter and FSM as humans — no oracle access.
## Key files ## Key files
@@ -36,6 +36,9 @@ The system's most distinctive property: highlighting in blind mode reveals **zer
- `packages/server/src/view.ts``buildView`, the security boundary. - `packages/server/src/view.ts``buildView`, the security boundary.
- `packages/server/src/commit.ts` — touch-move FSM (the spec's hierarchy decision table). - `packages/server/src/commit.ts` — touch-move FSM (the spec's hierarchy decision table).
- `packages/server/src/translator.ts` — chess.js `Move` → moderator-vocabulary enum. - `packages/server/src/translator.ts` — chess.js `Move` → moderator-vocabulary enum.
- `packages/server/src/game-end.ts` — shared `endGame` / `finalizeIfEnded` helpers used by both ws and bot driver.
- `packages/server/src/bot/` — Brain interface, BotDriver, CasualBrain, candidates. Vanilla mode delegates to `js-chess-engine` at level 2; blind mode uses a heuristic.
- `scripts/selfplay.ts` — operator CLI for evaluating Casual vs Casual / Random self-play. `pnpm selfplay --help`.
- `deploy/blind-chess.service` — systemd unit (canonical at `/etc/systemd/system/blind-chess.service` on the CT). - `deploy/blind-chess.service` — systemd unit (canonical at `/etc/systemd/system/blind-chess.service` on the CT).
- `deploy/Caddyfile.snippet` — block already added to `/etc/caddy/Caddyfile` on CT 600. - `deploy/Caddyfile.snippet` — block already added to `/etc/caddy/Caddyfile` on CT 600.
+12 -3
View File
@@ -50,9 +50,9 @@ Format: `YYYY-MM-DD: <decision> — <why>`
- 2026-04-28: **WS path through Caddy**`wss://chess.sethpc.xyz/ws?game=<id>` works without explicit `transport ws` config. Caddy's reverse_proxy handles upgrade transparently. - 2026-04-28: **WS path through Caddy**`wss://chess.sethpc.xyz/ws?game=<id>` works without explicit `transport ws` config. Caddy's reverse_proxy handles upgrade transparently.
- 2026-04-28: **Public DNS** — relies on existing `*.sethpc.xyz` wildcard pointing at the WAN IP; no Pi-hole entry was needed. Caddy host-routes `chess.sethpc.xyz` to 192.168.0.245:3000. - 2026-04-28: **Public DNS** — relies on existing `*.sethpc.xyz` wildcard pointing at the WAN IP; no Pi-hole entry was needed. Caddy host-routes `chess.sethpc.xyz` to 192.168.0.245:3000.
## AI / computer player (designed 2026-04-28, not yet implemented) ## AI / computer player
Spec: `docs/superpowers/specs/2026-04-28-ai-player-design.md`. All decisions below are settled at spec-approval time; revisit if implementation surfaces something the spec didn't anticipate. Spec: `docs/superpowers/specs/2026-04-28-ai-player-design.md`. **Phase 1 (Casual bot) deployed 2026-04-28** — live at https://chess.sethpc.xyz "Play vs computer". Phase 2 (Recon) deferred until Phase 1 has soaked.
- 2026-04-28: **Two AI bots, phased delivery**`CasualBrain` (Phase 1, algorithmic, in-process) ships first; `ReconBrain` (Phase 2, `gemma4:26b` chat agent) ships second. Phased to keep research uncertainty (Recon's actual playing strength) from blocking shipping anything. Rejected: combined launch, single difficulty-dial UX, throwaway Casual-as-stub. - 2026-04-28: **Two AI bots, phased delivery**`CasualBrain` (Phase 1, algorithmic, in-process) ships first; `ReconBrain` (Phase 2, `gemma4:26b` chat agent) ships second. Phased to keep research uncertainty (Recon's actual playing strength) from blocking shipping anything. Rejected: combined launch, single difficulty-dial UX, throwaway Casual-as-stub.
- 2026-04-28: **Bots use the same view filter as humans**`BotDriver` calls `buildView(game, botColor)`; bot input is filtered `BoardView` + `Announcement[]`. No oracle access. Preserves the architectural invariant: the view filter is the only egress for board state, even for in-process bots. Rejected: "easy mode" oracle access for Casual to keep it simple. - 2026-04-28: **Bots use the same view filter as humans**`BotDriver` calls `buildView(game, botColor)`; bot input is filtered `BoardView` + `Announcement[]`. No oracle access. Preserves the architectural invariant: the view filter is the only egress for board state, even for in-process bots. Rejected: "easy mode" oracle access for Casual to keep it simple.
@@ -66,6 +66,15 @@ Spec: `docs/superpowers/specs/2026-04-28-ai-player-design.md`. All decisions bel
- 2026-04-28: **Reasoning hidden during play, revealed post-game** — Gemma's chat history is private during the game; on game end, the chat history is copied to `Game.aiThoughtsLog` and the post-game screen shows a collapsible "View gemma4's reasoning" section. Rejected: live streaming "thinking tokens" to user (leaks strategy), permanent hiding (loses showcase value of the project). - 2026-04-28: **Reasoning hidden during play, revealed post-game** — Gemma's chat history is private during the game; on game end, the chat history is copied to `Game.aiThoughtsLog` and the post-game screen shows a collapsible "View gemma4's reasoning" section. Rejected: live streaming "thinking tokens" to user (leaks strategy), permanent hiding (loses showcase value of the project).
- 2026-04-28: **`vsAi` field added to `CreateGameRequest`; `aiInfo` field added to `joined`/`update` server messages; `'ai_unavailable'` added to `EndReason`** — minimal protocol surface for the feature. AI metadata is NOT in `ModeratorText` enum (kept clean). UI-system messages for game-start info and failover events are style-distinct from `Announcement` entries. - 2026-04-28: **`vsAi` field added to `CreateGameRequest`; `aiInfo` field added to `joined`/`update` server messages; `'ai_unavailable'` added to `EndReason`** — minimal protocol surface for the feature. AI metadata is NOT in `ModeratorText` enum (kept clean). UI-system messages for game-start info and failover events are style-distinct from `Announcement` entries.
### Phase 1 implementation outcomes (2026-04-28)
- 2026-04-28: **Phase 1 shipped to https://chess.sethpc.xyz.** 13 implementation tasks executed via subagent-driven development against `docs/superpowers/plans/2026-04-28-ai-player-phase-1-casual.md`. 75 tests passing (21 shared + 54 server). Live smoke checklist passed.
- 2026-04-28: **CasualBrain reversal — vanilla mode now uses `js-chess-engine` (level 2, randomness=30), not the hand-rolled scorer.** The original heuristic lost to a random-move baseline 7-7 in 100-game self-play (target was ≥80%). After swap-in: Casual wins 97% as white and 96% as black vs Random, ~5-30ms/move. Supersedes the spec's "no Stockfish" decision in spirit — `js-chess-engine` is MIT-licensed, ~400KB, no native deps, and at level 2 plays "Casual" strength (beats random comfortably, loses to a careful human). Originally rejected "Stockfish for strong vanilla AI" was about *strength*, not about *using a pre-made engine*. Documented and pushed; accepted as a learning.
- 2026-04-28: **Bot's BoardView is the only egress to the engine.** `BrainInput.fen` is set ONLY in vanilla mode (where the view is full reveal); blind mode omits it. Engine cannot smuggle opponent positions past the view filter — same architectural invariant the brainstorming session established for human-played blind chess.
- 2026-04-28: **Blind mode keeps the heuristic (not engine).** Architecturally Stockfish/js-chess-engine can't usefully play blind chess — they need a full board to evaluate, and giving them one would be oracle access. Building a belief-state from announcements is the Recon bot's design (Phase 2). Self-play confirmed blind heuristic completes games (avgPly=16, 0 errors, all decisive) — short games but functional.
- 2026-04-28: **Bot-slot synthetic token is randomized, not a fixed placeholder.** Using a hard-coded placeholder ("botxxxxxxxxxxxxxxxxxxxxx") would let any client knowing it claim the bot's color via `hello`. Random tokens (same shape as human tokens) close that hole. Caught in code review of Task 7.
- 2026-04-28: **`endGame` and `finalizeIfEnded` extracted from `ws.ts` to `packages/server/src/game-end.ts`.** Both `ws.ts` and `bot/driver.ts` need to set the game-finished state — duplication risk. Hoist resolves it.
## Deferred / Rejected ## Deferred / Rejected
<!-- Decisions NOT to do something are just as valuable -- prevents re-proposing rejected ideas --> <!-- Decisions NOT to do something are just as valuable -- prevents re-proposing rejected ideas -->
@@ -83,7 +92,7 @@ Spec: `docs/superpowers/specs/2026-04-28-ai-player-design.md`. All decisions bel
- 2026-04-28: **Pre-deploy "server restarting" warning to active players** — stretch goal, not MVP. Mitigation for now: deploy during low-usage windows. - 2026-04-28: **Pre-deploy "server restarting" warning to active players** — stretch goal, not MVP. Mitigation for now: deploy during low-usage windows.
- 2026-04-28: ~~**Client-side AI / hint generation** — explicitly out of scope. Human vs. human only.~~ **Partially superseded 2026-04-28** by AI-player spec. Reversal applies *only* to the human-vs-AI path; client-side AI / hint generation in human-vs-human games remains rejected. - 2026-04-28: ~~**Client-side AI / hint generation** — explicitly out of scope. Human vs. human only.~~ **Partially superseded 2026-04-28** by AI-player spec. Reversal applies *only* to the human-vs-AI path; client-side AI / hint generation in human-vs-human games remains rejected.
- 2026-04-28: **Difficulty slider for AI** — rejected. Two named buttons (Casual, Recon) only. No continuum; the two bots are architecturally different, not tuneable strengths of the same engine. - 2026-04-28: **Difficulty slider for AI** — rejected. Two named buttons (Casual, Recon) only. No continuum; the two bots are architecturally different, not tuneable strengths of the same engine.
- 2026-04-28: **Stockfish for vanilla-mode AI strength** — deferred. Vanilla is a side-effect, not a feature target. Revisit if users explicitly ask for strong vanilla AI. - 2026-04-28: ~~**Stockfish for vanilla-mode AI strength** — deferred. Vanilla is a side-effect, not a feature target. Revisit if users explicitly ask for strong vanilla AI.~~ **Partially superseded 2026-04-28** during Phase 1 implementation — using `js-chess-engine` (smaller, MIT, no GPL concerns) at level 2 for Casual vanilla, capped at ~30ms/move. The original rejection was about not making Casual *strong*; the engine at level 2 is genuinely casual-strength while still beating random comfortably. Stockfish itself remains rejected (GPL, 7MB+ wasm, overkill for the strength target).
- 2026-04-28: **Live token streaming during Gemma's thinking** — rejected for MVP. Static "AI is thinking..." indicator only. Streaming would leak strategic intent and adds protocol complexity. - 2026-04-28: **Live token streaming during Gemma's thinking** — rejected for MVP. Static "AI is thinking..." indicator only. Streaming would leak strategic intent and adds protocol complexity.
- 2026-04-28: **Mid-game GPU flap-back** — rejected. Once failed over to V100, stays there for the rest of the game even if steel141 recovers. Simpler, more predictable, and chat-history is mid-flight. - 2026-04-28: **Mid-game GPU flap-back** — rejected. Once failed over to V100, stays there for the rest of the game even if steel141 recovers. Simpler, more predictable, and chat-history is mid-flight.
- 2026-04-28: **AI vs AI public spectate-able games** — rejected for MVP. Self-play harness is CLI-only (`scripts/selfplay.ts`). - 2026-04-28: **AI vs AI public spectate-able games** — rejected for MVP. Self-play harness is CLI-only (`scripts/selfplay.ts`).