From 86f3d2dc0a0932375b29771b1d987b0f5686942f Mon Sep 17 00:00:00 2001 From: serversdown Date: Sun, 28 Jun 2026 03:54:31 +0000 Subject: [PATCH] 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).