Files
project-lyra/docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md
T
serversdown 86f3d2dc0a 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-28 03:54:31 +00:00

116 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 46 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.** 12 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; ~$2025 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).