From 86f3d2dc0a0932375b29771b1d987b0f5686942f Mon Sep 17 00:00:00 2001 From: serversdown Date: Sun, 28 Jun 2026 03:54:31 +0000 Subject: [PATCH 01/39] docs: spec for poker message-type prompts + dumb capture Two-track poker interaction: dumb data capture (stack/buyin/cashout) that bypasses the LLM, plus a message-type classifier that injects type-specific prompt fragments (HAND/STATUS/MENTAL/LOG/CHAT) in place of the one broad poker card. Kills the false tilt-reads and the coaching-essay-on-every-turn behavior. Includes the iOS-PWA bottom safe-area fix and the 2nd input box from Brian's screenshot. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx --- .../2026-06-28-poker-mode-prompts-design.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md diff --git a/docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md b/docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md new file mode 100644 index 0000000..07056f5 --- /dev/null +++ b/docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md @@ -0,0 +1,115 @@ +# Poker mode: message-type-specific prompts + dumb capture + +- **Date:** 2026-06-28 +- **Status:** Draft for review +- **Scope:** Poker (`poker_cash`) mode only. This is the template; the same pattern will later extend to Build/Explore/Study/Decide in a separate pass. + +## Problem + +In poker mode Lyra routes correctly but her replies are generic — "stringing poker words together." Evidence from the 2026-06-27 (Meadows) and 2026-06-28 (Wheeling) sessions in chat `sess-dff2s91c`: + +- **Every turn is a coaching essay, including pure data.** A stack update (`Stack=$685`) draws 4–6 sentences of momentum filler ("keep that momentum rolling," "you've got this"). Bare facts don't need a brain. +- **She projects tilt/fatigue onto neutral facts.** "Table broke, it's 11:50pm" → three replies about "late-night fatigue-driven decisions… mental reset." Brian had to say *"I'm not upset… you seem to be reading me as tilted."* Cause: the `_route` mood nudge (`lyra/mind.py:328`) firing on non-mood messages. +- **She doesn't reason about bet intent.** On a hand where Brian flopped bottom set, turned a full house, value bet $40 and got 88 to fold, she said *"his fold… shows the power of representing something stronger… well executed."* That was a **value bet that got no value** — money left on the table — not a successful rep. She pattern-matched "bet → fold → good." +- **She never calls `analyze_spot`; she eyeballs.** The 77 multiway spot got *"a disciplined fold might have been the better path"* — pure hedge, no math, violating the persona's "never eyeball poker math" rule. +- **Even her best reply leaks bad poker logic.** The sharp Connie read included "limp-checking in position" — contradictory nonsense — proving "sounds sharp" ≠ "is correct." + +Root cause: one large per-turn card (`_CASH_CARD`, `lyra/modes.py:66`) describes **traits** ("be decisive," "be present"). The model satisfies trait language with safe, flattering abstraction. Pure-data turns shouldn't reach the model at all; judgment turns need concrete response contracts, not adjectives. + +## Goals + +1. Pure data entry (stack, buy-in, cash-out) never touches the LLM. +2. Conversational turns get a **message-type-specific** prompt instead of one broad card. +3. Kill the false tilt/fatigue reads. +4. On hands, reason about **bet intent** and lean on `analyze_spot` — flag missed value, value-owning, and sizing rather than reflexively praising. +5. Fix the iOS-PWA bottom safe-area gap surfaced in testing. + +## Non-goals + +- Rewriting the other five modes (separate pass). +- Changing the tool handlers themselves (`poker.log_stack`, etc. already exist and are LLM-independent). +- An LLM-based classifier in v1 (the design leaves the seam for it; v1 ships heuristics). +- Itemized buy-in history (buy-ins remain a single `buy_in_total`). + +## Architecture + +Split poker input by whether it needs judgment. + +- **Track 1 — dumb capture:** pure data → direct HTTP endpoint → tool handler. No prompt, no reply. +- **Track 2 — conversational:** anything needing a brain → a classifier tags the message type → a type-specific prompt fragment is injected in place of the giant card. +- **PWA fix:** correct the bottom safe-area inset. + +### Deliverable 1 — Track 1: dumb capture (no LLM) + +**Backend (new endpoints in `lyra/web/server.py`).** Today the *only* write path for a stack/buy-in is chat → tool-calling; no direct endpoint exists. Add: + +- `POST /session/stack` → `poker.log_stack(amount)` → returns updated `stack_state()`. Server stamps the time. +- `POST /session/buyin` → `poker.add_buyin(amount)` → returns new `buy_in_total`. +- Cash-out reuses the existing `PATCH /session/{id}` with `{cash_out}` (already recomputes `net`). + +Both resolve the live session via `poker._resolve(None)` and return `{"ok": ..., "stack": ...}`. On "no live session" return a clear error the UI can show. + +**Frontend — the 2nd input box (`lyra/web/static/index.html`).** A slim quick-capture row **below the message input, above the bottom nav icons**. Behavior: + +- Type a number (e.g. `685`) → `POST /session/stack` → on success, append a one-line confirmation to the **Live Log** (`$685 logged · 11:34pm`) and update the HUD. **No chat message, no LLM call, main message box untouched.** +- Scope: **stack only** in v1. Buy-in and cash-out live on the Session HUD widget below. +- Empty/non-numeric input is ignored (or shows inline hint). Decimal and `$`-prefixed input tolerated (`$685`, `685`). + +**Frontend — HUD widget (`lyra/web/static/session.html`).** In the Stack card (`session.html:280`), mirroring the existing `saveEdit()` PATCH pattern (`session.html:192`): a stack field (`POST /session/stack`), a buy-in field (`POST /session/buyin`), and a cash-out field (existing PATCH). Each updates the sparkline on success. + +### Deliverable 2 — Track 2: classifier + type fragments + +**Classifier (`lyra/poker_classify.py`, new).** A function `classify(message: str) -> str` returning one of `HAND | STATUS | MENTAL | LOG | CHAT`. v1 is heuristic; the signature is the swappable seam — an LLM/MI50 implementation can replace the body later with no other changes. + +Heuristic signals (first match wins, in priority order): + +1. **HAND** — card-token regex (`\b[2-9TJQKA][shdc]\b` appearing ≥2×), or position tokens (UTG/MP/HJ/CO/BTN/SB/BB/"button"/"hijack"), or street words (flop/turn/river) combined with betting verbs (bet/raise/call/fold/check/shove/limp). +2. **MENTAL** — first-person feeling: "I feel", "I'm tilted/steaming/fried/tired/frustrated/confident", "in my head", "mental", "leak", "on tilt". +3. **STATUS** — time/venue/logistics with no cards: "table broke", "new table", "waiting for a seat", "seat opened", clock times, venue names. +4. **LOG** — prose money/result that slipped past the 2nd box: "I'm at 350", "stack is", "out for", "cashed", "rebought" with a number. +5. **CHAT** — default fallback (questions, open talk). + +Ambiguous → `CHAT` (safe full-voice default). + +**Pipeline integration (`lyra/mind.py`).** Add a `_classify` step to `PIPELINE` (after `_route`, before `_compose`) that sets `ctx.msg_type` when `ctx.mode.key == "poker_cash"`. In `build_messages` (`mind.py:137`), when in poker mode, inject the **type fragment** in the seat currently held by `mode.card` (`mind.py:152`) instead of the whole `_CASH_CARD`. Non-poker modes are unchanged. + +**Fragment registry (new `lyra/poker_prompts.py`).** `POKER_FRAGMENTS: dict[str, str]` keyed by type. Each fragment is short and concrete (rules + response shape), not trait language. Persona guardrails ("never eyeball math," "don't invent reads") stay sourced from `personas/lyra.md` via `_persona_block`. + +Response contracts: + +- **HAND.** Steps, in order: (1) log it (`record_hand` preferred, or `log_hand`); (2) if the decision is close or equity matters, call `analyze_spot` — do **not** eyeball; (3) reason about **bet intent** — for each meaningful bet, name what it was for (value / bluff / protection) and whether it achieved that (a fold to a value bet = value lost — flag it; a call of a bluff = bluff failed); (4) name leaks plainly (value-owning, missed value, bad sizing); (5) one clear opinion. **No reflexive praise** — do not open with "nice hand." If a named villain is referenced, call `player_profile`/`get_villain_file` first; never invent a read. Talk in prose, not a listicle. Seed with 2 exemplars (below). +- **STATUS.** 1–2 sentences. Acknowledge, log a stack only if a bare number is present, then stop. **No coaching. No tilt/fatigue reads.** +- **MENTAL.** Full presence, real voice, an actual point of view. (This register already works — preserve it.) +- **LOG.** One line. `$317 logged.` Nothing else. +- **CHAT.** Full voice, a real opinion, no filler sign-offs. + +**HAND exemplars** (embedded in the fragment): + +> *Brian:* "Flopped bottom set with 22, turned a full house, bet $40 on the river, he folded 88." +> *Lyra:* "That's a value bet that got no value — 88 was never folding to a smaller stab. $40 into that pot was too much; ~$20–25 gets called by the over-pair and the worse two-pairs. The hand's a monster, the leak is the sizing." + +> *Brian:* "77 in a multiway pot, flop 4h5c3c, HJ bets 75, BTN shoves 74, I call. Turn/river check through, HJ wins with 64." +> *Lyra:* "Against a bet and a shove on 4-5-3 you're drawing thin — sets, two pair, and the made wheel are all ahead, and you block almost none of it. The stack-depth logic (he only had ~150 behind) is real, but that's a reason to fold and wait, not to call off light. This is the value-owning spot you flagged yourself." + +**Pipeline fixes:** + +- Remove/gate the `_route` mood nudge (`mind.py:328`) so a steady→"steady" / up→"hype" note is **not** appended on non-MENTAL messages. Mental-game register comes only from the MENTAL fragment. +- Trim the always-on mode-menu note (`mind.py:77`) inside poker mode — it dilutes every turn. + +### Deliverable 3 — PWA bottom safe-area fix + +The band below the nav icons is an iOS-PWA safe-area issue (likely `100vh` not accounting for `env(safe-area-inset-bottom)` / the home indicator). Fix the layout container and bottom nav in the web CSS so the app fills the viewport with the icons seated above the home indicator. Use the `building-ios-pwas` skill at implementation time. + +## Testing / verification + +- **Classifier unit tests:** feed the real user turns from both transcripts and assert the expected type (e.g. "Stack is at $373" → LOG/handled by box, "Button straddle… I limp UTG with 22…" → HAND, "table broke, it's 11:50pm" → STATUS, "I feel like I'm being mean when I raise" → MENTAL). +- **Endpoint tests:** `POST /session/stack` / `/session/buyin` against a live test session; assert the row is written, time stamped, and `stack_state()` updates; assert a clean error when no live session. +- **Transcript replay (eyeball eval):** run yesterday's + today's user turns through the new classifier + fragments and compare replies to the logged mush — confirm stack updates produce no essay, the $40 hand is flagged as missed value, and STATUS turns carry no tilt-reading. +- **PWA:** verify on the iOS PWA that the gap is gone and the new input box sits above the nav with the keyboard open. + +## Out of scope / future + +- LLM classifier behind the same interface (if heuristics prove brittle). +- Extending message-type fragments to the other five modes. +- Itemized buy-in history / per-table stack series. +- Buy-in/cash-out in the chat-page 2nd box (HUD only for v1). From 5c63175a3cce5267a5e5607b9ad070ed16b9018d Mon Sep 17 00:00:00 2001 From: serversdown Date: Sun, 28 Jun 2026 20:59:52 +0000 Subject: [PATCH 02/39] =?UTF-8?q?docs:=20restructure=20spec=20=E2=80=94=20?= =?UTF-8?q?poker=20logging=20service=20first,=20then=20Lyra=20wiring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decompose into two sub-projects: (1) harden poker.py into a standalone logging service with a complete REST API, a first-class documented tool/API contract, and a human UI to log/edit/correct everything — usable by Brian alone, zero LLM dependency; (2) wire Lyra in as a client (classifier + message-type prompts), parked. Contract is first-class because it's the shared seam for the cloud model, a fine-tuned MI50 poker model, RTO, the human UI, and a future MCP wrap. MCP deferred until a second host app exists. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx --- .../2026-06-28-poker-mode-prompts-design.md | 203 +++++++++++------- 1 file changed, 123 insertions(+), 80 deletions(-) diff --git a/docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md b/docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md index 07056f5..8d71f94 100644 --- a/docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md +++ b/docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md @@ -1,115 +1,158 @@ -# Poker mode: message-type-specific prompts + dumb capture +# Poker logging service + message-type prompts - **Date:** 2026-06-28 -- **Status:** Draft for review -- **Scope:** Poker (`poker_cash`) mode only. This is the template; the same pattern will later extend to Build/Explore/Study/Decide in a separate pass. +- **Status:** Sub-project 1 spec ready for review; sub-project 2 parked. +- **Branch:** `feat/poker-mode-prompts` -## Problem +## Origin -In poker mode Lyra routes correctly but her replies are generic — "stringing poker words together." Evidence from the 2026-06-27 (Meadows) and 2026-06-28 (Wheeling) sessions in chat `sess-dff2s91c`: +This started as "make Lyra's poker replies less generic" (message-type-specific prompts). During design we decided to **build the logging tool first** as a standalone system of record with a clean API and a human-usable UI, then wire Lyra in as a *client* of it. Rationale: -- **Every turn is a coaching essay, including pure data.** A stack update (`Stack=$685`) draws 4–6 sentences of momentum filler ("keep that momentum rolling," "you've got this"). Bare facts don't need a brain. -- **She projects tilt/fatigue onto neutral facts.** "Table broke, it's 11:50pm" → three replies about "late-night fatigue-driven decisions… mental reset." Brian had to say *"I'm not upset… you seem to be reading me as tilted."* Cause: the `_route` mood nudge (`lyra/mind.py:328`) firing on non-mood messages. -- **She doesn't reason about bet intent.** On a hand where Brian flopped bottom set, turned a full house, value bet $40 and got 88 to fold, she said *"his fold… shows the power of representing something stronger… well executed."* That was a **value bet that got no value** — money left on the table — not a successful rep. She pattern-matched "bet → fold → good." -- **She never calls `analyze_spot`; she eyeballs.** The 77 multiway spot got *"a disciplined fold might have been the better path"* — pure hedge, no math, violating the persona's "never eyeball poker math" rule. -- **Even her best reply leaks bad poker logic.** The sharp Connie read included "limp-checking in position" — contradictory nonsense — proving "sounds sharp" ≠ "is correct." +- Brian can log and **correct** data himself, independent of whether Lyra parsed it right (she mislabeled "Dave the rock" vs "Dave the mechanic" mid-session). +- The data stops being hostage to the agent. Lyra becomes one client among potentially several. +- It's reusable: RTO (the solver) and a **fine-tuned poker model on the MI50** could consume the same hand/session data through the same contract. -Root cause: one large per-turn card (`_CASH_CARD`, `lyra/modes.py:66`) describes **traits** ("be decisive," "be present"). The model satisfies trait language with safe, flattering abstraction. Pure-data turns shouldn't reach the model at all; judgment turns need concrete response contracts, not adjectives. +## Decomposition + +Two sub-projects, built and shipped in order. + +### Sub-project 1 — Poker logging service *(this spec)* +Harden `lyra/poker.py` into a well-bounded store, expose a **complete REST API** over it, define a **stable, documented tool/API contract**, and build the human UI to log/edit/correct everything. Fully usable by Brian alone, zero LLM dependency. + +### Sub-project 2 — Lyra wiring *(parked; summarized at the end)* +Message-type classifier + type-specific prompt fragments; Lyra's tools call the sub-project 1 service. Separately, enabling tool-calling on the MI50 backend so a fine-tuned poker model can drive the same contract. + +**Why the contract is first-class:** in every design (in-process, REST, MCP) the *model* never calls the API directly — it emits a tool-call and the host app executes it. So what lets the cloud model, the MI50 fine-tune, RTO, and a human UI all interoperate is a single **stable tool/API schema** (operation names + JSON arg schemas). That contract is the training target for the fine-tune and the seam for every backend. MCP is deferred: it's a thin wrap over the same service, worth adding only when a *second host application* appears. + +--- + +# Sub-project 1 — Poker logging service ## Goals -1. Pure data entry (stack, buy-in, cash-out) never touches the LLM. -2. Conversational turns get a **message-type-specific** prompt instead of one broad card. -3. Kill the false tilt/fatigue reads. -4. On hands, reason about **bet intent** and lean on `analyze_spot` — flag missed value, value-owning, and sizing rather than reflexively praising. -5. Fix the iOS-PWA bottom safe-area gap surfaced in testing. +1. A complete API surface over the poker data model — create/read/update/delete for every entity, not just the few edit/delete endpoints exposed today. +2. A single **documented, versioned tool/API contract** that the REST API, Lyra's LLM tools, the human UI, a future MCP wrapper, and the MI50 fine-tune all share. +3. A human UI to **log** (fast capture) and **edit/correct** (fix Lyra's mistakes) every entity. +4. The 2nd input box (stack quick-capture) and the iOS-PWA bottom safe-area fix. +5. Pure data capture never touches the LLM. ## Non-goals -- Rewriting the other five modes (separate pass). -- Changing the tool handlers themselves (`poker.log_stack`, etc. already exist and are LLM-independent). -- An LLM-based classifier in v1 (the design leaves the seam for it; v1 ships heuristics). -- Itemized buy-in history (buy-ins remain a single `buy_in_total`). +- Lyra's classifier / prompt fragments (sub-project 2). +- Enabling tools on the MI50 backend (sub-project 2). +- MCP wrapper (deferred until a second host app exists). +- Itemized buy-in history — buy-ins stay a single `buy_in_total` scalar. +- Rewriting the SQLite schema; we build on the existing tables. -## Architecture +## Current state (what exists) -Split poker input by whether it needs judgment. +- **Store & logic:** `lyra/poker.py` — schema at `poker.py:21` (tables `poker_sessions`, `poker_hands`, `poker_stack_log`, `poker_rituals`, `poker_players`, `player_reads`, `player_observations`). Functions: `start_session` (157), `add_buyin` (389), `log_stack` (407), `stack_state` (446), `update_session` (362), `end_session` (515), `log_hand` (541, flat/no-LLM), `record_hand` (770, LLM-parses shorthand), `add_read` (1010), `hud` (1245). +- **Exposed endpoints (`lyra/web/server.py`):** `GET /session/data` (hud), `PATCH /session/{id}`, `DELETE /session/entry/{kind}/{id}`, `GET/DELETE /history`, `GET /hand/{id}/data`, `POST /hand/{id}/reconstruct`, `GET /hands/data`, `GET /recap/...`. **No** direct create endpoint for stack/buyin/hand/read/session — those are reachable only through chat → tool-calling. +- **UI:** `index.html` (chat), `session.html` (live HUD: stack card + sparkline + a PATCH-based edit form via `saveEdit()` at `session.html:192`, and `del(kind,id)` at `211`), `history.html`, `hand.html`. +- **Tool specs:** `lyra/tools.py` already defines arg schemas for each operation (`_f(...)` specs, `tools.py:469-658`) — the embryo of the contract. -- **Track 1 — dumb capture:** pure data → direct HTTP endpoint → tool handler. No prompt, no reply. -- **Track 2 — conversational:** anything needing a brain → a classifier tags the message type → a type-specific prompt fragment is injected in place of the giant card. -- **PWA fix:** correct the bottom safe-area inset. +## Design -### Deliverable 1 — Track 1: dumb capture (no LLM) +### 1. Store layer — harden `poker.py` -**Backend (new endpoints in `lyra/web/server.py`).** Today the *only* write path for a stack/buy-in is chat → tool-calling; no direct endpoint exists. Add: +Keep the existing functions and tables; tighten the module into a clean service boundary so both the REST layer and Lyra's tools call the *same* functions. Each operation: validates input, resolves the target session (`_resolve`), writes, returns a consistent dict. No behavior change to existing callers; this is consolidation, not a rewrite. -- `POST /session/stack` → `poker.log_stack(amount)` → returns updated `stack_state()`. Server stamps the time. -- `POST /session/buyin` → `poker.add_buyin(amount)` → returns new `buy_in_total`. -- Cash-out reuses the existing `PATCH /session/{id}` with `{cash_out}` (already recomputes `net`). +### 2. The tool/API contract *(first-class deliverable)* -Both resolve the live session via `poker._resolve(None)` and return `{"ok": ..., "stack": ...}`. On "no live session" return a clear error the UI can show. +A single source-of-truth document + schema defining every operation: name, purpose, JSON arg schema, return shape, and which REST route + which LLM tool map to it. Versioned (e.g. `contract_version: 1`). Lives at `docs/POKER_API.md` (or a machine-readable `poker_contract.py` that both the REST routes and `tools.py` specs derive from — preferred, so they can't drift). -**Frontend — the 2nd input box (`lyra/web/static/index.html`).** A slim quick-capture row **below the message input, above the bottom nav icons**. Behavior: +Operations (the canonical set): -- Type a number (e.g. `685`) → `POST /session/stack` → on success, append a one-line confirmation to the **Live Log** (`$685 logged · 11:34pm`) and update the HUD. **No chat message, no LLM call, main message box untouched.** -- Scope: **stack only** in v1. Buy-in and cash-out live on the Session HUD widget below. -- Empty/non-numeric input is ignored (or shows inline hint). Decimal and `$`-prefixed input tolerated (`$685`, `685`). +| Operation | Args | Entity | +|---|---|---| +| `start_session` | venue, stakes, game, format, buy_in, mantra | session | +| `update_session` | venue, stakes, game, format, buy_in_total, cash_out, mantra, mood | session | +| `end_session` | cash_out, mood | session | +| `delete_session` | id | session | +| `log_stack` | amount, note | stack entry | +| `delete_stack` | id | stack entry | +| `add_buyin` | amount | session (increments buy_in_total) | +| `log_hand` | position, hole_cards, board, streets…, pot, result, tag, lesson | hand | +| `record_hand` | shorthand (LLM-parsed) | hand | +| `update_hand` | id, any hand field | hand | +| `delete_hand` | id | hand | +| `add_read` | note, name, seat, tendencies, adjustment, category, venue | player/read | +| `update_read` / `update_player` | id, fields | player/read | +| `delete_read` | id | player/read | +| rituals: `scar_note`, `confidence_bank`, `alligator_blood`, `reset_ritual` | … | ritual | -**Frontend — HUD widget (`lyra/web/static/session.html`).** In the Stack card (`session.html:280`), mirroring the existing `saveEdit()` PATCH pattern (`session.html:192`): a stack field (`POST /session/stack`), a buy-in field (`POST /session/buyin`), and a cash-out field (existing PATCH). Each updates the sparkline on success. +### 3. REST API — complete the surface (`lyra/web/server.py`) -### Deliverable 2 — Track 2: classifier + type fragments +Add the missing **create/update** routes so the human UI (and any non-LLM client) can do everything: -**Classifier (`lyra/poker_classify.py`, new).** A function `classify(message: str) -> str` returning one of `HAND | STATUS | MENTAL | LOG | CHAT`. v1 is heuristic; the signature is the swappable seam — an LLM/MI50 implementation can replace the body later with no other changes. +- `POST /session/stack` → `log_stack(amount, note?)`; server-stamped time; returns `stack_state()`. +- `POST /session/buyin` → `add_buyin(amount)`; returns `buy_in_total`. +- `POST /session` → `start_session(...)`. +- `POST /session/hand` → `log_hand(...)` (flat) and/or `record_hand(shorthand)`. +- `PATCH /hand/{id}` → `update_hand(...)`; `DELETE /hand/{id}`. +- `POST /session/read` → `add_read(...)`; `PATCH /read/{id}`; `DELETE` via existing entry-delete. +- Keep existing `PATCH /session/{id}`, `DELETE /session/entry/{kind}/{id}`, `GET /session/data`. -Heuristic signals (first match wins, in priority order): +All return `{ok, ...}` and a clear error on "no live session." Routes are thin wrappers over the store, mirroring the contract one-to-one. -1. **HAND** — card-token regex (`\b[2-9TJQKA][shdc]\b` appearing ≥2×), or position tokens (UTG/MP/HJ/CO/BTN/SB/BB/"button"/"hijack"), or street words (flop/turn/river) combined with betting verbs (bet/raise/call/fold/check/shove/limp). -2. **MENTAL** — first-person feeling: "I feel", "I'm tilted/steaming/fried/tired/frustrated/confident", "in my head", "mental", "leak", "on tilt". -3. **STATUS** — time/venue/logistics with no cards: "table broke", "new table", "waiting for a seat", "seat opened", clock times, venue names. -4. **LOG** — prose money/result that slipped past the 2nd box: "I'm at 350", "stack is", "out for", "cashed", "rebought" with a number. -5. **CHAT** — default fallback (questions, open talk). +### 4. Human UI — log + edit/correct -Ambiguous → `CHAT` (safe full-voice default). +**Fast capture:** +- **2nd input box** (`index.html`): slim row **below the message input, above the bottom nav icons**. Type a number → `POST /session/stack` → time-stamped, sparkline updates, a one-line confirmation drops into the **Live Log**. **No chat message, no LLM call.** Stack-only in v1. Tolerates `$685`/`685`. +- **HUD widget** (`session.html`, in the Stack card at `:280`, mirroring `saveEdit()` at `:192`): stack field (`POST /session/stack`), buy-in field (`POST /session/buyin`), cash-out field (existing PATCH). -**Pipeline integration (`lyra/mind.py`).** Add a `_classify` step to `PIPELINE` (after `_route`, before `_compose`) that sets `ctx.msg_type` when `ctx.mode.key == "poker_cash"`. In `build_messages` (`mind.py:137`), when in poker mode, inject the **type fragment** in the seat currently held by `mode.card` (`mind.py:152`) instead of the whole `_CASH_CARD`. Non-poker modes are unchanged. +**Edit / correct (fix Lyra's mistakes):** +- Edit any session field (exists via the PATCH edit form — verify coverage). +- Hands list with edit + delete (`hand.html` + new PATCH/DELETE) — fix mislabeled villains, wrong board, wrong result. +- Reads/players list with edit + delete — rename "Dave the rock" ≠ "Dave the mechanic", fix tendencies. +- Stack entries deletable (exists via `del('stack', id)`) — verify. -**Fragment registry (new `lyra/poker_prompts.py`).** `POKER_FRAGMENTS: dict[str, str]` keyed by type. Each fragment is short and concrete (rules + response shape), not trait language. Persona guardrails ("never eyeball math," "don't invent reads") stay sourced from `personas/lyra.md` via `_persona_block`. +### 5. iOS-PWA bottom safe-area fix -Response contracts: - -- **HAND.** Steps, in order: (1) log it (`record_hand` preferred, or `log_hand`); (2) if the decision is close or equity matters, call `analyze_spot` — do **not** eyeball; (3) reason about **bet intent** — for each meaningful bet, name what it was for (value / bluff / protection) and whether it achieved that (a fold to a value bet = value lost — flag it; a call of a bluff = bluff failed); (4) name leaks plainly (value-owning, missed value, bad sizing); (5) one clear opinion. **No reflexive praise** — do not open with "nice hand." If a named villain is referenced, call `player_profile`/`get_villain_file` first; never invent a read. Talk in prose, not a listicle. Seed with 2 exemplars (below). -- **STATUS.** 1–2 sentences. Acknowledge, log a stack only if a bare number is present, then stop. **No coaching. No tilt/fatigue reads.** -- **MENTAL.** Full presence, real voice, an actual point of view. (This register already works — preserve it.) -- **LOG.** One line. `$317 logged.` Nothing else. -- **CHAT.** Full voice, a real opinion, no filler sign-offs. - -**HAND exemplars** (embedded in the fragment): - -> *Brian:* "Flopped bottom set with 22, turned a full house, bet $40 on the river, he folded 88." -> *Lyra:* "That's a value bet that got no value — 88 was never folding to a smaller stab. $40 into that pot was too much; ~$20–25 gets called by the over-pair and the worse two-pairs. The hand's a monster, the leak is the sizing." - -> *Brian:* "77 in a multiway pot, flop 4h5c3c, HJ bets 75, BTN shoves 74, I call. Turn/river check through, HJ wins with 64." -> *Lyra:* "Against a bet and a shove on 4-5-3 you're drawing thin — sets, two pair, and the made wheel are all ahead, and you block almost none of it. The stack-depth logic (he only had ~150 behind) is real, but that's a reason to fold and wait, not to call off light. This is the value-owning spot you flagged yourself." - -**Pipeline fixes:** - -- Remove/gate the `_route` mood nudge (`mind.py:328`) so a steady→"steady" / up→"hype" note is **not** appended on non-MENTAL messages. Mental-game register comes only from the MENTAL fragment. -- Trim the always-on mode-menu note (`mind.py:77`) inside poker mode — it dilutes every turn. - -### Deliverable 3 — PWA bottom safe-area fix - -The band below the nav icons is an iOS-PWA safe-area issue (likely `100vh` not accounting for `env(safe-area-inset-bottom)` / the home indicator). Fix the layout container and bottom nav in the web CSS so the app fills the viewport with the icons seated above the home indicator. Use the `building-ios-pwas` skill at implementation time. +The empty band below the nav icons is a safe-area issue (likely `100vh` not accounting for `env(safe-area-inset-bottom)` / the home indicator). Fix the layout container + bottom nav CSS so the app fills the viewport with the icons seated above the home indicator. Use the `building-ios-pwas` skill at implementation time. ## Testing / verification -- **Classifier unit tests:** feed the real user turns from both transcripts and assert the expected type (e.g. "Stack is at $373" → LOG/handled by box, "Button straddle… I limp UTG with 22…" → HAND, "table broke, it's 11:50pm" → STATUS, "I feel like I'm being mean when I raise" → MENTAL). -- **Endpoint tests:** `POST /session/stack` / `/session/buyin` against a live test session; assert the row is written, time stamped, and `stack_state()` updates; assert a clean error when no live session. -- **Transcript replay (eyeball eval):** run yesterday's + today's user turns through the new classifier + fragments and compare replies to the logged mush — confirm stack updates produce no essay, the $40 hand is flagged as missed value, and STATUS turns carry no tilt-reading. -- **PWA:** verify on the iOS PWA that the gap is gone and the new input box sits above the nav with the keyboard open. +- **Contract conformance:** a test asserting each REST route and each `tools.py` spec matches the canonical contract (names, required args) — catches drift between the human API and the LLM API. +- **Endpoint round-trips:** create → read → update → delete for stack, buyin, hand, read against a test session; assert rows written, time stamped, `stack_state()`/`hud()` reflect changes; assert clean error with no live session. +- **UI manual pass:** log a stack via the 2nd box and confirm it lands in Live Log + sparkline without a chat reply; edit a hand's villain and confirm persistence; delete a bad read. +- **PWA:** on the iOS PWA, confirm the bottom gap is gone and the 2nd input box sits above the nav with the keyboard open. -## Out of scope / future +--- -- LLM classifier behind the same interface (if heuristics prove brittle). -- Extending message-type fragments to the other five modes. -- Itemized buy-in history / per-table stack series. -- Buy-in/cash-out in the chat-page 2nd box (HUD only for v1). +# Sub-project 2 — Lyra wiring *(parked)* + +Detail preserved here; gets its own spec → plan after sub-project 1 is MVP'd. + +## Why it exists (diagnosis from real sessions) + +Evidence from `sess-dff2s91c` (2026-06-27 Meadows, 2026-06-28 Wheeling): + +- **Coaching essay on every turn, including pure data** — `Stack=$685` drew 4–6 sentences of "keep that momentum rolling." (Sub-project 1's dumb capture removes these from the LLM entirely.) +- **False tilt/fatigue reads** — "table broke, it's 11:50pm" → repeated "late-night fatigue… mental reset"; Brian: *"you seem to be reading me as tilted."* Cause: the `_route` mood nudge (`mind.py:328`) firing on non-mood messages. +- **No bet-intent reasoning** — a value bet ($40, full house) that folded out 88 was praised as "the power of representing something stronger." It was value *lost*, not a successful rep. +- **Eyeballs instead of `analyze_spot`** — 77 multiway got "a disciplined fold might have been better," no math, violating the persona's "never eyeball poker math" rule. +- **Even her sharp reads leak bad logic** — the Connie read included "limp-checking in position" (contradictory). + +Root cause: one broad per-turn card (`_CASH_CARD`, `modes.py:66`) describes traits; the model satisfies trait language with safe abstraction. + +## Planned approach + +- **Classifier** (`lyra/poker_classify.py`): `classify(message) -> HAND | STATUS | MENTAL | LOG | CHAT`. Heuristic v1 (card-token regex, position/street keywords, feeling phrases, time/venue), swappable for an LLM/MI50 classifier behind the same signature. Ambiguous → CHAT. +- **Pipeline:** a `_classify` step in `mind.PIPELINE` sets `ctx.msg_type` (poker mode only); `build_messages` injects the **type fragment** in the seat now held by `mode.card` (`mind.py:152`) instead of the whole `_CASH_CARD`. +- **Fragments** (`lyra/poker_prompts.py`, `POKER_FRAGMENTS`): concrete rules + response shape per type. + - **HAND:** log it → `analyze_spot` if close → reason about **bet intent** (value/bluff/protection — did it work? a fold to a value bet = value lost, flag it) → name leaks (value-owning, missed value, sizing) → one opinion. No reflexive praise. Pull `player_profile` before referencing a villain. Seeded with 2 real-hand exemplars (the $40 value bet; the 77 spot). + - **STATUS:** 1–2 sentences, no coaching, no tilt-reading. + - **MENTAL:** full presence (already works — preserve). + - **LOG:** one line. + - **CHAT:** full voice, real opinion. +- **Pipeline fixes:** kill the misfiring `_route` mood nudge (`mind.py:328`); trim the always-on mode-menu note (`mind.py:77`) in poker mode. +- **MI50 tool-calling:** enable tools on the MI50 backend (`chat.py` `TOOL_BACKENDS = {"cloud"}` at `:21`) so a fine-tuned, tool-calling poker model can drive the contract. Requires the fine-tune to emit the contract's tool-call format. + +## HAND exemplars (preserved for sub-project 2) + +> *Brian:* "Flopped bottom set with 22, turned a full house, bet $40 on the river, he folded 88." +> *Lyra:* "That's a value bet that got no value — 88 was never folding to a smaller stab. $40 into that pot was too much; ~$20–25 gets called by the over-pair and worse two-pairs. The hand's a monster, the leak is the sizing." + +> *Brian:* "77 multiway, flop 4h5c3c, HJ bets 75, BTN shoves 74, I call. Turn/river check through, HJ wins with 64." +> *Lyra:* "Against a bet and a shove on 4-5-3 you're drawing thin — sets, two pair, and the made wheel are all ahead, and you block almost none of it. The stack-depth read (he only had ~150 behind) is real, but that's a reason to fold and wait, not to call off light. This is the value-owning spot you flagged yourself." From a2835500bc3fd40dd75e210523a1401e240d7c19 Mon Sep 17 00:00:00 2001 From: serversdown Date: Sun, 28 Jun 2026 23:27:58 +0000 Subject: [PATCH 03/39] docs: implementation plan for poker logging service (sub-project 1) Seven tasks: contract module + conformance test, direct capture endpoints (stack/buyin/start), hands API, reads/players API + route conformance, chat-page stack quick-capture box, HUD quick inputs + villain rename, and the iOS-PWA bottom safe-area fix. TDD with real test/impl code grounded in the actual poker.py signatures and FastAPI route patterns. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx --- .../plans/2026-06-28-poker-logging-service.md | 720 ++++++++++++++++++ 1 file changed, 720 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-28-poker-logging-service.md diff --git a/docs/superpowers/plans/2026-06-28-poker-logging-service.md b/docs/superpowers/plans/2026-06-28-poker-logging-service.md new file mode 100644 index 0000000..cc55b71 --- /dev/null +++ b/docs/superpowers/plans/2026-06-28-poker-logging-service.md @@ -0,0 +1,720 @@ +# Poker Logging Service Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Turn `lyra/poker.py` into a standalone logging system-of-record with a complete REST API, a single source-of-truth tool/API contract, and a human UI to log and correct everything — usable by Brian with zero LLM dependency. + +**Architecture:** Thin FastAPI routes wrap the existing (already-working) `poker.py` store functions; a declarative `poker_contract.py` pins operation names + required args so the REST API and Lyra's LLM tool specs can't drift; the web UI gets dumb capture inputs (2nd stack box on chat, quick inputs on the HUD) and correction controls. This is sub-project 1 of 2; Lyra's classifier/prompts (sub-project 2) are parked. + +**Tech Stack:** Python 3.11+ (venv runs 3.14), FastAPI + uvicorn, SQLite (WAL), pytest, vanilla HTML/JS/CSS. + +## Global Constraints + +- Python files: start with `from __future__ import annotations`; 4-space indent; ruff `line-length = 100`, `target-version = "py311"`. +- **`lyra/web/static/index.html` uses CRLF (`\r\n`) line endings and mixed tabs/spaces.** Every other static file (`session.html`, `style.css`, `nav.js`) and all Python use **LF + spaces**. Match the file you edit or you produce a noisy diff. +- Pure data capture (stack / buy-in / cash-out / hand / read) must reach the store via the REST endpoints, **never** through the chat/LLM path. +- `poker_contract.py` is the single source of truth: REST routes and `tools.py` specs must agree with it (enforced by a conformance test). +- Web app runs via `lyra-web` (uvicorn) on `0.0.0.0:7078`. DB path from `LYRA_DB_PATH` (default `data/lyra.db`, WAL). +- Test idiom: fixture sets `LYRA_DB_PATH` to a `tmp_path` file, stubs `llm.embed` (and `llm.complete` where needed), then `importlib.reload(memory)` **then** `importlib.reload(poker)` (order matters), then `importlib.reload(server)` for endpoint tests. Run with `.venv/bin/pytest` (or `uv run pytest`). +- Existing store facts to respect: `start_session(...)` uses `fmt=` (column is `format`); `add_buyin` returns a float total; `log_stack` returns the `stack_state` dict `{current, buy_in, net}`; `end_session(cash_out, ...)` takes `cash_out` first; `hud()` returns `None` when no session; `_HAND_FIELDS = ("position","hole_cards","board","preflop","flop","turn","river","showdown","pot","result","stack_after","tag","lesson")`; `upsert_player(name, **fields)` returns an int player id; `tools.dispatch(name, args, ctx)` — `ctx` is a plain dict. + +--- + +### Task 1: Contract module + tool-spec conformance test + +**Files:** +- Create: `lyra/poker_contract.py` +- Create: `tests/test_poker_contract.py` + +**Interfaces:** +- Produces: `lyra.poker_contract.OPERATIONS: dict[str, dict]` and `CONTRACT_VERSION: int`. Each op value: `{"required": tuple[str,...], "llm_tool": str | None, "rest": tuple[str, str] | None}` where `rest` is `(METHOD, PATH)` with PATH exactly matching the FastAPI route template. + +- [ ] **Step 1: Write the contract module** + +`lyra/poker_contract.py`: +```python +from __future__ import annotations + +# Single source of truth for poker logging operations. The REST API, Lyra's LLM +# tool specs, the human UI, and (later) an MCP wrapper all derive from this. +# `required` MUST match the `required` list in the matching tools.py spec. +# `rest` PATH MUST match the FastAPI route template verbatim. +CONTRACT_VERSION = 1 + +OPERATIONS: dict[str, dict] = { + "start_session": {"required": (), "llm_tool": "start_session", "rest": ("POST", "/session")}, + "update_session": {"required": (), "llm_tool": "update_session", "rest": ("PATCH", "/session/{session_id}")}, + "end_session": {"required": ("cash_out",), "llm_tool": "end_session", "rest": None}, + "log_stack": {"required": ("amount",), "llm_tool": "log_stack", "rest": ("POST", "/session/stack")}, + "add_buyin": {"required": ("amount",), "llm_tool": "add_buyin", "rest": ("POST", "/session/buyin")}, + "log_hand": {"required": (), "llm_tool": "log_hand", "rest": ("POST", "/session/hand")}, + "update_hand": {"required": ("id",), "llm_tool": None, "rest": ("PATCH", "/hand/{hand_id}")}, + "add_read": {"required": ("note",), "llm_tool": "add_read", "rest": ("POST", "/session/read")}, + "update_player": {"required": ("id",), "llm_tool": None, "rest": ("PATCH", "/player/{player_id}")}, +} +``` + +- [ ] **Step 2: Write the failing conformance test** + +`tests/test_poker_contract.py`: +```python +from __future__ import annotations + +from lyra import tools +from lyra.poker_contract import OPERATIONS + + +def test_llm_tool_required_args_match_contract(): + for op, decl in OPERATIONS.items(): + name = decl["llm_tool"] + if not name: + continue + spec = tools.TOOLS[name]["spec"] + required = set(spec["function"]["parameters"]["required"]) + assert required == set(decl["required"]), ( + f"{op}: tools spec required {required} != contract {set(decl['required'])}" + ) +``` + +- [ ] **Step 3: Run the test** + +Run: `.venv/bin/pytest tests/test_poker_contract.py -v` +Expected: PASS (the contract's `required` tuples were copied from the live specs). + +- [ ] **Step 4: Commit** + +```bash +git add lyra/poker_contract.py tests/test_poker_contract.py +git commit -m "feat: poker operation contract + tool-spec conformance test" +``` + +--- + +### Task 2: Direct capture endpoints (stack / buy-in / start) + +**Files:** +- Modify: `lyra/web/server.py` (add three routes inside `create_app`, near the existing `PATCH /session/{session_id}` at server.py:116) +- Create: `tests/test_poker_api.py` + +**Interfaces:** +- Consumes: `poker.log_stack(amount, note=None)`, `poker.add_buyin(amount)`, `poker.start_session(venue=, stakes=, game=, fmt=, buy_in=, mantra=)`, `poker.live_session()`. +- Produces: `POST /session/stack` → `{ok, stack}` or `{ok:false, error}`; `POST /session/buyin` → `{ok, buy_in_total}`; `POST /session` → `{ok, id}`. + +- [ ] **Step 1: Write the failing endpoint tests** + +`tests/test_poker_api.py`: +```python +from __future__ import annotations + +import importlib + +import pytest + + +@pytest.fixture +def client(tmp_path, monkeypatch): + monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db")) + from lyra import llm + monkeypatch.setattr(llm, "embed", lambda texts: [[0.1, 0.2, 0.3] for _ in texts]) + import lyra.memory as memory + importlib.reload(memory) + import lyra.poker as poker + importlib.reload(poker) + import lyra.web.server as server + importlib.reload(server) + from fastapi.testclient import TestClient + return TestClient(server.app), poker + + +def test_post_stack_logs_and_returns_state(client): + c, poker = client + poker.start_session(venue="Meadows", stakes="1/3", buy_in=400) + r = c.post("/session/stack", json={"amount": 373}) + assert r.status_code == 200 + body = r.json() + assert body["ok"] is True + assert body["stack"]["current"] == 373 + assert body["stack"]["net"] == pytest.approx(-27) + + +def test_post_stack_without_session_errors(client): + c, _ = client + r = c.post("/session/stack", json={"amount": 373}) + assert r.json()["ok"] is False + assert "error" in r.json() + + +def test_post_buyin_increments_total(client): + c, poker = client + poker.start_session(buy_in=400) + r = c.post("/session/buyin", json={"amount": 200}) + assert r.json()["buy_in_total"] == pytest.approx(600) + + +def test_post_session_starts_live(client): + c, poker = client + r = c.post("/session", json={"venue": "Wheeling", "stakes": "1/3", "buy_in": 400}) + sid = r.json()["id"] + assert poker.live_session()["id"] == sid +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `.venv/bin/pytest tests/test_poker_api.py -v` +Expected: FAIL with 404s (routes not defined). If it errors with "No module named 'httpx'", run `.venv/bin/pip install httpx` (TestClient needs it). + +- [ ] **Step 3: Add the three routes** + +In `lyra/web/server.py`, immediately after the `PATCH /session/{session_id}` handler (server.py:122), add: +```python + @app.post("/session/stack") + async def session_log_stack(request: Request) -> dict: + """Log Brian's current stack directly (no LLM). Server-stamps the time.""" + body = await request.json() + try: + amount = float(body.get("amount")) + except (TypeError, ValueError): + return {"ok": False, "error": "amount must be a number"} + note = (body.get("note") or "").strip() or None + try: + state = await asyncio.to_thread(poker.log_stack, amount, note) + except ValueError as exc: + return {"ok": False, "error": str(exc)} + logbus.log("info", "stack logged (direct)", amount=amount) + return {"ok": True, "stack": state} + + @app.post("/session/buyin") + async def session_add_buyin(request: Request) -> dict: + """Add a buy-in/rebuy directly (no LLM).""" + body = await request.json() + try: + amount = float(body.get("amount")) + except (TypeError, ValueError): + return {"ok": False, "error": "amount must be a number"} + try: + total = await asyncio.to_thread(poker.add_buyin, amount) + except ValueError as exc: + return {"ok": False, "error": str(exc)} + logbus.log("info", "buyin added (direct)", amount=amount) + return {"ok": True, "buy_in_total": total} + + @app.post("/session") + async def session_start(request: Request) -> dict: + """Open a new live session directly (no LLM).""" + body = await request.json() + sid = await asyncio.to_thread(lambda: poker.start_session( + venue=body.get("venue"), stakes=body.get("stakes"), + game=body.get("game") or "NLH", fmt=body.get("format") or "cash", + buy_in=body.get("buy_in") or 0, mantra=body.get("mantra"), + )) + logbus.log("info", "poker session started (direct)", id=sid) + return {"ok": True, "id": sid} +``` + +- [ ] **Step 4: Run to verify it passes** + +Run: `.venv/bin/pytest tests/test_poker_api.py -v` +Expected: PASS (4 tests). + +- [ ] **Step 5: Commit** + +```bash +git add lyra/web/server.py tests/test_poker_api.py +git commit -m "feat: direct REST endpoints for stack/buyin/start-session (no LLM)" +``` + +--- + +### Task 3: Hands API (log / edit / delete) + +**Files:** +- Modify: `lyra/poker.py` (add `update_hand` near `log_hand` at poker.py:558) +- Modify: `lyra/web/server.py` (add routes after the Task 2 routes) +- Modify: `tests/test_poker_api.py` (add tests) + +**Interfaces:** +- Consumes: `poker.log_hand(**fields)`, `poker.get_hand(id)`, `poker.delete_entry("hand", id)`, `_HAND_FIELDS`. +- Produces: `poker.update_hand(hand_id, **fields) -> dict | None`; `POST /session/hand` → `{ok, id}`; `PATCH /hand/{hand_id}` → `{ok, hand}`; `DELETE /hand/{hand_id}` → `{ok}`. + +- [ ] **Step 1: Write the failing tests** + +Append to `tests/test_poker_api.py`: +```python +def test_post_hand_edit_and_delete(client): + c, poker = client + poker.start_session(buy_in=400) + r = c.post("/session/hand", json={"position": "BTN", "hole_cards": "22", "result": 120}) + assert r.json()["ok"] is True + hid = r.json()["id"] + r2 = c.patch(f"/hand/{hid}", json={"hole_cards": "2c2d"}) + assert r2.json()["ok"] is True + assert r2.json()["hand"]["hole_cards"] == "2c2d" + r3 = c.delete(f"/hand/{hid}") + assert r3.json()["ok"] is True + assert poker.get_hand(hid) is None +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `.venv/bin/pytest tests/test_poker_api.py::test_post_hand_edit_and_delete -v` +Expected: FAIL (404 on `/session/hand`). + +- [ ] **Step 3: Add `update_hand` to the store** + +In `lyra/poker.py`, immediately after `log_hand` (poker.py:558), add: +```python +def update_hand(hand_id: int, **fields) -> dict | None: + """Edit a logged hand's flat fields (fix a mislabeled board, result, villain). + Only known columns are touched. Returns the updated hand row or None.""" + sets, vals = [], [] + for k, v in fields.items(): + if k in _HAND_FIELDS and v is not None: + sets.append(f"{k} = ?") + vals.append(v) + if sets: + conn = _c() + with conn: + conn.execute(f"UPDATE poker_hands SET {', '.join(sets)} WHERE id = ?", + (*vals, hand_id)) + return get_hand(hand_id) +``` + +- [ ] **Step 4: Add the three routes** + +In `lyra/web/server.py`, after the Task 2 routes, add: +```python + @app.post("/session/hand") + async def session_log_hand(request: Request) -> dict: + """Log a hand directly with flat fields (no LLM parse).""" + body = await request.json() + try: + hid = await asyncio.to_thread(lambda: poker.log_hand(**body)) + except ValueError as exc: + return {"ok": False, "error": str(exc)} + logbus.log("info", "hand logged (direct)", id=hid) + return {"ok": True, "id": hid} + + @app.patch("/hand/{hand_id}") + async def hand_update(hand_id: int, request: Request) -> dict: + """Edit a logged hand's flat fields.""" + body = await request.json() + h = await asyncio.to_thread(lambda: poker.update_hand(hand_id, **body)) + logbus.log("info", "hand edited", id=hand_id, fields=list(body)) + return {"ok": h is not None, "hand": h} + + @app.delete("/hand/{hand_id}") + async def hand_delete(hand_id: int) -> dict: + """Delete a logged hand.""" + ok = await asyncio.to_thread(poker.delete_entry, "hand", hand_id) + return {"ok": ok} +``` + +- [ ] **Step 5: Run to verify it passes** + +Run: `.venv/bin/pytest tests/test_poker_api.py -v` +Expected: PASS (all tests, including the new hand test). + +- [ ] **Step 6: Commit** + +```bash +git add lyra/poker.py lyra/web/server.py tests/test_poker_api.py +git commit -m "feat: hands API — log_hand endpoint, update_hand store fn, edit/delete routes" +``` + +--- + +### Task 4: Reads/players API + route conformance + +**Files:** +- Modify: `lyra/poker.py` (add `update_player` near `upsert_player`) +- Modify: `lyra/web/server.py` (add routes) +- Modify: `tests/test_poker_api.py` (add tests) +- Modify: `tests/test_poker_contract.py` (add route-coverage test) + +**Interfaces:** +- Consumes: `poker.add_read(note=, name=, ...)`, `poker.upsert_player(name, **fields)`. +- Produces: `poker.update_player(player_id, **fields) -> dict | None`; `POST /session/read` → `{ok, id}`; `PATCH /player/{player_id}` → `{ok, player}`. + +- [ ] **Step 1: Write the failing tests** + +Append to `tests/test_poker_api.py`: +```python +def test_post_read(client): + c, poker = client + poker.start_session(buy_in=400) + r = c.post("/session/read", json={"note": "3-bets light", "name": "James K"}) + assert r.json()["ok"] is True + assert isinstance(r.json()["id"], int) + + +def test_rename_player_fixes_mislabel(client): + c, poker = client + pid = poker.upsert_player("Dave the rock", category="reg") + r = c.patch(f"/player/{pid}", json={"name": "Dave the mechanic"}) + assert r.json()["ok"] is True + assert r.json()["player"]["name"] == "Dave the mechanic" +``` + +Append to `tests/test_poker_contract.py`: +```python +def test_rest_routes_registered(): + import lyra.web.server as server + registered = set() + for route in server.app.routes: + methods = getattr(route, "methods", None) + path = getattr(route, "path", None) + if not methods or not path: + continue + for m in methods: + registered.add((m, path)) + for op, decl in OPERATIONS.items(): + if not decl["rest"]: + continue + method, path = decl["rest"] + assert (method, path) in registered, f"{op}: {method} {path} not registered" +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `.venv/bin/pytest tests/test_poker_api.py::test_rename_player_fixes_mislabel tests/test_poker_contract.py::test_rest_routes_registered -v` +Expected: FAIL (404 on `/player/...`; route-coverage missing several POST/PATCH paths). + +- [ ] **Step 3: Add `update_player` to the store** + +In `lyra/poker.py`, immediately after `upsert_player` (find it near poker.py:1010), add: +```python +_PLAYER_FIELDS = ("name", "venue", "description", "tendencies", "adjustment", "category") + + +def update_player(player_id: int, **fields) -> dict | None: + """Edit a player's dossier (rename, fix tendencies/category). Returns the row or None.""" + sets, vals = [], [] + for k, v in fields.items(): + if k in _PLAYER_FIELDS and v is not None: + sets.append(f"{k} = ?") + vals.append(v) + if sets: + conn = _c() + with conn: + conn.execute(f"UPDATE poker_players SET {', '.join(sets)} WHERE id = ?", + (*vals, player_id)) + row = _c().execute("SELECT * FROM poker_players WHERE id = ?", (player_id,)).fetchone() + return dict(row) if row else None +``` + +- [ ] **Step 4: Add the two routes** + +In `lyra/web/server.py`, after the Task 3 routes, add: +```python + @app.post("/session/read") + async def session_add_read(request: Request) -> dict: + """Log a read directly (no LLM); upserts the villain file when name is given.""" + body = await request.json() + rid = await asyncio.to_thread(lambda: poker.add_read( + note=body.get("note") or "", seat=body.get("seat"), name=body.get("name"), + tendencies=body.get("tendencies"), adjustment=body.get("adjustment"), + description=body.get("description"), category=body.get("category"), + venue=body.get("venue"), + )) + return {"ok": True, "id": rid} + + @app.patch("/player/{player_id}") + async def player_update(player_id: int, request: Request) -> dict: + """Edit a player's dossier (rename, fix tendencies).""" + body = await request.json() + p = await asyncio.to_thread(lambda: poker.update_player(player_id, **body)) + logbus.log("info", "player edited", id=player_id, fields=list(body)) + return {"ok": p is not None, "player": p} +``` + +- [ ] **Step 5: Run to verify it passes** + +Run: `.venv/bin/pytest tests/test_poker_api.py tests/test_poker_contract.py -v` +Expected: PASS (all API tests + both conformance tests). + +- [ ] **Step 6: Run the full suite (no regressions)** + +Run: `.venv/bin/pytest -q` +Expected: PASS (existing poker/tools/chat tests still green). + +- [ ] **Step 7: Commit** + +```bash +git add lyra/poker.py lyra/web/server.py tests/test_poker_api.py tests/test_poker_contract.py +git commit -m "feat: reads/players API + REST route conformance test" +``` + +--- + +### Task 5: Chat-page stack quick-capture (2nd input box) + +**Files:** +- Modify: `lyra/web/static/index.html` (**CRLF + tabs** — add markup + JS) +- Modify: `lyra/web/static/style.css` (LF + spaces — add styling) + +**Interfaces:** +- Consumes: `POST /session/stack` (Task 2). Reads `currentSession` and the Live Log DOM (`#thinkingContent`, `#thinkingEmpty`) already present in index.html. +- Produces: a stack-only input that logs without any chat/LLM call. + +- [ ] **Step 1: Add the input row markup** + +In `lyra/web/static/index.html`, insert **between** the `
` block (ends ~index.html:125) and `