promotional bannermobile promotional banner

Loot & Baloot

Loot & Baloot — Play Saudi Baloot (بلوت) inside World of Warcraft. A four-player trick-taking card game over the party addon channel. Host with friends or play solo against bots, with full Hokm / Sun rules, melds, escalations, and SWA claims.

File Details

v3.2.0

  • R
  • May 11, 2026
  • 5.16 MB
  • 13
  • 12.0.5
  • Retail

File Name

WHEREDNGN-v3.2.0.zip

Supported Versions

  • 12.0.5

Loot & Baloot

v3.2.0 (2026-05-11)

Full Changelog Previous Releases

  • docs: amend v3.2.0 release notes per Codex review
    Three Codex amends to the v3.2.0 CHANGELOG entry:
    1. Bot/Bidding.lua bullet no longer claims a public Bot.PickAshkal
      function (there is none — Ashkal is an internal decision path
      inside Bot.PickBid). Lists the actual public symbols:
      Bot.PickBid, Bot.PickPreempt, Bot.PickOvercall, plus a
      note about the internal Ashkal path.
    2. Bot.lua line-count claim softened to "about 8.4k → about 6.1k,
      roughly 2,300 lines" to avoid tying public release notes to a
      brittle exact wc-l number across tool/line-ending variation.
    3. Harness count framing: "1,106 → 1,219 passing checks, covering
      behavioral paths plus source-pin regression guards" instead of
      calling every assertion a "behavioral pin" (the harness mixes
      behavioral checks with source-pin regression assertions).
      CHANGELOG.md only. No runtime, test, .toc, .pkgmeta, workflow, or
      experimental-branch changes. Tag still held for Codex review.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 release notes (maintenance / internal restructure)
    Document the v3.2.0 cleanup wave outcomes:
    • Bot/Tiers.lua, Bot/PlayPrimitives.lua, Bot/Bidding.lua,
      Bot/Escalation.lua, UI/Themes.lua extractions
    • Bot.lua reduced from 8,451 to 6,078 lines (-28%)
    • Harness up from 1,106 to 1,219 behavioral pins
    • Explicit "no intended gameplay / UI / protocol / saved-variable /
      scoring changes" framing
      Release-prep only. No runtime / test / .toc / .pkgmeta edits in this
      commit. Tag is held for Codex review per release-prep workflow.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 release-readiness checkpoint
  • docs(batch9): normalize whitespace + correct harness counts (+30 / 1219)
    Three doc/whitespace amendments after Codex review of Batch 9
    implementation. No runtime behavior changes.
    1. Bot/Escalation.lua line-ending normalization
      The original verbatim move concatenated the new file's header
      (Python \n LF literals) with the moved escalation cluster (which
      uses CRLF throughout the rest of the repo on Windows). Result:
      ~33 lines of bare LF in the module header followed by ~595 lines
      of CRLF in the moved code. git diff --check fed7a46...HEAD
      flagged every line where the diff hunk straddled an LF/CRLF
      boundary as "trailing whitespace" (CR before LF reads as trailing
      CR). Normalize the whole file to CRLF + strip trailing whitespace.
    2. .swarm_findings/v3_2_0_batch9_escalation_design.md
      Replace the design-pass estimated +18 / ~1207 references with
      the actual implementation result:
      • 45 AJ.9g asserts added (not 18)
      • -6 AJ.9f-bind retirements (the Batch 8 Bidding re-binding
        header on Bot.lua was removed in Batch 9, so the 6 AJ.9f-bind
        asserts that previously checked for its presence are now
        stale and were retired)
      • -9 retarget side-effects net
      • Total: +30 (1189 baseline -> 1219 final harness)
        Updated 7 spots: AJ.9g delta line, delta breakdown table,
        §6D expected count, §6A implementation step 10, §8C tests
        added, §8D harness count delta, §8 summary table.
    3. tests/test_state_bot.lua AJ.9f block comment refresh
      The block-leading comment claimed "Bot.lua re-binds 6 helpers
      (suitStrengthAsTrump + ...) consumed by escalation deciders."
      That was true post-Batch 8 but stale post-Batch 9 (Batch 9
      moved the consumers AND the re-binding header to Bot/Escalation
      .lua). Replace with a HISTORICAL NOTE clearly stating:
      • Batch 8 originally added the 6-locals header to Bot.lua
      • Batch 9 migrated the header WITH the escalation deciders to
        Bot/Escalation.lua
      • AJ.9g (below) enforces both the new location AND the absence
        of the old header from Bot.lua
        The bidderHoldsBidcard stays-in-Bot.lua note is preserved.
        Verification:
    • git diff --check fed7a46...HEAD: clean
      - Full harness: 1219/1219 pass (unchanged)
      - tests/test_H7_sun_shortest_lead.lua: 9/0 pass
      No runtime Lua touched (Bot/Escalation.lua's whitespace
      normalization is metadata-only; the semantic content is unchanged).
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • v3.2.0 cleanup batch 9: extract Bot/Escalation.lua
    Move the four-rung escalation chain (Bel x2 -> Triple x3 -> Four x4 ->
    Gahwa match-win) out of Bot.lua into a new Bot/Escalation.lua module.
    Mirrors the Batch 5B / 5C / 8 extraction pattern: new file loaded
    before Bot.lua, public picker functions set directly on the shared
    B.Bot table (no Bot.Escalation sub-table), file-local helpers and
    jitter constants live inside the new module.
    Moved to Bot/Escalation.lua:
    Public on B.Bot.:
    * Bot.PickDouble (defender's Bel response, ~231 lines)
    * Bot.PickTriple (bidder's Triple counter, ~71 lines)
    * Bot.PickFour (defender's Four counter, ~83 lines)
    * Bot.PickGahwa (bidder's terminal Gahwa, ~55 lines)
    File-local helpers (3):
    * escalationStrength -- shared bid-strength calc for the 3
    bidder-side pickers (~77 lines)
    * selfStyleJitterBonus -- Fzloky+ jitter-widening based on the
    seat's lifetime escalation count
    * styleBelTendency -- partner-style tendency consumed only
    by Bot.PickTriple
    Per-rung jitter constants (4):
    * BEL_JITTER = 10 (Bel)
    * TRIPLE_JITTER = 12
    * FOUR_JITTER = 15
    * GAHWA_JITTER = 18 (highest variance for terminal rung)
    Total: 7 file-local moved symbols/consts + 4 public picker functions
    = 11 moved symbols. Plus inline copies of jitter + shuffledSuits
    (utility duplicates, mirroring Batch 8's Bot/Bidding.lua pattern).
    Stays in Bot.lua:
    * Bot.OnEscalation, Bot.OnRoundEnd -- style-ledger updaters
    called from State.lua's ApplyDouble/Triple/Four/Gahwa
    * emptyStyle -- style-ledger initializer
    * styleTrumpTempo -- consumed by pickLead/pickFollow
    * bidderHoldsBidcard, meldKnownHeld, anyOpponentVoidIn -- play helpers
    * jitter, shuffledSuits, BEL_JITTER (intentionally NOT in Bot.lua
    after this move; the original BEL_JITTER lived inside the
    escalation cluster, now in Bot/Escalation.lua)
    * Bot._partnerStyle, Bot._memory tables (on shared B.Bot.
    )
    Bot.lua re-binding header removal:
    The 6-locals Bidding re-binding header introduced in Batch 8 (to
    feed escalation deciders consuming Bidding helpers) is now DELETED
    because all consumers (escalation deciders) moved to
    Bot/Escalation.lua. Net.lua and other Bot.lua call sites that
    previously consumed those Bidding helpers via the re-binding are
    grep-confirmed absent. Bot.lua's surface SHRINKS rather than grows.
    Style-ledger comment refresh:
    The stale shared "Convenience derived metrics... Currently unused
    by the picker code" comment that previously sat above both
    styleBelTendency and styleTrumpTempo was incorrect (both ARE used
    by picker code). The comment is refreshed in Bot.lua above the now-
    alone styleTrumpTempo to truthfully name its consumers
    (pickLead/pickFollow). styleBelTendency carries a concise truthful
    comment to Bot/Escalation.lua naming PickTriple as its only caller.
    Bot/Escalation.lua imports:
    * 6 helpers from Bot.Bidding (suitStrengthAsTrump, sunStrength,
    partnerBidBonus, partnerEscalatedBonus, combinedUrgency,
    opponentUrgency) re-bound as file-locals
    * Bot.IsAdvanced / Bot.IsBotSeat / Bot.IsFzloky via shared B.Bot
    * Bot._partnerStyle / Bot._memory via shared B.Bot
    * K / C / R / S (Constants / Cards / Rules / State)
    * inline jitter + shuffledSuits
    WHEREDNGN.toc: Bot/Escalation.lua loads after Bot/Bidding.lua and
    before Bot.lua.
    11 test loaders each gain one new load line for Bot/Escalation.lua
    (between Bot/Bidding.lua and Bot.lua).
    Source-pin retargets in tests/test_state_bot.lua (7 do-blocks):
    AA.1a/b (escalationStrength void/sideAce bonuses)
    AB.2 (PickGahwa DEAD-2 rationale)
    AD.3 (duplicate PickGahwa DEAD-2)
    AD.7a/b (PickDouble eltrace helper + eval line)
    AH.3 (PickTriple floor cap)
    AH.7 (escalationStrength Sun-penalty neutralization comment)
    AI.4 (PickDouble bid-history inflection marker)
    AJ.9f-bind block retired:
    The 6-iteration AJ.9f-bind assertions that previously checked
    Bot.lua's Bidding re-binding header are now obsolete (header
    removed). Replaced with a commentary block; new AJ.9g block
    contains stricter assertions including ABSENCE of the header in
    Bot.lua.
    New AJ.9g source-pin block (45 asserts):
    * 3 helper presence asserts in Bot/Escalation.lua
    * 4 public picker presence asserts
    * 4 jitter constant presence asserts
    * 6 Bidding helper import asserts
    * 5 .toc-order asserts (including escalation slot)
    * 2 negative-export asserts (no Bot.Escalation sub-table)
    * 5 Bot.lua state asserts (no Bidding header, styleTrumpTempo +
    OnEscalation + OnRoundEnd stay, escalation helpers absent)
    * 7 "did NOT define" asserts (3 helpers + 4 pickers absent from
    Bot.lua) -- the loop runs 4 times for pickers
    Plus 9 misc
    Bot.lua dropped from 6,673 raw lines to 6,078 raw lines (-595 net).
    Bot/Escalation.lua is 682 raw lines.
    Verification:
    * Full harness: 1219/1219 pass (1189 baseline + 30 net)
    * Standalone smokes all green:
    - tests/test_H7_sun_shortest_lead.lua 9 passed
    - tests/test_H1_pin_J9_trump.lua 11 passed
    - tests/test_numworlds_scaling.lua 21 passed
    - tests/test_v0.5_traced_game.lua 10 passed
    - tests/test_bel_decision_quality.lua 1000x3 sweep clean
    No behavior change. No release tag. No Bot.Escalation sub-table
    introduced. Net.lua call sites for B.Bot.PickDouble/Triple/Four/Gahwa
    resolve through the shared table unchanged.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 cleanup batch 9 (Bot/Escalation.lua) design/inventory doc
  • docs(batch8): fix remaining stale counts in design doc
    Five more stale references in .swarm_findings/v3_2_0_batch8_bidding_design.md
    left over from the original "14 helpers / 5-locals / 1178 harness"
    plan, written before the late suitStrengthAsTrump move:
    • AJ.9f helper presence asserts count: 14 -> 15
    • AJ.9f delta table: +32 -> actual +41 (with -1 retarget side
      effect = net +40)
    • Risk register row #2: re-binding header "7 locals" -> "6 locals"
      with the explicit name list
    • Section 9 "Why this scope (not smaller)" rationale:
      "Bot.lua re-binding header stays compact at 7 locals" -> "6 locals"
    • Section 9 implementation expected harness: "1178 / 1178" ->
      actual "1189 / 1189" with the verified commit hash
      Comment / doc only. No runtime logic changes.
      Verification:
      • Full harness still 1189/1189 pass
      • tests/test_H7_sun_shortest_lead.lua still 9/0 pass
        Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs(batch8): align wording with final 15-helper / 6-locals implementation
    Stale comment / doc wording from the original "14 helpers / 5 Bot.lua
    re-bindings" plan, written before the late suitStrengthAsTrump
    move that was forced by grep-confirming PickDouble/PickTriple still
    called it.
    Updates (comment / doc only — no runtime logic changes):
    • .swarm_findings/v3_2_0_batch8_bidding_design.md
      • 14 file-local helpers -> 15
      • 5-locals re-binding header -> 6-locals (suitStrengthAsTrump added)
      • 18 in-scope symbols -> 19 in-scope
      • 23 symbols moved -> 25 symbols moved
      • Three non-contiguous source ranges -> four (adds 962-1014 for
        suitStrengthAsTrump)
      • Expected harness count 1181 -> actual 1189 (verified on this
        branch at commit c699812)
      • AJ.9f assert count 32 -> actual 41
    • Bot/Bidding.lua header comment
      • 14 bidding helpers -> 15 (suitStrengthAsTrump prepended to the list)
      • "Five of them are also re-exported" -> "Six of them"
    • Bot.lua breadcrumb at original PickBid location
      • "The 5-locals re-binding header" -> "The 6-locals re-binding header"
      • Adds suitStrengthAsTrump to the explicit helper list
    • tests/test_H7_sun_shortest_lead.lua loadFile comment
      • "14 file-local helpers (incl. sunStrength / ...)" -> "15 file-local
        helpers (incl. suitStrengthAsTrump / sunStrength / ...)"
      • "Bot.lua's 5-locals re-binding header" -> "6-locals"
        Verification:
      • Full harness still 1189/1189 pass
      • tests/test_H7_sun_shortest_lead.lua still 9/0 pass
        No runtime behavior changes. No release tag.
        Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • v3.2.0 cleanup batch 8: extract Bot/Bidding.lua
    Move the bidding-window deciders + 15 file-local helpers out of
    Bot.lua into a new Bot/Bidding.lua module. Mirrors the Batch 5B / 5C
    extraction pattern: new file loaded before Bot.lua, public functions
    set directly on the shared B.Bot table, file-local helpers exposed
    via a narrow B.Bot.Bidding.* sub-table for Bot.lua's re-binding
    header.
    Moved to Bot/Bidding.lua:
    Public on B.Bot.:
    * Bot.PickBid (R1/R2 bidding decision; ~590-line decider)
    * Bot.PickPreempt (triple-on-Ace pre-emption)
    * Bot.PickOvercall (Sun overcall window)
    * Bot.OpponentUrgency (consumed by BotMaster.lua)
    Test-internal export:
    * Bot._beloteBypassQualifies = beloteBypassQualifies
    File-local helpers (15):
    * suitStrengthAsTrump, sideSuitAceBonus, hokmMinShape, sunMinShape
    * beloteSuit, beloteBypassQualifies, aceCountAndMardoofa
    * withBidcard, sunStrength
    * partnerBidBonus, scoreUrgency, opponentUrgency
    * matchPointUrgency, combinedUrgency, partnerEscalatedBonus
    Tuning aliases (moved from Bot.lua:24-60):
    * TH_HOKM_R1_BASE, TH_HOKM_R2_BASE, TH_SUN_BASE, BID_JITTER
    * BEL_JITTER (new file-local in Bot/Bidding.lua for PickPreempt's
    jitter call; mirrors Bot.lua:5777 which stays for escalation)
    Plus inline copies of jitter + shuffledSuits (4-line pure helpers).
    Stays in Bot.lua:
    * bidderHoldsBidcard (line 979 — only consumed by pickLead/pickFollow
    trump-J inference at Bot.lua:2150)
    * meldKnownHeld (only consumed by pickLead/pickFollow)
    * jitter, shuffledSuits, BEL_JITTER (used everywhere else)
    * Escalation deciders (PickDouble/Triple/Four/Gahwa), escalationStrength,
    styleBelTendency, selfStyleJitterBonus
    * All play deciders (pickLead, pickFollow, PickAKA, PickPlay, PickMelds,
    PickKawesh, PickTakweesh, PickSWA, PickSWAResponse)
    * Memory/style ledgers (Bot._memory, Bot._partnerStyle)
    Bot.lua re-binding header (6 file-locals, just below the Batch 5C
    Primitives header):
    local suitStrengthAsTrump = Bidding.suitStrengthAsTrump
    local sunStrength = Bidding.sunStrength
    local partnerBidBonus = Bidding.partnerBidBonus
    local partnerEscalatedBonus = Bidding.partnerEscalatedBonus
    local combinedUrgency = Bidding.combinedUrgency
    local opponentUrgency = Bidding.opponentUrgency
    scoreUrgency and matchPointUrgency intentionally stay file-local in
    Bot/Bidding.lua — grep-confirmed they have zero call sites in
    Bot.lua post-move (only 2 comment references at lines 6040, 6289).
    WHEREDNGN.toc: Bot/Bidding.lua loads between Bot/PlayPrimitives.lua
    and Bot.lua.
    11 test loaders each gain one new load line for Bot/Bidding.lua.
    Source-pin retargets in tests/test_state_bot.lua: ~19 pins
    (R.2, T.2, T.3, X.2, X.3, Y.2b, Y.3, Z.1-Z.3, Z.5, AD.1, AD.9,
    AF.1-AF.3, AH.6, P.8) switched io.open path from /Bot.lua to
    /Bot/Bidding.lua.
    New AJ.9f source-pin block (41 asserts):
    * 15 local-function presence asserts
    * 4 public-function presence asserts
    * 1 Bot._beloteBypassQualifies test-export assert
    * 1 BEL_JITTER local presence assert
    * 6 Bidding.
    export asserts
    * 2 negative-export asserts (scoreUrgency, matchPointUrgency NOT exported)
    * 4 .toc-order asserts
    * 6 Bot.lua re-binding header asserts
    * 2 bidderHoldsBidcard location asserts
    Bot.lua dropped from ~8070 raw lines to ~6671 raw lines (-1399).
    Bot/Bidding.lua is 1534 raw lines.
    Verification:
    * Full harness: 1189/1189 pass (1149 baseline + 40 net)
    * Standalone smokes:
    - tests/test_H7_sun_shortest_lead.lua 9 passed
    - tests/test_H1_pin_J9_trump.lua 11 passed
    - tests/test_numworlds_scaling.lua 21 passed
    - tests/test_v0.5_traced_game.lua 10 passed
    - tests/test_bel_decision_quality.lua 1000x3 sweep clean
    No behavior change. No release tag.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 cleanup batch 8 (Bot/Bidding.lua) design/inventory doc
  • docs(batch7): align inventory table with final scope
  • v3.2.0 cleanup batch 7: convert bidding pins to behavior
  • docs: add v3.2.0 cleanup batch 7 design/inventory doc
  • docs(batch6): correct projected test-count delta
  • v3.2.0 cleanup batch 6: convert bidding pins to behavior
  • docs: add v3.2.0 cleanup batch 6 design/inventory doc
  • docs(checkpoint): amend per Codex review
    Documentation accuracy fixes flagged by Codex:
    • Current State table: main commit was listed as 6ae91e1 (the H7
      comment polish); update to the actual current HEAD 3b333eb (the
      checkpoint doc commit). Keep 6ae91e1 as a separate "previous
      polish commit" row so the H7 polish is still visible.
    • Module line-count table: switch from raw wc -l counts to
      non-blank-line counts (empty rows excluded, comment rows
      included). This is the metric Codex normalizes against
      batch-to-batch. New totals:
      Bot.lua 7 875 (was 8 070 raw)
      Net.lua 6 205 (was 6 430 raw)
      UI.lua 4 534 (was 4 745 raw)
      State.lua 2 464 (was 2 570 raw)
      ... and the rest, total 26 918 (was 27 987 raw).
    • Subheadings updated: "What remains in Bot.lua (7 875 lines)",
      same for UI.lua / Net.lua / State.lua.
    • Summary sentence under "Why not immediate 5D?" updated to use the
      new totals and corrects the "1 040 lines moved" → "694 non-blank
      lines moved" (UI/Themes 247 + Bot/Tiers 75 + Bot/PlayPrimitives
      372).
    • Added a clarifying note inside "What remains in Bot.lua" that the
      internal ~xxxx-yyyy address ranges are raw source-line addresses
      (what your editor shows), to avoid confusion with the non-blank
      total in the section heading.
      Documentation-only amend. No runtime Lua touched. No release tag.
      Full harness still 1150/1150.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 post-cleanup checkpoint
    Pause point after the v3.2.0 cleanup/decomposition wave. No new
    extraction batch from this doc — design/audit checkpoint only.
    Contents:
    1. Current state (commit, last tag, harness, branches)
    2. Cleanup stack summary (Batches 1-5C + bel-quality shim +
    H7 comment polish)
    3. Module map (line counts, what remains in Bot.lua / UI.lua /
    Net.lua / State.lua post-extraction)
    4. Test and source-pin health (1150/1150 + 5 standalone smokes,
    pin counts per file, AJ.9c/d/e migrations)
    5. Remaining risk register (4B/4C/4D, Bot/Memory, Bot/Escalation,
    Bot/Bidding, UI renderer)
    6. Recommended next work (ranked design-pass candidates) — top
    recommendation is a Batch 3-style source-pin-to-behavioral
    conversion of 3-6 high-value pins as prep before Bot/Bidding.
    7. Release guidance (v3.1.14 remains shipped; criteria for what
    would justify v3.2.0)
    No runtime code changed.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • test(H7): refresh stale comment about hunk-1 injection point
    The "Apply H-7 patch" prose at the top of the file still said hunk 1
    adds cardsOfSuit "immediately after the closing end of lowestByRank."
    That was true pre-Batch 5C when both lowestByRank and highestByRank
    lived in Bot.lua. After Batch 5C those primitives moved to
    Bot/PlayPrimitives.lua and the actual anchor (already updated below
    at ANCHOR_HELPER) is \nlocal function pickLead, so cardsOfSuit now
    lands immediately before pickLead.
    Comment-only change. Anchor / helper block / Sun-branch anchor / load
    order all unchanged. H7 standalone still 9/0; full harness still
    1150/1150.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • v3.2.0 cleanup batch 5C: extract Bot/PlayPrimitives.lua
    Move the ten play-primitive helpers out of Bot.lua into a new
    Bot/PlayPrimitives.lua module:
    * pickRandomTied
    * lowestByRank
    * highestByRank
    * highestByFaceValue
    * holdsBeloteThusFar
    * highestTrump
    * legalPlaysFor
    * wouldWin
    * tahreebClassify
    * applyClosedTrumpLeadGate
    Exported via the B.Bot.Primitives sub-table (UI/Themes pattern).
    Inter-primitive calls (lowest/highest/highestByFaceValue →
    pickRandomTied) stay as file-local closures inside the new module.
    Bot.lua gains an 11-locals re-binding header at the top of the
    chunk, right after the Batch 5B tier breadcrumb, so every existing
    call site below (pickLead / pickFollow / PickPlay / PickAKA /
    escalation deciders / etc.) resolves through file-locals unchanged.
    The "-- Play" section header at the original location stays in
    Bot.lua with a short breadcrumb in place of the moved block.
    WHEREDNGN.toc: add Bot/PlayPrimitives.lua between
    Bot/Tiers.lua and Bot.lua in the # Game runtime section.
    Test loaders (11 files) each gain one new line:
    load("Bot/PlayPrimitives.lua") -- between Tiers and Bot.lua
    H7 anchor update: tests/test_H7_sun_shortest_lead.lua swaps
    ANCHOR_HELPER from \nlocal function highestByRank to
    \nlocal function pickLead. highestByRank moved to
    PlayPrimitives.lua so the old anchor no longer matches; pickLead
    is the next stable file-scope symbol and is deferred indefinitely
    from extraction. The patched code's lowestByRank references
    resolve through Bot.lua's re-binding header. The Sun-branch hunk
    anchor (inside pickLead's body) is unchanged.
    Tests:
    * AJ.9e (new): 10 def + 10 export + 3 toc + 10 bind = 33 source
    pins asserting Bot/PlayPrimitives.lua presence, exports, .toc
    order, and Bot.lua's re-binding header.
    * AN.1a / AN.1b (UPDATED per Codex correction): now scan
    Bot/PlayPrimitives.lua for the v3.0.3 GAP-01 marker and the
    return "want_hint" string (both moved with tahreebClassify).
    AN.1c (cls == "want_hint" consumer in pickLead/pickFollow)
    keeps scanning Bot.lua.
    * AN.8a / AN.8b / AN.8c (UPDATED per Codex correction): now scan
    Bot/PlayPrimitives.lua for the v3.0.6 SENDER-INTENT marker,
    classifier-side lenAtFirstDiscard read, and if lenAtFirst >= 3 then gate. The recorder-side lenAtFirstDiscard write
    stays in Bot.lua so scanning Bot.lua would give false positives
    for the classifier code.
    Verification:
    * Full harness: 1150/1150 pass (+33 from AJ.9e source pins).
    * Standalone smokes:
    - test_H1_pin_J9_trump.lua 11 passed
    - test_H7_sun_shortest_lead.lua 9 passed <- anchor swap
    - test_numworlds_scaling.lua 21 passed
    - test_v0.5_traced_game.lua 10 passed
    - test_bel_decision_quality.lua 1000×3 sweep clean
    No behavior change. No release tag. No Bot.PickBid / pickLead /
    pickFollow / memory / style ledger / escalation / BotMaster /
    Net / State / UI / Slash / saved-variable / protocol / gameplay
    changes.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 cleanup batch 5C design/inventory doc
    Plans the Bot/PlayPrimitives.lua extraction — the deferred Option B
    from the Batch 5B design pass.
    Implementation shape:
    * 10 functions moved to Bot/PlayPrimitives.lua under B.Bot.Primitives
    * Bot.lua gains a 14-line re-binding header (UI/Themes pattern)
    * inter-primitive calls stay file-local in the new module
    * test_H7 anchor swaps from \nlocal function highestByRank to
    \nlocal function pickLead (pickLead is deferred indefinitely
    so the new anchor is stable)
    * 11 test loader edits mirror the Batch 5B shape
    Risk class MEDIUM (Batch 5B was VERY LOW, 5A was LOW). Failure
    modes:
    - Re-binding header mis-order
    - ~155 lines of audit comments in tahreebClassify must move
    verbatim
    - test_H7 anchor must land at file-scope, not inside a body
    Defer: pickLead/pickFollow/PickBid/memory/escalation deciders.
    No runtime code changed.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • test(bel_quality): fix math.random shim arity dispatch
    The standalone bel_decision_quality runner errored under Lupa when
    Bot.lua picker code called math.random() (0 args) or math.random(n)
    (1 arg) — the shim unconditionally forwarded as origRandom(a, b),
    passing nil arguments that Lua's stdlib rejects.
    Dispatch on argument presence:
    math.random() → origRandom() — Fisher-Yates, probability
    math.random(n) → origRandom(n) — shuffledSuits, pickRandomTied
    math.random(-10, 10) → 0 — jitter freeze (unchanged)
    math.random(a, b) → origRandom(a, b) — general 2-arg
    The jitter freeze guard at the top still works as before because it
    requires both args present.
    Refresh bel_decision_quality.json with the now-real 1000-hand sweep
    output (the prior file was stale since the runner errored before
    producing data).
    Full harness still 1117/1117.
    Standalone smoke python tests/run_bel_decision_quality.py now
    completes the full sweep cleanly.
    No runtime addon code changed — test-harness fix only.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • v3.2.0 cleanup batch 5B: extract Bot/Tiers.lua
    Move the five tier-detection predicates out of Bot.lua into a new
    Bot/Tiers.lua module:
    * Bot.IsAdvanced
    * Bot.IsM3lm
    * Bot.IsFzloky
    * Bot.IsSaudiMaster
    * Bot.IsBotSeat
    These are already public on the shared B.Bot.* table, so every
    existing call site inside Bot.lua (Bot.IsAdvanced(), etc.)
    continues to resolve through local Bot = B.Bot unchanged. No
    re-binding header in Bot.lua is needed — the predicates simply
    arrive on the shared table earlier in load order.
    WHEREDNGN.toc: add Bot/Tiers.lua between State.lua and Bot.lua in
    the # Game runtime section.
    Test loaders: 11 test files load Bot.lua directly. Each now also
    loads Bot/Tiers.lua after State.lua and before Bot.lua:
    * tests/test_state_bot.lua
    * tests/test_botmaster.lua
    * tests/test_multiseed_metrics.lua
    * tests/test_asymmetric_metrics.lua
    * tests/test_baseline_metrics.lua
    * tests/probe_defender_strength.lua
    * tests/test_bel_decision_quality.lua
    * tests/test_v0.5_traced_game.lua
    * tests/test_H1_pin_J9_trump.lua
    * tests/test_numworlds_scaling.lua
    * tests/test_H7_sun_shortest_lead.lua
    The H7 test patches Bot.lua source manually; the new loadFile call
    runs before the patched chunk compiles. The H7 anchor on
    \nlocal function highestByRank is preserved by this batch — the
    PlayPrimitives extraction that would invalidate it is deferred to
    Batch 5C.
    Tests (new section AJ.9d in test_state_bot.lua):
    * 5 source pins asserting Bot/Tiers.lua defines each function
    * 3 .toc-order pins asserting Bot/Tiers.lua loads before Bot.lua
    Full harness: 1117/1117 pass (+8 from new AJ.9d asserts).
    No behavior change. No release tag.
    Tier API symmetry preserved: Bot.IsBotSeat is kept as a thin proxy
    to S.IsSeatBot (NOT collapsed into direct S.IsSeatBot calls), per
    the Tier 3 audit comment in Bot/Tiers.lua.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 cleanup batch 5B design/inventory doc
    Evaluates Bot-side decomposition slices after UI/Themes.lua.
    Inventories two candidate areas:
    * Bot/Tiers.lua (5 tier-detection functions, ~60 lines)
    * Bot/PlayPrimitives.lua (10 hand-shape helpers, ~330 lines)
    Cross-module coupling check confirms both blocks are consumed only
    inside Bot.lua. BotMaster / Net / State / UI / Slash are uncoupled.
    Source-pin sweep: zero existing pins on any of the 15 candidate
    function names in test_state_bot.lua. One hard pin in
    test_H7_sun_shortest_lead.lua (anchor on highestByRank) only affects
    the PlayPrimitives option.
    Recommended scope: Option A — extract Bot/Tiers.lua only. Smallest
    slice that proves the Bot-side .toc extraction pattern. No Bot.lua
    re-binding header needed because tiers are already on the public
    B.Bot.* table.
    Defer Option B (PlayPrimitives) to Batch 5C.
    No runtime code changed.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • v3.2.0 cleanup batch 5A: extract UI/Themes.lua
    Move the card/felt theme data + palette + theme helpers out of
    UI.lua into UI/Themes.lua. UI.lua now binds the moved symbols as
    file-locals from the shared U.Theme table near the top of the file,
    so every existing call site (COL.feltDark, cardTexturePath(...),
    applyThemeColors(), CARD_STYLES[name], etc.) resolves unchanged.
    Moved:
    * CARD_STYLES, FELT_THEMES (independent theme axes)
    * COL palette table (shared reference; in-place mutations from
    applyThemeColors() propagate to UI.lua readers automatically)
    * migrateLegacyTheme (one-shot WHEREDNGNDB.cardTheme migration)
    * activeCardStyleName / activeFeltThemeName resolvers
    * cardStyleData / feltThemeData accessors
    * applyThemeColors palette stamp
    * CARD_TEX_DIR + cardTexturePath texture resolver
    The file-load-time calls (migrateLegacyTheme(), applyThemeColors())
    now run inside UI/Themes.lua, which the .toc loads immediately
    before UI.lua. Identical ordering to the pre-extraction code, just
    one file earlier in the chain.
    Kept in UI.lua (still need shared upvalues f / tablePanel /
    lobbyPanel / seatBadges / cardBackEntries):
    * U.SetCardStyle, U.SetFeltTheme, U.SetTheme
    * U.GetCardStyles, U.GetFeltThemes, U.GetThemes
    * U.GetActiveCardStyle, U.GetActiveFeltTheme, U.GetActiveTheme
    WHEREDNGN.toc: add UI/Themes.lua to the # UI section before
    UI.lua.
    Tests:
    * AJ.9 (deck-name renames): now scans UI.lua + UI/Themes.lua
    concatenated so the "4 Colors" / "Ba8ala SET" name assertions
    stay valid no matter which file currently owns the table.
    * New AJ.9c: source-pin that .toc lists UI/Themes.lua before
    UI.lua (reordering would break load).
    Full harness: 1109/1109 pass (+3 from new AJ.9c asserts).
    No behavior change. No release tag.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 cleanup batch 5 design/inventory doc
    Inventory + decomposition proposal for the large UI/Bot surfaces.
    No runtime code changed.
    Recommended Batch 5A scope: extract UI/Themes.lua (CARD_STYLES,
    FELT_THEMES, COL, theme helpers, ~180 lines moved out of UI.lua).
    Pure data + almost-pure helpers with zero behavioral coupling and
    a clean .toc load-order seam.
    Explicit deferrals: pickLead/pickFollow, Bot._memory move,
    PickBid, UI frame renderers, Net.lua/State.lua decomposition.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • v3.2.0 cleanup batch 4A: skip/preempt retry coverage
    Per CLAUDE_V3_2_0_CLEANUP_BATCH4A_PROMPT.md scope. Tight 5-item
    batch: 2 raw→helper migrations + 3 helper retry additions. No
    UI/bot strategy/rule changes. SendSkipTriple/Four/Gahwa intentionally
    remain one-shot (no clean post-apply guard available).
    Net.lua senders changed:
    1. _HostBelTimeout preempt_pass AFK path (Net.lua:5438 pre-fix)
      Raw broadcast(MSG_PREEMPT_PASS, seat)N.SendPreemptPass(seat).
      AFK path now inherits the v3.2.0 retry below.
    2. MaybeRunBot bot SWA dispatch (Net.lua:6147 pre-fix)
      Raw broadcast(MSG_SWA_REQ, seat, enc)N.SendSWAReq(seat, enc).
      Bot SWA path now inherits the v3.1.12 SendSWAReq retry.
    3. N.SendSkipDouble — retry added
      Post-apply guard: phase==PHASE_DOUBLE AND belPending~=nil AND
      not pendingContains(belPending, seat). Empty belPending (non-host
      final-skip case where host echo hasn't returned yet) is accepted.
      Receiver-side _OnSkipDouble is idempotent on already-removed
      seats. Triple/Four/Gahwa rungs remain one-shot — phase advances
      immediately and no per-rung state token exists for a meaningful
      post-apply guard.
    4. N.SendPreempt — retry added with TWO-BRANCH guard (codex correction)
      Two state orderings exist for preempt-claim:
      (a) Non-host LocalPreempt applies state to PHASE_DEAL2BID/
      contract=nil BEFORE send. If first frame drops, contract
      never becomes Sun-by-seat locally — contract-only guard
      would dead-code this case.
      (b) Host/bot post-ApplyContract: contract.bidder == seat,
      contract.type == SUN.
      Guard:
      (phase == PHASE_DEAL2BID and contract == nil) or
      (contract and contract.bidder == seat and contract.type == SUN)
      Receiver-side _OnPreempt is idempotent on already-removed seats.
    5. N.SendPreemptPass — retry added
      Guard: phase<mark>PHASE_PREEMPT AND not preemptContains(preemptEligible,
      seat). preemptEligible</mark>nil (final-pass _FinalizePreempt) is
      accepted. Receiver-side _OnPreemptPass is idempotent.
      Tests added in AZ section:
    • AZ.29e split into 3 pins (Triple/Four/Gahwa still one-shot)
    • AZ.33a-e: SendSkipDouble initial + 3 retry-suppress branches
      • empty-belPending final-skip non-host case
    • AZ.34a-e: SendPreempt initial + branch (a) + branch (b) +
      retry-suppress when neither branch holds
    • AZ.35a-d: SendPreemptPass initial + seat-removed + preemptEligible
      -nil + retry-suppress when phase moves past PREEMPT
    • AZ.36a-c: bot SWA migration source-pin + helper retry verification
    • AZ.37a-c: AFK preempt-pass migration source-pin verification
      Tests: 1106/1106 pass (was 1084; +22 new pins: +2 AZ.29e split,
      +20 AZ.33-37). Lua syntax: 26 files clean.
      Explicitly deferred (Codex review):
      • SendSkipTriple/Four/Gahwa retry
      • preempt window-open (seat=0, eligCsv) frame
      • N.SendSWA direct claim/fallback
      • SendSWAOut, SendTrick, SendRound, SendGameEnd
      • MSG_DEAL redeal banner
      • MSG_TAKWEESH_REVIEW, MSG_TAKWEESH_OUT
      • Any new runtime state fields or protocol tags
        Branch: v3.2.0-cleanup-batch4a
        Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 cleanup batch 4 design/inventory doc
    Pre-implementation design pass inventorying remaining one-shot/raw
    wire broadcasts around skip, preempt, and adjacent SWA/Takweesh
    outcome paths.
    Reviewed by Codex in CLAUDE_V3_2_0_BATCH4_DESIGN_REVIEW.md (not in
    repo): approved with corrections, applied to this commit:
    - Total raw/one-shot count corrected from 13 to 18 (matches table
    rows: 4 + 4 + 3 + 7)
    - Area D heading fixed from "5 sites" to "7 sites"
    - Area C split: SendSWA helper, raw bot MSG_SWA_REQ, raw C_Timer-
    unavailable MSG_SWA fallback now distinct rows
    - SendPreempt mutation-order row corrected: LocalPreempt applies
    BEFORE send but lands in PHASE_DEAL2BID/contract=nil on non-host
    clients; bot path sends BEFORE host apply. Two orderings require
    a two-branch post-send guard (Codex's critical correction)
    - Summary numbers updated; codex-questions section replaced with
    review-status outcome
    Implementation will follow corrected scope in
    CLAUDE_V3_2_0_CLEANUP_BATCH4A_PROMPT.md (not in repo): 5 items only
    — 2 raw-broadcast → helper migrations + 3 helper retry additions.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • v3.2.0 cleanup batch 3: convert 4 source pins to behavioral
    Per CLAUDE_V3_2_0_BATCH3_DESIGN_REVIEW.md scope. Test-only commit;
    no runtime addon code changes.
    AD.4a — BotMaster single-card-shortcut diagnostic
    Source pin retired from tests/test_state_bot.lua. Behavioral test
    added in tests/test_botmaster.lua section F.1 (F.1a-d): sets up
    a single-card hand, calls BM.PickPlay(2), asserts the returned
    card AND BM._lastShortCircuit == "single-card" AND
    BM._lastWorldsCompleted == 0 AND Bot._inRollout restored.
    AD.4b Slash message pin retained (out of scope per prompt).
    AC.6 — PickFour unconditional +5 partner-open-Bel calibration
    Source pin (literal strength = strength + 5 scan) replaced with
    behavioral. Uses the codex-provided fixture
    { "QD", "JC", "8S", "7H", "8H", "9C", "AD", "JD" } for seat 2 at
    PHASE_FOUR. math.random overridden to return 0 (zero jitter),
    then restored. With +5 bonus applied: strength=80, jth=80 →
    PickFour fires. Pre-fix removal of the +5 would drop strength
    below jth → test fails loudly. Deterministic.
    AI.6 — saveForPartnerTouch source pin retired
    Per codex review, the existing AK.4 behavioral (touching-honors
    save in pickFollow smother → donate QH instead of AH) already
    exhaustively exercises this code path end-to-end. The AI.6 source
    pin added no incremental coverage; deleted. AK.4 unchanged.
    AC.4 + AC.5 — combined PickOvercall Bel-fear boundary
    Per codex review: AC.4's source pin was stale/mislabelled (it
    scanned for the OVERCALL strict-gate code, not PickDouble).
    Both AC.4 and AC.5 protected the same overcall Bel-fear path —
    replaced with one behavioral boundary test using H.13's PickOvercall
    scaffolding (seat 3, hand AS/AH/AD/AC/TS/TH/TD/TC, bidcard 9C,
    dealer 4, M3lm enabled). Two-direction assertion:
    cumulative.A == 100 → Bot.PickOvercall(3) == "TAKE" (strict gate, not >=)
    cumulative.A == 101 → Bot.PickOvercall(3) == "WAIVE" (Bel-fear flips it)
    Proves both invariants AC.4 and AC.5 claimed.
    Deferred (out of scope per design review):
    - AH.4 BM-04-FALLBACK sampler internals (would require exporting
    internal helpers — risk too high for the gain)
    - Any other source-pin conversion not in the approved list
    Tests: 1084/1084 pass. Net pin change vs main: +4 botmaster
    behavioral (F.1a-d), -1 AD.4a source, ±0 AC.6 swap, -1 AI.6 source,
    ±0 AC.4+AC.5 swap = 1082 + 2 net.
    Branch: v3.2.0-cleanup-batch3
    Files changed: tests/test_botmaster.lua, tests/test_state_bot.lua
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 cleanup batch 3 design/inventory doc
    Pre-implementation design pass for source-pin → behavioral
    conversion. Inventories ~244 source-pin assertions, prioritizes 5
    low-risk candidates with existing nearby behavioral scaffolding.
    Reviewed and approved with corrections by Codex in
    CLAUDE_V3_2_0_BATCH3_DESIGN_REVIEW.md (not in repo):
    - AC.4 source pin is stale/mislabelled (matches overcall code,
    not PickDouble) → combine AC.4+AC.5 into one PickOvercall
    boundary test
    - AC.6 needs deterministic RNG override
    - AH.4 deferred (sampler internals not safely testable)
    - AD.4a behavioral preferably in tests/test_botmaster.lua
    Implementation will follow corrected scope in
    CLAUDE_V3_2_0_CLEANUP_BATCH3_PROMPT.md (not in repo).
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • v3.2.0 cleanup batch 2: extract retry broadcast helper
    Per CLAUDE_V3_2_0_CLEANUP_BATCH2_PROMPT.md scope and the Codex design
    review (CLAUDE_V3_2_0_BATCH2_DESIGN_REVIEW.md): mechanical
    abstraction only, no behavior changes.
    Helper added near broadcast (Net.lua:~58):
    local function broadcastWithRetry(frame, guardFn)
    broadcast(frame)
    if not (C_Timer and C_Timer.After) then return end
    C_Timer.After(0.25, function()
    if guardFn() then broadcast(frame) end
    end)
    end
    Module-local, not exported, not directly tested (per Codex's
    "test through migrated senders" guidance).
    Migrated 17 N.Send* sites:
    - SendBidCard, SendTurn, SendBid, SendContract
    - SendDouble, SendTriple, SendFour, SendGahwa (Pattern C — closure
    captures contractAtSend upvalue for post-apply identity guard)
    - SendBelote, SendMeld, SendAKA
    - SendSWAReq, SendSWAResp (the v3.1.14 LocalSWAResp(false) deny
    path stays intact — its 0.35s delayed-clear timer lives outside
    the helper)
    - SendTakweesh, SendKawesh
    - SendPlay, SendOvercallDecision
    Each migration:
    - Preserves the exact same wire frame string
    - Preserves the exact same guard semantics (closure captures match
    the previous inline gate)
    - Preserves per-site documentation comments, updated to reference
    broadcastWithRetry instead of inline C_Timer.After
    Per R4 decision in the design doc: N._HostResolveOvercall's inline
    retry (Net.lua:~2094) is intentionally LEFT UNTOUCHED. Removing it
    would change the overcall MSG_CONTRACT rebroadcast count from 4
    frames across ~0.5s to 2 frames. That's a behavior change; deferred
    to a separate audit batch with explicit behavioral test coverage.
    U.1 source pin therefore unchanged.
    Source-pin updates (4 pins):
    - AX.2: now looks for broadcastWithRetry( in SendPlay body
    - AX.3: now looks for PHASE_PLAY guard inside SendPlay (no longer
    requires literal C_Timer.After)
    - AX.5: now looks for broadcastWithRetry( in SendOvercallDecision
    - AX.6: now looks for PHASE_OVERCALL guard inside SendOvercallDecision
    New behavioral tests (AZ.30, AZ.31, AZ.32 + suppress variants):
    - AZ.30a-e: SendTurn initial + retry + 2 suppress cases (S.s.turn
    changed, turnKind changed)
    - AZ.31a-d: SendPlay initial + retry + suppress (phase moves past
    PLAY)
    - AZ.32a-d: SendOvercallDecision initial + retry + suppress (phase
    moves past OVERCALL)
    Tests: 1082/1082 pass (was 1065, +17 new behavioral pins). Lua
    syntax: 26 files clean.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • docs: add v3.2.0 cleanup batch 2 design/inventory doc
    Pre-implementation design pass for the retry-abstraction batch.
    Inventories all 18 C_Timer.After(0.25) sites in Net.lua, categorizes
    the guard patterns (A: phase-only, B: phase+state-identity, C:
    contract-table-identity+post-apply, D: contract-by-value, E:
    redundant recursive retry), proposes the broadcastWithRetry helper
    API, and flags migration risks.
    Reviewed and approved by Codex with corrections in
    CLAUDE_V3_2_0_BATCH2_DESIGN_REVIEW.md (not in repo):
    - R4: leave _HostResolveOvercall alone in this batch
    - 4 source pins need updates (AX.2/3/5/6), not 2
    - helper stays module-local, no direct tests
    - 3 new behavioral tests needed (SendTurn/Play/OvercallDecision)
    - drop optional delay param (every site uses 0.25)
    Implementation will follow the corrected scope.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • v3.2.0 cleanup batch 1: remove dead helpers and extract skip senders
    Per CLAUDE_V3_2_0_CLEANUP_BATCH1_PROMPT.md scope: mechanical cleanup
    only, no gameplay behavior changes, no retry logic, no phase guards.
    Task 1 — Bot.lua dead-code removal:
    • Bot.lua:2397 highestNonTrump — confirmed unused (grep hit only its
      definition). Removed.
    • Bot.lua:7742 escalateDecision — confirmed unused (PickDouble/Triple
      /Four/Gahwa each have their own inline yes-wantOpen computation
      that doesn't call this helper). Removed.
      Stale doc references in docs/strategy/glossary.md and
      .swarm_findings/ point to old line numbers from prior versions; not
      runtime callers. Glossary cleanup is a separate concern.
      Task 2 — Net.lua skip-sender extraction:
    • Added N.SendSkipDouble/Triple/Four/Gahwa one-shot broadcast
      helpers near other N.Send* functions.
    • Migrated 16 raw-broadcast call sites to the new helpers:
      • N._OnDouble Race-A recovery (1 site)
      • N.LocalSkipDouble/Triple/Four/Gahwa (4 sites)
      • N._HostBelTimeout AFK paths (4 sites: double/triple/four/gahwa)
      • N.MaybeRunBot bot Bel-decision botSkips loop (1 site)
      • N.MaybeRunBot bot Triple-decision skip + error-recovery (2 sites)
      • N.MaybeRunBot bot Four-decision skip + error-recovery (2 sites)
      • N.MaybeRunBot bot Gahwa-decision skip + error-recovery (2 sites)
        All migrations preserve existing state mutations around the call
        sites. Helpers are intentionally one-shot — no retry, no phase
        guards — matching prior single-emit semantics exactly. Retry
        coverage for skip messages is deferred to a later batch per the
        v3.2.0 cleanup plan.
        Untouched per scope: MSG_PREEMPT_PASS, MSG_PREEMPT, MSG_SWA_REQ,
        MSG_SWA, MSG_TAKWEESH_*, MSG_DEAL;redeal.
        Task 3 — Tests (AZ.29a-e):
    • AZ.29a: SendSkipDouble emits one MSG_SKIP_DBL frame
    • AZ.29b: SendSkipTriple emits one MSG_SKIP_TRP frame
    • AZ.29c: SendSkipFour emits one MSG_SKIP_FOR frame
    • AZ.29d: SendSkipGahwa emits one MSG_SKIP_GHW frame
    • AZ.29e: SendSkipDouble queues no C_Timer retry (one-shot by design)
      Tests: 1065/1065 pass (was 1060, +5 helper-extraction tests).
      Branch: v3.2.0-cleanup-batch1.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com