From 5c63175a3cce5267a5e5607b9ad070ed16b9018d Mon Sep 17 00:00:00 2001 From: serversdown Date: Sun, 28 Jun 2026 20:59:52 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20restructure=20spec=20=E2=80=94=20poker?= =?UTF-8?q?=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."