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

10 KiB
Raw Blame History

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/stackpoker.log_stack(amount) → returns updated stack_state(). Server stamps the time.
  • POST /session/buyinpoker.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).