docs: readjust the poker-prompts spec for last night's scouting-desk/roster work
The message-type-prompts plan (sub-project 2) predated the scouting desk + roster build on this branch. Readjust it so the prompting work starts from reality: - Add a "Readjustment 2026-07-04" section: the real failure shifted from mush to MISSED TOOL CALLS; two new message types (READ, TABLE); HAND gains a hero-vs-observed split; the scouting desk is a live per-turn injection layer to compose with (not duplicate); BASE must cover the expanded toolset + identity rules; source card grew to modes.py:67-169; Phase A still unbuilt. - Taxonomy → READ | HAND | TABLE | MENTAL | STATUS | LOG | CHAT, with READ above HAND (a villain's action lands on their file, not Brian's). - New READ + TABLE fragments; BASE routes the full current tool set; STATUS narrowed to pure logistics; classify tests add READ/TABLE + the READ↔HAND edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,83 @@
|
|||||||
# Poker message-type prompts (sub-project 2)
|
# Poker message-type prompts (sub-project 2)
|
||||||
|
|
||||||
- **Date:** 2026-07-01
|
- **Date:** 2026-07-01 (**readjusted 2026-07-04** — see below)
|
||||||
- **Status:** Spec for review
|
- **Status:** Spec — **needs rework before build** (foundations shifted; nothing here built yet)
|
||||||
- **Branch:** `feat/poker-mode-prompts` (continues on the same branch; sub-project 1 shipped there)
|
- **Branch:** `feat/poker-mode-prompts` (continues on the same branch; sub-project 1 shipped there)
|
||||||
- **Supersedes:** the parked "sub-project 2" section of `docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md`
|
- **Supersedes:** the parked "sub-project 2" section of `docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠ Readjustment — 2026-07-04 (read this first)
|
||||||
|
|
||||||
|
A long live-session build on `feat/poker-mode-prompts` (the "scouting desk" +
|
||||||
|
roster work — see `docs/SCOUTING_DESK.md` and commits after `3afa75f`) landed
|
||||||
|
**after** this spec was written and changes its foundations. Nothing in Phases
|
||||||
|
A/B/C is built yet, but the plan below must absorb these deltas before it's coded.
|
||||||
|
The core idea — *classify the turn, inject a small per-type contract instead of one
|
||||||
|
giant card* — is now **more** justified (the card nearly doubled). But:
|
||||||
|
|
||||||
|
1. **The real failure mode shifted from mush to MISSED TOOL CALLS.** Live, the
|
||||||
|
pain wasn't flattering essays — it was reads/TAGs not getting logged, and
|
||||||
|
"clear the table" claimed-but-not-done. So every action-type fragment (LOG,
|
||||||
|
READ, TABLE, HAND) needs a hard *"call the tool FIRST, every time, then one
|
||||||
|
short line"* contract. This raises the stakes on Phase B and validates the
|
||||||
|
whole dynamic approach (a targeted directive beats a 100-line card).
|
||||||
|
|
||||||
|
2. **The taxonomy is missing two types that dominated the session:**
|
||||||
|
- **READ** (a *villain's* action): "TAG limped A4o in the SB", "Jonathan
|
||||||
|
called the 3bet". Under the current classifier rules these misfire as **HAND**
|
||||||
|
(card tokens + position + a betting verb) and get logged as *Brian's* hand.
|
||||||
|
They must route to **`add_read`** on the named player/handle/descriptor — NOT
|
||||||
|
`record_hand`. New priority rule, ABOVE HAND: if the actor is another player
|
||||||
|
(a handle/name/descriptor is the subject, not "I/me/my"), it's a READ.
|
||||||
|
Handles are often initials/all-caps (e.g. **TAG** is a *person*, not the
|
||||||
|
tight-aggressive style).
|
||||||
|
- **TABLE** (roster ops): "seat the table: TAG, Jonathan…", "table broke",
|
||||||
|
"I got moved", "TAG left". These now have real tool actions
|
||||||
|
(**`seat_players` / `clear_table` / `unseat_player`**), not just "acknowledge
|
||||||
|
and stop." Split these out of STATUS (STATUS stays for pure logistics with no
|
||||||
|
roster action).
|
||||||
|
|
||||||
|
3. **HAND now has a hero-vs-observed distinction.** The parser gained
|
||||||
|
`hero_involved`; a hand Brian *watched* between others is logged with null hero
|
||||||
|
fields (not pinned to him). The HAND fragment must tell her: if he was in it →
|
||||||
|
`record_hand` as hero + analysis; if he only watched → it's really READ(s) on
|
||||||
|
the players, or an observed hand — never analyze it as his.
|
||||||
|
|
||||||
|
4. **A new live per-turn injection layer already exists: the scouting desk**
|
||||||
|
(`lyra/scouting.py`, injected in `build_messages` at the poker-mode gate,
|
||||||
|
~`mind.py:177`). It dynamically adds a `SCOUTING DESK` note (named/descriptor
|
||||||
|
villain recall + leak/pattern recall) every poker turn, fail-safe. **The
|
||||||
|
classifier/fragment injection must compose with it, not duplicate it:** the
|
||||||
|
desk supplies *who this villain is / past leaks*; the fragments supply *response
|
||||||
|
shape + which tool to call*. Both are system-note appends in the same block.
|
||||||
|
|
||||||
|
5. **BASE must cover the expanded toolset + identity rules.** Beyond the original
|
||||||
|
tools, BASE now routes: `seat_players`/`unseat_player`/`clear_table` (roster),
|
||||||
|
`add_read` with **`name` OR `descriptor`** (nameless villains), `name_villain`
|
||||||
|
and `link_villains` (confirm-loop). Plus the hard rules learned live: `name` =
|
||||||
|
real handle ONLY (a description in `name` spawns duplicates — put the look in
|
||||||
|
`descriptor`); confirm before merging; never claim a tool ran without calling it.
|
||||||
|
|
||||||
|
6. **Source material grew (good news).** `_CASH_CARD` is now `modes.py:67-169`
|
||||||
|
(was 66-116) and much of the new text — roster, TAG/read routing, PLAYERS,
|
||||||
|
session-narration `note` rules — is already the *concrete, tool-routing
|
||||||
|
contract* this spec wanted, not traits. Better raw material to distill into
|
||||||
|
BASE + fragments than the original vague card.
|
||||||
|
|
||||||
|
7. **Phase A is still unbuilt and still valid.** `_mode_menu_note` is still
|
||||||
|
appended every turn (`mind.py:162`); the `_route` mood nudge still fires. The
|
||||||
|
scouting-desk work already established the `mode.key == "poker_cash"` gate to
|
||||||
|
reuse. (Note: revalidate all `mind.py` line numbers below — they've drifted.)
|
||||||
|
|
||||||
|
**Net:** taxonomy becomes **HAND / READ / TABLE / STATUS / MENTAL / LOG / CHAT**;
|
||||||
|
fragments lead with a hard tool-call contract; injection sits alongside the
|
||||||
|
scouting desk; BASE lists the full current toolset. The rest of the plan stands.
|
||||||
|
The classifier/dynamic-prompting build is being explored in a separate session —
|
||||||
|
this doc is its poker-side source of truth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Problem (recap)
|
## Problem (recap)
|
||||||
|
|
||||||
In poker mode Lyra routes correctly but her replies are generic — one broad `_CASH_CARD` (`lyra/modes.py:66-116`) describes *traits* and gets injected on every turn, so the model satisfies it with safe, flattering abstraction. From real sessions: coaching essays on bare stack updates, false tilt/fatigue reads on neutral logistics ("table broke, it's 11:50pm" → "late-night fatigue…"), praising a value bet that got *no* value, and hedging ("a disciplined fold might have been better") instead of calling `analyze_spot`.
|
In poker mode Lyra routes correctly but her replies are generic — one broad `_CASH_CARD` (`lyra/modes.py:66-116`) describes *traits* and gets injected on every turn, so the model satisfies it with safe, flattering abstraction. From real sessions: coaching essays on bare stack updates, false tilt/fatigue reads on neutral logistics ("table broke, it's 11:50pm" → "late-night fatigue…"), praising a value bet that got *no* value, and hedging ("a disciplined fold might have been better") instead of calling `analyze_spot`.
|
||||||
@@ -45,21 +118,23 @@ Both are independent of the classifier and immediately reduce mush in poker mode
|
|||||||
Cohesive home for poker prompting: the classifier, a lean always-on base, and the per-type fragments.
|
Cohesive home for poker prompting: the classifier, a lean always-on base, and the per-type fragments.
|
||||||
|
|
||||||
```
|
```
|
||||||
classify(user_msg: str) -> str # "HAND" | "STATUS" | "MENTAL" | "LOG" | "CHAT"
|
classify(user_msg: str) -> str # "READ"|"HAND"|"TABLE"|"MENTAL"|"STATUS"|"LOG"|"CHAT"
|
||||||
BASE: str # always-on poker rules (logging, session_state, rituals, equity)
|
BASE: str # always-on poker rules (logging, tools, session_state, rituals, equity)
|
||||||
FRAGMENTS: dict[str, str] # msg_type -> response-shape contract
|
FRAGMENTS: dict[str, str] # msg_type -> response-shape contract
|
||||||
fragment_for(msg_type: str | None) -> str # FRAGMENTS.get(msg_type, FRAGMENTS["CHAT"])
|
fragment_for(msg_type: str | None) -> str # FRAGMENTS.get(msg_type, FRAGMENTS["CHAT"])
|
||||||
```
|
```
|
||||||
|
|
||||||
`classify` is a **pure function** (no DB), unit-tested like `perceive.read`. Heuristic signals, first match wins in priority order:
|
`classify` is a **pure function** (no DB), unit-tested like `perceive.read`. Heuristic signals, first match wins in priority order (**updated 2026-07-04** — READ + TABLE added):
|
||||||
|
|
||||||
1. **HAND** — card tokens (regex `\b[2-9TJQKA][shdc]\b`, ≥2), or position tokens (UTG/MP/HJ/CO/BTN/SB/BB/"button"/"hijack"/"straddle"), or a street word (flop/turn/river) with a betting verb (bet/raise/call/fold/check/shove/limp/jam).
|
1. **READ** — *another player* did something. A handle/name/descriptor is the actor (not "I/me/my") followed by a poker action: "TAG limped A4o", "Jonathan called the 3bet", "the neck-tattoo guy shoved". Route → `add_read(name|descriptor, note)`. **Must beat HAND** — these carry card/position/verb tokens but are NOT Brian's hand. Signal: a leading proper-noun/handle/ALL-CAPS token or a descriptor phrase as the subject, with no first-person holding. (Hard case: disambiguating a bare "limped A4o" with no clear subject — default to HAND if he's the implied actor, READ if a named player is.)
|
||||||
2. **MENTAL** — first-person feeling: "I feel", "I'm tilted/steaming/fried/tired/frustrated/confident/stuck/bored", "on tilt", "in my head", "mental", "leak".
|
2. **HAND** — *Brian's* hand: first-person + card tokens (`\b[2-9TJQKA][shdc]\b`, ≥2) / position tokens (UTG/MP/HJ/CO/BTN/SB/BB/button/hijack/straddle) / a street word (flop/turn/river) with a betting verb. The fragment handles hero-vs-observed (`hero_involved`): if he only watched, treat as READ(s)/observed, don't analyze as his.
|
||||||
3. **STATUS** — logistics with no cards: "table broke", "new table", "waiting for a seat", "seat opened", "just sat", clock times, "heading to"/venue mentions.
|
3. **TABLE** — roster ops with a tool action: "seat the table: …", "table broke", "they broke us", "I got moved", "switched tables", "TAG left/busted", "new guy in seat 3". Route → `seat_players` / `clear_table` / `unseat_player`. (Was folded into STATUS; now distinct because it *does* something.)
|
||||||
4. **LOG** — bare money/result prose that slipped past the quick-capture box: "I'm at", "stack is", "down to", "up to", "out for", "cashed", "rebought", "rebuy" with a number.
|
4. **MENTAL** — first-person feeling: "I feel", "I'm tilted/steaming/fried/tired/frustrated/confident/stuck/bored", "on tilt", "in my head", "mental", "leak".
|
||||||
5. **CHAT** — default fallback (questions, open talk).
|
5. **STATUS** — pure logistics, no roster action, no cards: clock times, "waiting for a seat", "heading to"/venue mentions, bathroom/break. (Table changes moved to TABLE.)
|
||||||
|
6. **LOG** — bare money/result prose that slipped past the quick-capture box: "I'm at", "stack is", "down to", "up to", "out for", "cashed", "rebought", "rebuy" with a number.
|
||||||
|
7. **CHAT** — default fallback (questions, open talk).
|
||||||
|
|
||||||
(HAND wins over MENTAL so a described hand still gets logged even if he's venting; the HAND fragment tells her to acknowledge the feeling too.)
|
(READ beats HAND so a villain's action lands on their file, not Brian's. HAND beats MENTAL so a described hand still gets logged even if he's venting; the HAND fragment acknowledges the feeling too.)
|
||||||
|
|
||||||
### Injection (`mind.py`)
|
### Injection (`mind.py`)
|
||||||
|
|
||||||
@@ -78,7 +153,11 @@ fragment_for(msg_type: str | None) -> str # FRAGMENTS.get(msg_type, FRAGMENTS[
|
|||||||
|
|
||||||
### The fragments (concrete contracts, not traits)
|
### The fragments (concrete contracts, not traits)
|
||||||
|
|
||||||
**BASE** (always-on in poker) — distilled from the card's cross-cutting rules: log any trackable fact FIRST then reply (stack→`log_stack`, hand→`record_hand`, read→`add_read`, rebuy→`add_buyin`); for any equity/who's-ahead question call `analyze_spot`, never eyeball; when he asks where he's at (stack/net/gator), call `session_state` and answer from it; rituals (`scar_note`/`confidence_bank`/`alligator_blood`/`reset_ritual`) — run them in his language, honest punt-vs-cooler line, never invent one.
|
**BASE** (always-on in poker) — distilled from the card's cross-cutting rules. Log any trackable fact FIRST then reply, and **never claim a tool ran without calling it**. Tool routing (full current set as of 2026-07-04): his stack→`log_stack`; his hand→`record_hand`; a *villain's* action→`add_read` (with `name` for a real handle, or `descriptor` for a nameless player — a physical description in `name` spawns duplicates); rebuy→`add_buyin`; who's-at-the-table→`seat_players`/`unseat_player`/`clear_table`; attaching a caught name to a described player→`name_villain`; confirmed same/different person→`link_villains` (never merge on a guess). For any equity/who's-ahead question call `analyze_spot`, never eyeball. When he asks where he's at (stack/net/gator), call `session_state` and answer from it. Rituals (`scar_note`/`confidence_bank`/`alligator_blood`/`reset_ritual`) — run them in his language, honest punt-vs-cooler line, never invent one. (A `SCOUTING DESK` note may already be in context with a player's history — cite it, don't re-fetch or invent.)
|
||||||
|
|
||||||
|
**READ** *(new 2026-07-04)* — a villain did something and he wants it on their file. Call `add_read(name|descriptor, note)` FIRST, before replying — every time; this is the job that was silently getting skipped. A handle (often initials/ALL-CAPS like TAG) is a PERSON, not a play-style. If the player is on the roster, attach by that handle; if unnamed, use `descriptor`. Confirm in one short line ("Noted on TAG — limped A4o SB."). Optional: one crisp read if it's exploitable, but the log is mandatory, the commentary is not.
|
||||||
|
|
||||||
|
**TABLE** *(new 2026-07-04)* — roster management. "seat the table: …" → `seat_players`; a table change ("table broke", "I got moved", "switched tables") → `clear_table` then wait for the new roster; someone leaves/busts → `unseat_player`. Do the tool call, confirm one line, don't narrate. The session/stack keep going through a table change — only who's seated resets.
|
||||||
|
|
||||||
**HAND** — Log it (`record_hand`). Then **if it's NLH**: reason about **bet intent** — for each meaningful bet name what it was for (value / bluff / protection) and whether it worked (*a fold to a value bet = value left behind — flag it; a call of a bluff = it failed*); call `analyze_spot` for a close equity/who's-ahead spot; name leaks plainly (value-owning, missed value, sizing); give ONE real opinion. **No reflexive praise** — don't open with "nice hand." If a named villain is referenced, call `player_profile` first — never invent a read. **If it's PLO/Omaha or any non-NLH game**, just confirm it's logged and offer at most a light read; do NOT attempt NLH-style equity or deep analysis. Talk in prose, not a listicle. Seeded with 2 real-hand exemplars:
|
**HAND** — Log it (`record_hand`). Then **if it's NLH**: reason about **bet intent** — for each meaningful bet name what it was for (value / bluff / protection) and whether it worked (*a fold to a value bet = value left behind — flag it; a call of a bluff = it failed*); call `analyze_spot` for a close equity/who's-ahead spot; name leaks plainly (value-owning, missed value, sizing); give ONE real opinion. **No reflexive praise** — don't open with "nice hand." If a named villain is referenced, call `player_profile` first — never invent a read. **If it's PLO/Omaha or any non-NLH game**, just confirm it's logged and offer at most a light read; do NOT attempt NLH-style equity or deep analysis. Talk in prose, not a listicle. Seeded with 2 real-hand exemplars:
|
||||||
|
|
||||||
@@ -88,7 +167,7 @@ fragment_for(msg_type: str | None) -> str # FRAGMENTS.get(msg_type, FRAGMENTS[
|
|||||||
> *Brian:* "77 multiway, flop 4h5c3c, HJ bets 75, BTN shoves 74, I call. Turn/river check through, HJ wins with 64."
|
> *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."
|
> *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."
|
||||||
|
|
||||||
**STATUS** — He's narrating logistics (time, venue, table change, waiting for a seat). Acknowledge in 1–2 sentences, log a stack only if a bare number is present, then stop. **No coaching, no strategy dump, and do NOT read him as tilted/tired/impatient — a neutral update is not a mood.**
|
**STATUS** — Pure logistics with no roster action (time, venue, waiting for a seat, break). *(Table changes now route to TABLE.)* Acknowledge in 1–2 sentences, log a stack only if a bare number is present, then stop. **No coaching, no strategy dump, and do NOT read him as tilted/tired/impatient — a neutral update is not a mood.**
|
||||||
|
|
||||||
**MENTAL** — He told you how he's feeling. This is when he needs you most. Drop the shorthand, full presence, real voice — talk him down off tilt, hold him disciplined through a card-dead stretch, engage the mental game honestly. Never a clipped confirmation.
|
**MENTAL** — He told you how he's feeling. This is when he needs you most. Drop the shorthand, full presence, real voice — talk him down off tilt, hold him disciplined through a card-dead stretch, engage the mental game honestly. Never a clipped confirmation.
|
||||||
|
|
||||||
@@ -103,11 +182,14 @@ Flip `TOOL_BACKENDS = {"cloud"}` → `{"cloud", "mi50"}` (`chat.py:21`). Precond
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- **`classify` unit tests** (pure, no DB — mirror `test_perceive.py` top): real messages from the transcripts →
|
- **`classify` unit tests** (pure, no DB — mirror `test_perceive.py` top): real messages from the transcripts →
|
||||||
`"Button straddle on. I limp UTG with 22. Flop 2d7cjh…"` → `HAND`;
|
`"TAG limped A4o in the SB (UTG straddled)"` → `READ` (villain action, must NOT be HAND);
|
||||||
`"table broke, it's 11:50pm"` → `STATUS`;
|
`"Button straddle on. I limp UTG with 22. Flop 2d7cjh…"` → `HAND` (first-person);
|
||||||
|
`"seat the table: TAG, Jonathan, Wheelz"` → `TABLE`; `"table broke, I'm at a new table"` → `TABLE`;
|
||||||
|
`"it's 11:50pm, waiting for a seat"` → `STATUS`;
|
||||||
`"I feel like I'm being mean when I raise"` → `MENTAL`;
|
`"I feel like I'm being mean when I raise"` → `MENTAL`;
|
||||||
`"I'm at 317 now"` → `LOG`;
|
`"I'm at 317 now"` → `LOG`;
|
||||||
`"should I have folded the river?"` → `CHAT` (no cards) — or `HAND` if cards present.
|
`"should I have folded the river?"` → `CHAT` (no cards) — or `HAND` if cards present.
|
||||||
|
Include the READ-vs-HAND boundary explicitly (named subject → READ; first-person → HAND).
|
||||||
- **`build_messages` fragment injection** (blob-join pattern from `test_chat.py:57-70`): in poker mode, a HAND message includes the HAND fragment string and NOT the STATUS one; a STATUS message includes STATUS and NOT HAND; assert `poker_prompts.BASE` is always present in poker mode.
|
- **`build_messages` fragment injection** (blob-join pattern from `test_chat.py:57-70`): in poker mode, a HAND message includes the HAND fragment string and NOT the STATUS one; a STATUS message includes STATUS and NOT HAND; assert `poker_prompts.BASE` is always present in poker mode.
|
||||||
- **Pipeline fixes**: `assemble` in poker mode on a tilt-lexicon message → `turn.register is None` and no tilt note in the system blob (nudge suppressed); the mode-menu note string is absent in poker mode and present in a non-poker mode.
|
- **Pipeline fixes**: `assemble` in poker mode on a tilt-lexicon message → `turn.register is None` and no tilt note in the system blob (nudge suppressed); the mode-menu note string is absent in poker mode and present in a non-poker mode.
|
||||||
- **No regressions**: full suite green (currently 123).
|
- **No regressions**: full suite green (currently 123).
|
||||||
|
|||||||
Reference in New Issue
Block a user