houserules/.sisyphus/plans/thressgame-coverage.md
Joey Yakimowich-Payne 21838af5c1
feat(thressgame-coverage): Wave 10 (8 parity descriptors + 6 templates + perf + e2e spec)
8 parity tests (recreate ThressGame rules via descriptor JSON):
- T59 minefield: spawn mines + on-piece-entered destroys; one-shot consumption
- T60 mr_freeze: request-choice column + for-row + frozen-square spawn (lifetime moves:9)
- T61 parry: on-captured + RPS request-choice + conditional + cancel-capture
- T62 all_on_red: on-turn-start + with-probability(0.1) + BlockAllExceptKing seed (lifetime turns:5)
- T63 religious_conversion: on-move(bishop) + for-each-adjacent + set-piece-attr Color
- T64 ice_physics: on-rule-activated + for-each-piece(slider filter) + SlideMustBeMaxDistance
- T65 kamikaze: on-capture + with-probability(0.25) + for-each-adjacent + destroy-piece (king excluded)
- T66 mind_control: on-rule-activated + request-choice(forPlayer:both, LIFO stack) + for-each-piece + Color set

T67: 6 template descriptors in custom/recipes.ts (simple-mine, vampire-on-capture, frozen-column, coin-flip-restriction, religious-bishop, no-mans-land)
T68: Playwright e2e spec for 3 request-choice flows (.skip()'d pending UI integration; documents the gap)
T69: 100-marker performance budget test (p99 < 50ms via deterministic engine + perf.now timing)

Tests: 2703 -> 2740 (+37). bun run check exit 0.
2026-04-26 13:50:28 -06:00

111 KiB
Raw Permalink Blame History

ThressGame Rule-Coverage DSL Extension

TL;DR

Quick Summary: Extend the chess rules DSL with ~28 new primitives, 4 new triggers, marker entities, seeded RNG, and a stack-based player-choice state machine. Targets 95%+ coverage of ThressGame's 66-rule catalog (~62 rules expressible) with full UI polish.

Deliverables:

  • 28 new primitives (imperative board mutation, markers, iteration, RNG, restrictions, movement replacement, player choice)
  • 4 new triggers (on-rule-activated, on-rule-expire, on-piece-entered-marker, on-marker-expire)
  • Marker entity subsystem with 8 marker kinds + lifetime + collision priority
  • Seeded PRNG (Mulberry32) on GAME_ENTITY
  • WS protocol v2 with request-choice / submit-choice messages
  • Suspended-execution state machine (stack-based) for nested player choices
  • Editor: palette categories, narrate.ts entries, 4 ParamField custom renderers (square, piece, marker-kind, lifetime)
  • 8 ThressGame parity test descriptors with Playwright e2e
  • 6 template descriptors shipped in modifier library

Estimated Effort: XL Parallel Execution: YES — 11 waves Critical Path: L0 ADRs → L1 entity attrs → L2 param walker → L4 dispatcher → L7 protocol → L8 choice integration → L10 e2e


Context

Original Request

User asked: "From this file, how many rules can we implement using our system?" referencing ThressGame's mutators/ruleHooks.js. Initial analysis: ~5/66 (~10%) expressible. User then requested a plan to close the gap with full coverage push (95%+).

Interview Summary

Locked Decisions:

  • Scope: 95%+ coverage (~62 of 66 rules); pacman_style (board topology) + ~3 nuance cases explicitly deferred
  • Activation model: NEW trigger on-rule-activated fires once when modifier attaches; imperative primitives only legal inside trigger arrays
  • Square state: marker entities (each marker is a session entity with MarkerKind, Position, MarkerLifetime, optional MarkerOwner, MarkerLinks); markers participate in aura compute
  • UI: full polish — palette categories, narrate, 4 ParamField renderers, Playwright e2e for 3 rules
  • Player choice: NEW request-choice primitive with WS protocol v2 + STACK-based pending-choices state machine (nested allowed)
  • Trigger reentrance: deferred queue (imperative primitives enqueue trigger events; dispatcher drains after current arm)
  • Move-gen dry mode: move generator runs with suppressTriggers: true flag; triggers fire only on actual commit
  • RNG: seeded PRNG (Mulberry32) on GAME_ENTITY (RngSeed, RngStream attrs); primitives with-probability, random-pick
  • Marker collision: stackable, resolved by hardcoded marker-kind priority (portal-end < frozen-square < mine < pit < death-square < tornado < treasure < blocked)
  • Choice timeout: per-game configurable in CreateGameRequest — timeout-with-default (numeric seconds; default first option on expiry) OR no-timeout (pause on disconnect)

Research Findings:

  • 12-stage onAfterMove dispatch in apply.ts:996-1090 is the canonical extension point
  • Entity model is minimal: GAME_ENTITY (0), PRESET_STATE_ENTITY (-1), pieces id > 0; new entities via session.nextId()
  • engine.spawnPiece() (engine.ts:741-769) is the canonical entity factory; markers follow this pattern
  • No existing async / wait-for-input pattern; PlayerAction discriminated union is precedent for new actions
  • Engine fully deterministic; no Math.random in hot path; safe to add seeded PRNG
  • Aura model is fact-based (auras.ts) — must extend to include markers per V1 decision

Metis Review

Identified Gaps (addressed):

  • Nested suspension complexity → addressed via stack-based state, validator enforces depth ≤ 8
  • Trigger reentrance → addressed via deferred queue, cascade depth limit
  • Move-gen cycle risk → addressed via dry-mode flag
  • Marker/aura paradigm bridge → addressed (markers participate in auras with discriminator-based filter)
  • Position-attr semantics drift → addressed via L1 audit task
  • "95% coverage" claim unverified → addressed via paper-exercise (5 hardest rules verified expressible)
  • WS protocol versioning omitted → addressed via L7 versioning task
  • Test infrastructure for choices underestimated → addressed as dedicated L8 task (deterministic auto-resolver)
  • 8 ThressGame parity rules locked by name (see Test Strategy below)
  • 6 template descriptors locked by name (see Deliverables below)

Work Objectives

Core Objective

Add a complete primitive layer to the chess rules DSL that supports imperative board mutation, marker-based square state, seeded probabilistic effects, and stack-based player choice — covering 62 of 66 ThressGame rules with full editor and runtime support.

Concrete Deliverables

  • Primitives: 28 new in packages/chess/src/modifiers/primitives/ (each with .ts + .test.ts mirroring on-capture.ts pattern)
  • Triggers: 4 new (on-rule-activated, on-rule-expire, on-piece-entered-marker, on-marker-expire)
  • Entity attrs: MarkerKind, MarkerLifetime, MarkerOwner, MarkerLinks, RngSeed, RngStream, EntityKind, plus 5 movement-replacement flags
  • Engine: marker spawn factory (engine.spawnMarker()), aura compute extension, move-gen suppressTriggers flag, deferred trigger queue, cascade depth guard, seeded PRNG init in integration preset
  • Validator: imperative-in-passive rejection, binding-scope check, request-choice depth ≤ 8 check, lifetime-shape check
  • Param walker: resolution layer for { $var: "name" }, { ctx-attr: { entity, attr } }, { ctx-build: { col, row } }
  • WS protocol v2: request-choice (server→client), submit-choice (client→server), versioned with backward-compat error path
  • Suspended execution: stack-based pendingChoices on GAME_ENTITY, resume-from-saved-state in integration preset's onAfterAction
  • Game settings: choiceTimeout field in CreateGameRequest schema
  • Editor: palette taxonomy update (5 categories), narrate.ts entries for all new primitives + triggers, 4 custom ParamField renderers (square picker, piece picker, marker-kind enum, lifetime config)
  • Marker rendering: client board overlay component for markers (icons + tooltips)
  • Test descriptors: 8 ThressGame rules recreated as .json fixtures with Playwright e2e parity tests (LOCKED LIST: minefield, mr_freeze, parry, all_on_red, religious_conversion, ice_physics, kamikaze, mind_control)
  • Template descriptors: 6 templates shipped in modifier library (LOCKED LIST: simple-mine, vampire-on-capture, frozen-column, coin-flip-restriction, religious-bishop, no-mans-land)

Definition of Done

  • bun run check exits 0 with all 1957+ existing tests + ~250 new tests passing
  • bun x playwright test e2e/thressgame-parity.spec.ts exits 0 (8 rules)
  • bun x playwright test e2e/request-choice.spec.ts exits 0 (3 round-trip flows)
  • Determinism property test passes (N=100 iterations, byte-identical state hash)
  • Backward-compat: all 22 existing primitives' tests pass byte-identical
  • Validator rejects imperative-in-passive with error code descriptor.primitives.imperative-in-passive
  • Cascade depth guard: synthetic loop test asserts depth=8 limit fires error code descriptor.primitives.cascade-depth-exceeded
  • Wire-parity test extended to all 28 new primitive kinds (round-trips through server schema)
  • Performance test: 100 markers on board, move latency < 50ms (p99)

Must Have

  • All 28 new primitives with TDD coverage (RED test → GREEN impl → narrate + palette in same commit)
  • All 4 new triggers wired into 12-stage dispatcher (becomes 14-stage)
  • Marker entity discriminator (EntityKind: "piece" | "marker") audited across Position-attr callers
  • Seeded PRNG initialized at game start; all probabilistic primitives draw via the seeded stream
  • WS protocol versioned (v1 → v2); old client + new server defined behavior tested
  • Stack-based suspended execution with serializable state (no closures)
  • Choice timeout configurable per game (60s default; 0 = no-timeout); disconnect = forfeit OR pause based on mode

Must NOT Have (Guardrails)

  • NO modifications to existing 22 primitives' behavior (additive-only)
  • NO Math.random / Date.now / unsorted Map iteration anywhere in engine hot path
  • NO new primitives outside the locked list of 28 — adding a 29th requires a new plan
  • NO async/await in engine; suspended execution = state-machine snapshot, not Promise
  • NO as any / @ts-ignore introduced
  • NO breaking changes to CustomModifierDescriptor.version (stays at 1 — schema is structurally compatible)
  • NO breaking changes to existing WS message types in v1 (v2 is additive)
  • NO modifications to MAX_RECURSION_DEPTH (3) or MAX_PRIMITIVE_COUNT (50)
  • NO drag-from-palette DnD (separate concern; defer)
  • NO performance optimization passes during this plan (correctness first; profile only on regression)
  • NO refactoring of existing param walker — binding resolution is a layer ON TOP
  • NO multi-game / multi-tenant concerns
  • NO replay viewer changes
  • NO marker animations / particle effects (simple icons only)

Verification Strategy (MANDATORY)

ZERO HUMAN INTERVENTION — ALL verification is agent-executed. No exceptions. Acceptance criteria requiring "user manually tests/confirms" are FORBIDDEN.

Test Decision

  • Infrastructure exists: YES (Vitest + Playwright + react-dom/server SSR)
  • Automated tests: TDD per primitive — every primitive ships with failing test → impl → narrate + palette in single commit
  • Framework: bun test (Vitest); Playwright for e2e
  • Convention: co-located .test.ts(x) files; SSR via react-dom/server for component tests

QA Policy

Every task MUST include agent-executed QA scenarios. Evidence saved to .sisyphus/evidence/task-{N}-{scenario-slug}.{ext}.

  • Engine/Library code: bun test <file> and parse exit code + assertion count
  • API/Protocol: bun test packages/server/... for wire schema; ws.test.ts for round-trips
  • Editor/UI: SSR snapshot tests + Playwright for interactive
  • Determinism: state-hash comparison via SHA256 of session fact dump

Execution Strategy

Parallel Execution Waves

Maximize throughput by grouping independent tasks into parallel waves. Each wave completes before the next begins.

Wave 0 — Architecture Decisions (sequential, 1 task → blocks everything)
└── Task 0: ADR document + paper-exercise capture [deep]

Wave 1 — Foundation infrastructure (start immediately, parallel):
├── Task 1: Backward-compat baseline fixture [quick]
├── Task 2: Determinism property-test harness [unspecified-high]
├── Task 3: State-hash util (SHA256 of facts) [quick]
├── Task 4: Position-attr caller audit [unspecified-high]
└── Task 5: Param walker `$var` conflict audit [quick]

Wave 2 — Core data structures (after Wave 1, parallel):
├── Task 6: New entity attrs (EntityKind, MarkerKind, MarkerLifetime, MarkerOwner, MarkerLinks, RngSeed, RngStream) [unspecified-high]
├── Task 7: Aura compute filter (EntityKind discriminator; markers AuraSpec applicable) [deep]
├── Task 8: Movement-replacement attrs (MovesAs, MovesAlsoAs, SlideMustBeMaxDistance, BlockAllExceptKing, KingExtraReach) [unspecified-high]
├── Task 9: Mulberry32 PRNG utility + integration-preset seed init [unspecified-high]
└── Task 10: Marker entity factory (engine.spawnMarker) [unspecified-high]

Wave 3 — Param walker + binding resolver (after Wave 2):
├── Task 11: Binding scope stack on PrimitiveApplyContext [deep]
├── Task 12: Param walker resolves {$var}, {ctx-attr}, {ctx-build} shapes [deep]
├── Task 13: Validator: binding-ref-out-of-scope error [unspecified-high]
└── Task 14: Validator: imperative-in-passive-slot error + chooser-entity on activation context [unspecified-high]

Wave 4 — Dispatcher extension (after Wave 3):
├── Task 15: Deferred trigger queue + cascade depth guard [deep]
├── Task 16: on-rule-activated trigger dispatch (integration preset onActivate) [deep]
├── Task 17: on-rule-expire trigger + per-modifier countdown [deep]
├── Task 18: on-piece-entered-marker trigger + marker priority resolver [deep]
├── Task 19: on-marker-expire trigger + lifetime decrementer in onAfterMove [deep]
└── Task 20: Move-gen suppressTriggers flag wired through registry [deep]

Wave 5 — Imperative primitives (after Wave 4, MAX PARALLEL):
├── Task 21: place-piece [unspecified-high]
├── Task 22: destroy-piece [unspecified-high]
├── Task 23: move-piece [unspecified-high]
├── Task 24: swap-pieces [unspecified-high]
├── Task 25: convert-piece-type [unspecified-high]
├── Task 26: set-piece-attr (generic with target binding) [unspecified-high]
└── Task 27: cancel-capture (interrupts on-captured/on-capture event) [deep]

Wave 6 — Marker primitives + iteration (parallel with Wave 5 conceptually but tested after):
├── Task 28: spawn-marker [unspecified-high]
├── Task 29: spawn-marker-pair (portal pairs) [unspecified-high]
├── Task 30: destroy-marker [unspecified-high]
├── Task 31: for-each-piece (filter + binding) [deep]
├── Task 32: for-each-square (filter + binding) [deep]
├── Task 33: for-each-adjacent (target + filter + binding) [deep]
├── Task 34: for-each-marker (filter + binding) [unspecified-high]
└── Task 35: for-column / for-row [unspecified-high]

Wave 7 — RNG + restriction + movement-replacement primitives (parallel with 5/6):
├── Task 36: with-probability [unspecified-high]
├── Task 37: random-pick (with binding) [deep]
├── Task 38: must-class (capture-if-possible / advance-if-possible / move-to-square) [deep]
├── Task 39: block-by-piece-type [unspecified-high]
├── Task 40: set-moves-as / set-moves-also-as primitives [deep]
├── Task 41: pawn-pushes-pieces flag primitive [unspecified-high]
└── Task 42: lifetime field on imperative primitives (uniform shape) [deep]

Wave 8 — WS protocol v2 + suspended execution (after Waves 4 + 5 + 7):
├── Task 43: WS protocol v2 schema (request-choice + submit-choice + version negotiation) [deep]
├── Task 44: Server-side request-choice broadcast + validation [deep]
├── Task 45: Stack-based pendingChoices state on GAME_ENTITY + serializer [deep]
├── Task 46: Suspended-execution resume in integration preset's performAction [deep]
├── Task 47: request-choice primitive (kind: piece/square/column/row/coin-flip/rps; with prompt-both-players) [deep]
├── Task 48: Deterministic auto-resolver test transport (test-only WS handler) [unspecified-high]
├── Task 49: Choice timeout + disconnect handler (per-game setting) [deep]
└── Task 50: Game settings: choiceTimeout in CreateGameRequest [unspecified-high]

Wave 9 — Editor / UI (parallel with Wave 5/6/7):
├── Task 51: Palette taxonomy update (5 categories: State, Mechanic, Trigger, Imperative, Iteration) [visual-engineering]
├── Task 52: narrate.ts entries (all 28 primitives + 4 triggers) [writing]
├── Task 53: ParamField renderer: square picker [visual-engineering]
├── Task 54: ParamField renderer: piece picker [visual-engineering]
├── Task 55: ParamField renderer: marker-kind enum [visual-engineering]
├── Task 56: ParamField renderer: lifetime config [visual-engineering]
├── Task 57: Client marker rendering on chessboard [visual-engineering]
└── Task 58: Client request-choice modal (square/piece/column/row/coin-flip/rps variants) [visual-engineering]

Wave 10 — Test descriptors + Playwright e2e (after Waves 5-9):
├── Task 59: minefield descriptor + parity test [unspecified-high]
├── Task 60: mr_freeze descriptor + parity test (uses request-choice) [unspecified-high]
├── Task 61: parry descriptor + parity test (uses request-choice + cancel-capture + RPS) [unspecified-high]
├── Task 62: all_on_red descriptor + parity test (uses with-probability) [unspecified-high]
├── Task 63: religious_conversion descriptor + parity test [unspecified-high]
├── Task 64: ice_physics descriptor + parity test [unspecified-high]
├── Task 65: kamikaze descriptor + parity test (uses with-probability + for-each-adjacent) [unspecified-high]
├── Task 66: mind_control descriptor + parity test (uses request-choice on both players) [unspecified-high]
├── Task 67: 6 template descriptors shipped in modifier library [writing]
├── Task 68: Playwright: request-choice round-trip e2e (3 flows) [unspecified-high]
└── Task 69: Performance budget test (100 markers, p99 < 50ms) [unspecified-high]

Wave FINAL — 4 parallel reviews (after ALL implementation):
├── Task F1: Plan compliance audit (oracle)
├── Task F2: Code quality review (unspecified-high)
├── Task F3: Real manual QA across all 8 parity tests + 3 e2e flows (unspecified-high)
└── Task F4: Scope fidelity check (deep)
-> Present results -> Get explicit user okay

Critical Path: 0 → {1,2,3,4,5} → {6,9,10} → {11,12} → 15 → {16-20} → {21-27} → 43 → 47 → 59 → F1-F4
Parallel Speedup: ~75% faster than sequential
Max Concurrent: 8 (Waves 5+6+7+9 overlap)

Dependency Matrix (abbreviated)

  • 0: — / blocks: 1-69
  • 1-5: 0 / blocks: 6-69
  • 6-10: 1-5 / blocks: 11-69
  • 11-12: 6 / blocks: 13-69
  • 13-14: 11-12 / blocks: 21-69
  • 15-20: 13-14 / blocks: 21-69
  • 21-27: 15, 16 / blocks: 59-69
  • 28-35: 18, 19, 11-12 / blocks: 59-69
  • 36-42: 13-14, 11-12, 9 / blocks: 59-69
  • 43-50: 47 needs 11-14, 15, 21-27 / blocks: 60, 61, 66, 68
  • 51-58: 6, 13-14 / blocks: 67, 68
  • 59-66: 21-58 (varies per descriptor)
  • 67: 51-58
  • 68: 47, 49, 58
  • 69: 18, 19, 28
  • F1-F4: ALL implementation tasks

Agent Dispatch Summary

  • 0: 1 — T0 → deep
  • 1: 5 — T1 → quick, T2 → unspecified-high, T3 → quick, T4 → unspecified-high, T5 → quick
  • 2: 5 — T6,T8 → unspecified-high, T7,T9 → unspecified-high/deep, T10 → unspecified-high
  • 3: 4 — T11,T12 → deep, T13,T14 → unspecified-high
  • 4: 6 — All deep
  • 5: 7 — Mostly unspecified-high, T27 → deep
  • 6: 8 — Mix
  • 7: 7 — Mix
  • 8: 8 — Mostly deep
  • 9: 8 — visual-engineering/writing
  • 10: 11 — unspecified-high/writing
  • FINAL: 4 — F1 → oracle, F2-F3 → unspecified-high, F4 → deep

TODOs

Implementation + Test = ONE Task. Never separate. EVERY task MUST have: Recommended Agent Profile + Parallelization info + QA Scenarios.

  • 0. ADR document + paper-exercise capture

    What to do:

    • Create .sisyphus/notepads/thressgame-coverage/decisions.md with ALL locked architectural decisions (see plan Context section): suspended-execution stack semantics, deferred-trigger-queue semantics, move-gen suppressTriggers flag, marker-aura bridge, marker collision priority table, choice-timeout product spec
    • Capture the 5-rule paper exercise (parry, all_on_red, religious_conversion, ice_physics, mr_freeze) as .sisyphus/notepads/thressgame-coverage/paper-exercise.md — pseudocode each rule using locked primitive set
    • Create .sisyphus/notepads/thressgame-coverage/learnings.md (empty, ready for executor notes)
    • Lock the 8 parity test descriptor names (already in plan; mirror to decisions.md)
    • Lock the 6 template descriptor names (already in plan; mirror to decisions.md)
    • Lock the 5 palette categories (State, Mechanic, Trigger, Imperative, Iteration)
    • Lock the 4 ParamField renderers (square, piece, marker-kind, lifetime)
    • Lock cascade depth limit = 8 (matches existing RUNTIME_DEPTH_HARD_CAP)
    • Lock marker priority table: portal-end=1, frozen-square=2, mine=3, pit=4, death-square=5, tornado=6, treasure=7, blocked=8

    Must NOT do:

    • Modify any code in this task — pure documentation
    • Lock any decision not already in plan; this is capture-only

    Recommended Agent Profile:

    • Category: deep
      • Reason: high-stakes architectural capture; decisions here propagate to ~50 downstream tasks
    • Skills: []

    Parallelization:

    • Can Run In Parallel: NO
    • Parallel Group: Wave 0 alone
    • Blocks: ALL other tasks
    • Blocked By: None

    References:

    • Pattern: .sisyphus/notepads/visual-modifier-builder/decisions.md — same notepad structure
    • Plan: .sisyphus/plans/thressgame-coverage.md Context section — source of locked decisions

    Acceptance Criteria:

    • File exists: .sisyphus/notepads/thressgame-coverage/decisions.md
    • File exists: .sisyphus/notepads/thressgame-coverage/paper-exercise.md
    • File exists: .sisyphus/notepads/thressgame-coverage/learnings.md (empty placeholder ok)
    • wc -l decisions.md ≥ 100 lines (covers all 9 decision areas)
    • paper-exercise.md contains all 5 rule names + pseudocode

    QA Scenarios:

    Scenario: All decision areas captured
      Tool: Bash (grep)
      Steps:
        1. Run: grep -c '^##' .sisyphus/notepads/thressgame-coverage/decisions.md
        2. Assert count ≥ 9 (one heading per decision area)
      Expected Result: count ≥ 9
      Evidence: .sisyphus/evidence/task-0-decisions-headings.txt
    
    Scenario: Paper exercise covers all 5 rules
      Tool: Bash (grep)
      Steps:
        1. Run: grep -E '^### Rule [1-5]' .sisyphus/notepads/thressgame-coverage/paper-exercise.md | wc -l
        2. Assert count == 5
      Expected Result: 5
      Evidence: .sisyphus/evidence/task-0-paper-exercise-count.txt
    

    Commit: YES

    • Message: docs(thressgame): capture architectural decisions and paper-exercise
    • Files: .sisyphus/notepads/thressgame-coverage/*.md
    • Pre-commit: none (pure docs)
  • 1. Backward-compat baseline fixture

    What to do:

    • Run bun run check and capture green output to packages/chess/src/__fixtures__/baseline-test-count.json ({ files: 166, tests: 1957, timestamp })
    • Snapshot all 22 existing primitive .test.ts outputs to packages/chess/src/__fixtures__/baseline-primitive-tests.json (per-primitive: { kind, testCount, expectCount })
    • Add a meta-test packages/chess/src/__fixtures__/baseline-regression.test.ts that asserts current bun test output meets-or-exceeds baseline counts
    • This becomes our regression guard: any task that breaks an existing test is caught immediately

    Must NOT do:

    • Modify any existing primitive's test
    • Lower the baseline numbers if tests are removed (only add)

    Recommended Agent Profile:

    • Category: quick
      • Reason: scripted snapshot, no logic
    • Skills: []

    Parallelization:

    • Can Run In Parallel: YES (Wave 1)
    • Parallel Group: Wave 1 (with 2, 3, 4, 5)
    • Blocks: All implementation waves (Waves 5+)
    • Blocked By: 0

    References:

    • Existing: packages/chess/src/modifiers/primitives/ — directory of 22 existing primitives

    Acceptance Criteria:

    • packages/chess/src/__fixtures__/baseline-test-count.json exists with tests: ≥ 1957
    • bun test packages/chess/src/__fixtures__/baseline-regression.test.ts exits 0

    QA Scenarios:

    Scenario: Baseline fixture loads + asserts current state
      Tool: Bash
      Steps:
        1. Run: bun test packages/chess/src/__fixtures__/baseline-regression.test.ts 2>&1 | tee evidence.txt
        2. Assert exit code 0
        3. Assert "1 pass" in output
      Expected Result: exit 0; baseline matches
      Evidence: .sisyphus/evidence/task-1-baseline-test.txt
    

    Commit: YES

    • Message: test(chess): backward-compat baseline fixture
    • Files: packages/chess/src/__fixtures__/baseline-*.json, packages/chess/src/__fixtures__/baseline-regression.test.ts
  • 2. Determinism property-test harness

    What to do:

    • Create packages/chess/src/__fixtures__/determinism/ directory
    • Add packages/chess/src/__fixtures__/determinism/harness.ts exporting runDeterminismCheck(seed: number, moves: string[], iterations: number = 100): { hash: string; matches: boolean } — runs N iterations of the same engine setup, returns hash of final state, asserts all iterations match
    • Add packages/chess/src/__fixtures__/determinism/harness.test.ts with 1 sanity test (no rules, just standard chess) verifying baseline determinism

    Must NOT do:

    • Use Math.random anywhere in the harness
    • Skip iterations on time pressure

    Recommended Agent Profile:

    • Category: unspecified-high
      • Reason: critical infrastructure; need careful design
    • Skills: []

    Parallelization: Wave 1 (parallel with 1, 3, 4, 5). Blocks: Wave 8 (RNG primitives need this for verification).

    References:

    • Engine API: packages/chess/src/engine.ts — ChessEngine constructor + applyMove
    • Hash util: will be added in T3

    Acceptance Criteria:

    • bun test packages/chess/src/__fixtures__/determinism/harness.test.ts passes (1 test)
    • Harness runs N=100 iterations in < 5 seconds

    QA Scenarios:

    Scenario: Standard chess is deterministic at N=100
      Tool: Bash
      Steps:
        1. Run: bun test packages/chess/src/__fixtures__/determinism/harness.test.ts
        2. Assert exit 0
        3. Assert duration < 5000ms
      Expected Result: 1 pass, < 5s
      Evidence: .sisyphus/evidence/task-2-determinism-harness.txt
    

    Commit: YES

    • Message: test(chess): determinism property-test harness
    • Files: packages/chess/src/__fixtures__/determinism/{harness.ts,harness.test.ts}
  • 3. State-hash util (SHA256 of facts)

    What to do:

    • Create packages/chess/src/util/state-hash.ts with hashEngineState(engine: ChessEngine): string (returns SHA256 hex)
    • Algorithm: walk all entities, sort by id, for each entity sort facts by attr name, JSON.stringify, hash the concatenation
    • Co-located test state-hash.test.ts: identical engine state → identical hash; one fact difference → different hash; entity-id-order independence (insert in different orders, same hash)

    Must NOT do:

    • Use Date.now or any non-deterministic input
    • Hash arrays without sorting

    Recommended Agent Profile:

    • Category: quick
      • Reason: pure utility; well-known pattern
    • Skills: []

    Parallelization: Wave 1.

    References:

    • Node crypto: import { createHash } from 'node:crypto'
    • Engine: packages/chess/src/engine.ts — engine.session exposes facts

    Acceptance Criteria:

    • bun test packages/chess/src/util/state-hash.test.ts passes
    • Hash is 64-char hex string
    • Same state → same hash (3 different orderings)
    • One-fact diff → different hash

    QA Scenarios:

    Scenario: Determinism + insertion-order independence
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/util/state-hash.test.ts
      Expected Result: 4+ tests pass
      Evidence: .sisyphus/evidence/task-3-state-hash.txt
    

    Commit: YES

    • Message: feat(chess): state-hash util for determinism testing
    • Files: packages/chess/src/util/state-hash{.ts,.test.ts}
  • 4. Position-attr caller audit

    What to do:

    • Grep entire packages/chess/src/ for "Position" (the attr name as string)
    • For each callsite, classify: (a) reads Position assuming entity is a piece (RISK — markers will trigger false positives), (b) writes Position (just sets coords), (c) iterates entities-with-Position
    • Produce .sisyphus/notepads/thressgame-coverage/position-audit.md listing each callsite with file:line + classification + required fix (if any)
    • Common pattern: callers should add EntityKind === "piece" filter

    Must NOT do:

    • Modify any code yet — audit only
    • Skip non-obvious callsites

    Recommended Agent Profile:

    • Category: unspecified-high
      • Reason: critical safety audit; must be thorough
    • Skills: []

    Parallelization: Wave 1.

    References:

    • Files most likely affected: packages/chess/src/engine.ts, packages/chess/src/modifiers/auras.ts, packages/chess/src/modifiers/triggers.ts, packages/chess/src/move-generator.ts

    Acceptance Criteria:

    • position-audit.md lists ≥ 15 callsites (rough estimate; exact count TBD by grep)
    • Each callsite has classification (a/b/c) + fix recommendation

    QA Scenarios:

    Scenario: Audit completeness
      Tool: Bash (grep)
      Steps:
        1. grep -rn '"Position"' packages/chess/src/ | wc -l
        2. wc -l .sisyphus/notepads/thressgame-coverage/position-audit.md
      Expected Result: line count ≥ grep count (every grep hit covered)
      Evidence: .sisyphus/evidence/task-4-position-audit.txt
    

    Commit: YES

    • Message: docs(thressgame): position-attr caller audit
    • Files: .sisyphus/notepads/thressgame-coverage/position-audit.md
  • 5. Param walker $var conflict audit

    What to do:

    • Grep all primitive paramsSchema definitions for any field whose value or key uses $ prefix
    • Grep all existing template/library descriptors for $ in params
    • Confirm zero collisions; document in .sisyphus/notepads/thressgame-coverage/var-conflict-audit.md
    • If ANY collision found: STOP and surface to user — binding shape { $var: "name" } must be renamed (e.g., { __var: "name" })

    Must NOT do:

    • Modify code; audit only

    Recommended Agent Profile:

    • Category: quick
      • Reason: simple grep + write
    • Skills: []

    Parallelization: Wave 1.

    References:

    • Primitives: packages/chess/src/modifiers/primitives/*.ts
    • Templates: packages/chess/src/modifiers/library.ts or similar

    Acceptance Criteria:

    • var-conflict-audit.md exists with grep results + verdict (clean / conflict)
    • If clean: confirmed { $var: "..." } shape is safe to add

    QA Scenarios:

    Scenario: No existing $-prefixed params
      Tool: Bash (grep)
      Steps:
        1. grep -rn '"\\$' packages/chess/src/modifiers/primitives/ packages/chess/src/modifiers/library.ts 2>/dev/null
      Expected Result: zero matches OR all matches documented as non-conflicts
      Evidence: .sisyphus/evidence/task-5-var-conflict.txt
    

    Commit: YES

    • Message: docs(thressgame): param walker var-conflict audit
    • Files: .sisyphus/notepads/thressgame-coverage/var-conflict-audit.md
  • 6. New entity attrs (EntityKind, MarkerKind, MarkerLifetime, MarkerOwner, MarkerLinks, RngSeed, RngStream)

    What to do:

    • Edit packages/chess/src/schema.ts: extend ChessAttrMap with EntityKind: "piece" | "marker", MarkerKind: "mine" | "pit" | "portal-end" | "frozen-square" | "treasure" | "death-square" | "tornado" | "blocked", MarkerLifetime: { kind: "permanent" } | { kind: "moves"; expiresAtMove: number } | { kind: "one-shot" }, MarkerOwner: "white" | "black" | undefined, MarkerLinks: readonly EntityId[], RngSeed: number, RngStream: number
    • Edit packages/chess/src/modifiers/custom/apply.ts: register all 7 new attrs via registerAttrConsumer() calls (mirror existing pattern)
    • Add 7 unit tests to packages/chess/src/schema.test.ts asserting each new attr is present and typed correctly

    Must NOT do:

    • Change shape of any existing attr
    • Skip registerAttrConsumer (engine boot will throw via assertSeedConsumerIntegrity)

    Recommended Agent Profile:

    • Category: unspecified-high
      • Reason: cross-cutting type definitions; affects every later task
    • Skills: []

    Parallelization: Wave 2 (parallel with 7, 8, 9, 10).

    References:

    • packages/chess/src/schema.ts — ChessAttrMap definition
    • packages/chess/src/modifiers/custom/apply.ts:87-105 — registerAttrConsumer pattern
    • Notepad: .sisyphus/notepads/visual-modifier-builder/learnings.md:74-103 — T2/T3 pattern for adding attrs

    Acceptance Criteria:

    • bun test packages/chess/src/schema.test.ts passes (7 new tests)
    • grep -c '"MarkerKind"' packages/chess/src/schema.ts ≥ 1
    • grep -c "registerAttrConsumer.*EntityKind" packages/chess/src/modifiers/custom/apply.ts ≥ 1

    QA Scenarios:

    Scenario: All 7 attrs registered + consumed
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/schema.test.ts packages/chess/src/modifiers/custom/consumer-integration.test.ts
      Expected Result: all pass
      Evidence: .sisyphus/evidence/task-6-attrs.txt
    

    Commit: YES

    • Message: feat(chess): marker + RNG entity attrs
    • Files: packages/chess/src/schema.ts, packages/chess/src/modifiers/custom/apply.ts, packages/chess/src/schema.test.ts
  • 7. Aura compute filter (markers participate)

    What to do:

    • Edit packages/chess/src/modifiers/auras.ts: in computeAuraFacts(), where it walks all entities, change filter from "has PieceType" to "EntityKind in {piece, marker}"
    • Add getEntityKind(session, id): "piece" | "marker" | undefined helper
    • Update existing aura tests + add 2 new tests: marker emits aura → adjacent piece receives; marker receives aura → no-op (markers don't have HP/etc.)
    • Confirm via T4's audit which auras-related callsites need updating

    Must NOT do:

    • Change AuraSpec / AuraContributions shape
    • Allow markers to receive auras for piece-only attrs (Hp, AttackBonus, etc.) — they'd accumulate but never read; document this as benign

    Recommended Agent Profile:

    • Category: deep
      • Reason: subtle behavior change in established subsystem
    • Skills: []

    Parallelization: Wave 2.

    References:

    • packages/chess/src/modifiers/auras.ts:31-90 — computeAuraFacts walker
    • T4 output: .sisyphus/notepads/thressgame-coverage/position-audit.md
    • Notepad reference: learnings.md:74 mentions registerAttrConsumer integrity check

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/auras.test.ts passes (existing + 2 new)
    • Marker emitting aura test green

    QA Scenarios:

    Scenario: Marker can emit aura
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/modifiers/auras.test.ts -t "marker.*aura"
      Expected Result: 2+ tests pass
      Evidence: .sisyphus/evidence/task-7-marker-aura.txt
    

    Commit: YES

    • Message: feat(chess): aura compute includes markers
    • Files: packages/chess/src/modifiers/auras.ts, packages/chess/src/modifiers/auras.test.ts
  • 8. Movement-replacement attrs

    What to do:

    • Edit packages/chess/src/schema.ts: add MovesAs: PieceType | undefined (per piece — overrides movement pattern), MovesAlsoAs: PieceType | undefined (per piece — additive secondary pattern), SlideMustBeMaxDistance: boolean (per piece OR game), BlockAllExceptKing: boolean (game), KingExtraReach: number (per piece)
    • Register all 5 attrs via registerAttrConsumer()
    • Add unit tests asserting each attr exists

    Must NOT do:

    • Wire move-gen yet — that's Wave 7 (Task 40, 41)
    • Set defaults; let move-gen treat undefined as "use natural movement"

    Recommended Agent Profile:

    • Category: unspecified-high
      • Reason: more attr definitions; well-trodden path
    • Skills: []

    Parallelization: Wave 2.

    References: same as T6.

    Acceptance Criteria:

    • bun test packages/chess/src/schema.test.ts extended for 5 new attrs (12 total new since T6)
    • All 5 register via consumer

    QA Scenarios:

    Scenario: 5 movement attrs registered
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/schema.test.ts packages/chess/src/modifiers/custom/consumer-integration.test.ts
      Expected Result: pass
      Evidence: .sisyphus/evidence/task-8-movement-attrs.txt
    

    Commit: YES

    • Message: feat(chess): movement-replacement attrs
    • Files: schema.ts, apply.ts, schema.test.ts
  • 9. Mulberry32 PRNG utility + integration-preset seed init

    What to do:

    • Create packages/chess/src/util/rng.ts exporting class SeededRng { constructor(seed: number); next(): number /* 0..1 */; nextInt(max: number): number; pick<T>(arr: readonly T[]): T }
    • Algorithm: Mulberry32 (one-line, well-known, deterministic). Seed = RngSeed attr; RngStream increments per draw.
    • Edit integration preset (packages/chess/src/modifiers/custom/apply.ts) to seed RNG on game start: engine.session.insert(GAME_ENTITY, "RngSeed", deriveSeedFromGameId(gameId)), engine.session.insert(GAME_ENTITY, "RngStream", 0)
    • Helper: engine.rng() returns new SeededRng(RngSeed + RngStream) and increments RngStream after every next() call
    • Co-located test: rng.test.ts — same seed → same sequence (10 draws); different seed → different sequence; pick uniform across 10000 trials

    Must NOT do:

    • Use Math.random fallback
    • Allow direct new SeededRng(seed) calls outside engine.rng() in production code (test-only)

    Recommended Agent Profile:

    • Category: unspecified-high
      • Reason: standard algorithm; care needed in stream management
    • Skills: []

    Parallelization: Wave 2.

    References:

    • Mulberry32: well-known; verify with checked-in golden sequence in test
    • packages/chess/src/modifiers/custom/apply.ts — integration preset boot path
    • Notepad: learnings.md:240+ — apply.ts onBeforeMove pattern

    Acceptance Criteria:

    • bun test packages/chess/src/util/rng.test.ts passes (3+ tests)
    • Determinism harness (T2) integration: runDeterminismCheck with RNG-using rule passes N=100

    QA Scenarios:

    Scenario: Same seed → byte-identical 100 draws
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/util/rng.test.ts
      Expected Result: pass
      Evidence: .sisyphus/evidence/task-9-rng.txt
    

    Commit: YES

    • Message: feat(chess): seeded Mulberry32 PRNG + integration preset init
    • Files: packages/chess/src/util/rng.ts, packages/chess/src/util/rng.test.ts, packages/chess/src/modifiers/custom/apply.ts
  • 10. Marker entity factory (engine.spawnMarker)

    What to do:

    • Edit packages/chess/src/engine.ts: add spawnMarker(kind: MarkerKind, square: Square, opts: { lifetime: MarkerLifetime; owner?: Color; links?: readonly EntityId[] }): EntityId (mirrors spawnPiece pattern at engine.ts:741-769)
    • Inserts: EntityKind="marker", MarkerKind, Position=square, MarkerLifetime, optional MarkerOwner, optional MarkerLinks
    • Add engine.removeMarker(id: EntityId): void that retracts all marker facts
    • Add engine.getMarkersAtSquare(square: Square): EntityId[] returning sorted by marker-kind priority (use hardcoded priority table from T0 ADR)
    • Co-located tests: spawn → all attrs present; remove → all attrs gone; collision → priority order

    Must NOT do:

    • Allow id collisions with piece entities (use session.nextId)
    • Auto-fire any trigger on spawn (triggers come in Wave 4)

    Recommended Agent Profile:

    • Category: unspecified-high
      • Reason: factory + helpers; clear pattern from spawnPiece
    • Skills: []

    Parallelization: Wave 2.

    References:

    • packages/chess/src/engine.ts:741-769 — spawnPiece pattern
    • T0 decisions.md — marker priority table

    Acceptance Criteria:

    • bun test packages/chess/src/engine.test.ts -t "spawnMarker" passes (5+ tests)
    • Priority order test: spawn mine + portal-end on same square → portal-end first in getMarkersAtSquare result

    QA Scenarios:

    Scenario: Marker priority resolution
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/engine.test.ts -t "marker.*priority"
      Expected Result: pass
      Evidence: .sisyphus/evidence/task-10-marker-priority.txt
    

    Commit: YES

    • Message: feat(chess): marker entity factory + priority resolver
    • Files: packages/chess/src/engine.ts, packages/chess/src/engine.test.ts
  • 11. Binding scope stack on PrimitiveApplyContext

    What to do:

    • Edit packages/chess/src/modifiers/primitives/context.ts: extend PrimitiveApplyContext with bindings: ReadonlyMap<string, EntityId | readonly EntityId[] | Square | number | string>
    • Add helper withBinding(ctx, name, value): PrimitiveApplyContext that returns a NEW context with the binding added (immutable; bindings are scoped via context cloning, not mutation)
    • Update runPrimitives() in triggers.ts to thread the bindings through recursive calls
    • Co-located test: 3 levels of nested binding; later binding shadows earlier; bindings scoped to subtree

    Must NOT do:

    • Mutate the bindings Map in place
    • Allow null/undefined as binding values (use omission instead)

    Recommended Agent Profile:

    • Category: deep
      • Reason: foundation for all binding-using primitives; subtle scoping semantics
    • Skills: []

    Parallelization: Wave 3 (parallel with 12). Blocks: 13, 14, 21+.

    References:

    • packages/chess/src/modifiers/primitives/context.ts — PrimitiveApplyContext shape
    • packages/chess/src/modifiers/triggers.ts:128-144 — context construction; line 144+ for runPrimitives
    • Notepad: learnings.md:118+ for context.ts architecture

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/context.test.ts -t "binding" passes (3+ tests)
    • Existing 13 context tests unchanged

    QA Scenarios:

    Scenario: Binding scope works at depth 3
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/modifiers/primitives/context.test.ts
      Expected Result: ≥ 16 pass (13 existing + 3 new)
      Evidence: .sisyphus/evidence/task-11-bindings.txt
    

    Commit: YES

    • Message: feat(chess): binding scope on PrimitiveApplyContext
    • Files: packages/chess/src/modifiers/primitives/context.ts, context.test.ts
  • 12. Param walker resolves {$var}, {ctx-attr}, {ctx-build} shapes

    What to do:

    • Create packages/chess/src/modifiers/primitives/param-resolver.ts exporting resolveParams(params: unknown, ctx: PrimitiveApplyContext): unknown — recursively walks params, substituting:
      • { $var: "name" } → ctx.bindings.get("name") (throws if unbound)
      • { ctx-attr: { entity: "self" | "chooser" | EntityId; attr: string } } → ctx.session.get(entityId, attr) (throws if unset)
      • { ctx-build: { col: $varOrCharacter; row: $varOrChar } } → resolves to a Square number
      • Otherwise returns value unchanged
    • Wire into runPrimitives() BEFORE param schema validation: resolved params are then Zod-validated
    • Co-located test: 6 tests covering each shape + nested + array of shapes + error cases

    Must NOT do:

    • Modify Zod schemas of existing primitives
    • Allow recursion deeper than primitives recursion limit

    Recommended Agent Profile:

    • Category: deep
      • Reason: cross-cutting walker; must not break existing primitives
    • Skills: []

    Parallelization: Wave 3.

    References:

    • T0 paper exercise (paper-exercise.md) — example shapes
    • T11 — bindings API

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/param-resolver.test.ts passes (6+ tests)
    • All 22 existing primitive tests pass byte-identical (regression: walker must be no-op for unresolved params)

    QA Scenarios:

    Scenario: Walker is no-op for plain params
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/modifiers/primitives/
      Expected Result: existing 147+ tests pass
      Evidence: .sisyphus/evidence/task-12-param-resolver.txt
    

    Commit: YES

    • Message: feat(chess): param walker for binding/ctx-attr/ctx-build resolution
    • Files: packages/chess/src/modifiers/primitives/param-resolver.{ts,test.ts}, packages/chess/src/modifiers/triggers.ts
  • 13. Validator: binding-ref-out-of-scope error

    What to do:

    • Edit packages/chess/src/modifiers/custom/validate.ts: add a binding-scope walker that builds a binding-name set per primitive subtree and rejects any { $var: "X" } reference where X not in scope
    • New error code: descriptor.primitives.binding-out-of-scope
    • Co-located test: 3 cases (in-scope OK, out-of-scope rejected, redefined-shadows-correctly)

    Must NOT do:

    • Validate at runtime — this is descriptor-time validation
    • Reject valid existing descriptors (run T1 baseline regression after change)

    Recommended Agent Profile:

    • Category: unspecified-high
      • Reason: validator extension; pattern well-established
    • Skills: []

    Parallelization: Wave 3.

    References:

    • packages/chess/src/modifiers/custom/validate.ts — existing validator
    • Notepad: learnings.md:668-672 — validate.ts test patterns

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/custom/validate.test.ts -t "binding" passes (3+ tests)
    • T1 baseline regression test passes (no existing descriptor breaks)

    QA Scenarios: same pattern as T12.

    Commit: YES

    • Message: feat(validator): binding-scope check
    • Files: validate.ts, validate.test.ts
  • 14. Validator: imperative-in-passive + chooser-entity activation context

    What to do:

    • Edit validate.ts: walk descriptor tree; if a primitive whose kind is in IMPERATIVE_KINDS set (place-piece, destroy-piece, move-piece, swap-pieces, convert-piece-type, set-piece-attr, cancel-capture, spawn-marker, spawn-marker-pair, destroy-marker) appears OUTSIDE a trigger's primitives or then/else array → reject with error code descriptor.primitives.imperative-in-passive
    • Add IMPERATIVE_KINDS constant; co-locate with validator
    • Add chooser-entity symbolic ref support: when a descriptor activates, the integration preset records the chooser's color in PRESET_STATE_ENTITY (preset-namespaced); ctx-attr: { entity: "chooser", ... } resolves to a piece-id of chooser color (or rejects if no chooser)
    • Co-located test: 4 cases (imperative top-level rejected, imperative-in-trigger OK, chooser-ref resolves, chooser-ref unset error)

    Must NOT do:

    • Hardcode the chooser color in tests; resolve via session

    Recommended Agent Profile:

    • Category: unspecified-high
      • Reason: cross-cutting validation rule
    • Skills: []

    Parallelization: Wave 3.

    References:

    • Existing validator pattern as T13
    • T0 decisions.md — chooser-entity definition
    • packages/chess/src/presets/transferable-royalty.ts:146-148 — presetState pattern for chooser tracking

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/custom/validate.test.ts -t "imperative|chooser" passes (4+ tests)

    QA Scenarios:

    Scenario: Imperative top-level rejected with code
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/modifiers/custom/validate.test.ts -t "imperative-in-passive"
      Expected Result: pass + error code asserted
      Evidence: .sisyphus/evidence/task-14-validator.txt
    

    Commit: YES

    • Message: feat(validator): imperative-in-passive + chooser-entity ref
    • Files: validate.ts + test
  • 15. Deferred trigger queue + cascade depth guard

    What to do:

    • Edit packages/chess/src/modifiers/triggers.ts: add a queue pendingTriggers: Array<{ kind: TriggerName; pieceId: EntityId; payload: unknown }> to PrimitiveApplyContext
    • When an imperative primitive causes a state change that should fire a reactive trigger (e.g. destroy-piece causes on-captured), instead of firing inline, push to pendingTriggers
    • After current arm completes (current runPrimitives() call returns), drain the queue; each drained trigger creates a NEW arm that may itself enqueue more
    • Cascade depth: each arm increments cascadeDepth (separate from existing depth for nested primitives); reject when cascadeDepth > 8 with error runtime.cascade-depth-exceeded
    • Co-located tests: deferred firing OK, depth=8 hits guard

    Must NOT do:

    • Use synchronous reentrant firing
    • Mix cascadeDepth with depth (they're orthogonal)

    Recommended Agent Profile:

    • Category: deep
      • Reason: critical control-flow change in dispatcher
    • Skills: []

    Parallelization: Wave 4.

    References:

    • packages/chess/src/modifiers/triggers.ts:128-144 (context), various fire*Hooks (lines 193-510)
    • T0 decisions.md — deferred queue semantics

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/triggers.test.ts -t "deferred|cascade" passes (3+ tests)
    • Existing 20 trigger tests pass

    QA Scenarios:

    Scenario: Cascade depth=8 fires guard
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/modifiers/triggers.test.ts -t "cascade-depth"
      Expected Result: pass with error code assertion
      Evidence: .sisyphus/evidence/task-15-cascade.txt
    

    Commit: YES

    • Message: feat(chess): deferred trigger queue + cascade depth guard
    • Files: triggers.ts + test
  • 16. on-rule-activated trigger dispatch

    What to do:

    • Create packages/chess/src/modifiers/primitives/on-rule-activated.ts (mirror on-capture.ts shape ~75 lines): kind "on-rule-activated", label "On Rule Activated", seedsAttrs ["OnRuleActivatedHooks"], paramsSchema z.object({ primitives: z.array(NodeSchema) })
    • Add OnRuleActivatedHooks: readonly EffectPrimitiveNode[][] attr (seeded by primitive)
    • Add fireOnRuleActivatedHooks(engine, descriptorId, chooserColor) to triggers.ts — fires nested primitives once with pieceId = GAME_ENTITY, event = { kind: "rule-activated", descriptorId, chooserColor }, chooser resolvable via PRESET_STATE_ENTITY
    • Wire into integration preset's onActivate hook (mirroring piece-hp.ts:93-101 pattern): when descriptor attaches, fire on-rule-activated hooks
    • Co-located test: 6 tests (registry, apply seeds, stacks, fires once, chooser resolves, runs nested primitives)

    Must NOT do:

    • Fire on every game start — only on first activation per game
    • Allow re-firing on game reload from save

    Recommended Agent Profile:

    • Category: deep
      • Reason: novel trigger; integration with preset boot
    • Skills: []

    Parallelization: Wave 4.

    References:

    • packages/chess/src/modifiers/primitives/on-capture.ts — primitive pattern
    • packages/chess/src/modifiers/triggers.ts:193-207 — fire*Hooks pattern
    • packages/chess/src/presets/piece-hp.ts:93-101 — onActivate pattern
    • Notepad: learnings.md:301-340 — on-promotion as nearest equivalent

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/on-rule-activated.test.ts passes (6 tests)
    • bun test packages/chess/src/modifiers/triggers.test.ts -t "on-rule-activated" passes (1+ test)

    QA Scenarios:

    Scenario: Fires once on attachment
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/modifiers/primitives/on-rule-activated.test.ts packages/chess/src/modifiers/triggers.test.ts
      Expected Result: pass
      Evidence: .sisyphus/evidence/task-16-on-rule-activated.txt
    

    Commit: YES

    • Message: feat(chess): on-rule-activated trigger
    • Files: new primitive .ts + test, triggers.ts, schema.ts, apply.ts, types.ts (PrimitiveKind union), index.ts
  • 17. on-rule-expire trigger + per-modifier countdown

    What to do:

    • Create packages/chess/src/modifiers/primitives/on-rule-expire.ts (mirror T16): kind "on-rule-expire", paramsSchema z.object({ countMoves: z.number().int().positive(); primitives: z.array(NodeSchema) })
    • Add OnRuleExpireHooks: readonly { descriptorId: string; expiresAtMove: number; primitives: readonly EffectPrimitiveNode[] }[] attr
    • Add fireOnRuleExpireHooks(engine, currentMoveCount) to triggers.ts — fires when expiresAtMove <= currentMoveCount; auto-removes from list after firing
    • Wire into 14-stage onAfterMove dispatch (after stage 11 = onTurnEnd, before stage 12 = onTurnStart, but ordering TBD by paper exercise)
    • Co-located test: 5 tests

    Must NOT do:

    • Fire eagerly mid-turn — only at onAfterMove drain point

    Recommended Agent Profile:

    • Category: deep
      • Reason: per-modifier timer semantics
    • Skills: []

    Parallelization: Wave 4.

    References: same as T16

    Acceptance Criteria:

    • bun test on-rule-expire.test.ts passes
    • Triggers test extended

    QA Scenarios:

    Scenario: Expires at move count
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/modifiers/primitives/on-rule-expire.test.ts
      Expected Result: pass
      Evidence: .sisyphus/evidence/task-17-on-rule-expire.txt
    

    Commit: YES

    • Message: feat(chess): on-rule-expire trigger + countdown
  • 18. on-piece-entered-marker trigger + marker priority resolver

    What to do:

    • Create packages/chess/src/modifiers/primitives/on-piece-entered-marker.ts: kind "on-piece-entered-marker", paramsSchema z.object({ markerKind: z.enum([...]); primitives: z.array(NodeSchema) })
    • Add OnPieceEnteredMarkerHooks: readonly { markerKind: MarkerKind; primitives }[] attr
    • Add fireOnPieceEnteredMarkerHooks(engine, movedPieceIds, postMovePositions) to triggers.ts — for each moved piece, look up markers at piece's new position via engine.getMarkersAtSquare(square); fire matching hooks in priority order
    • Wire after stage 7 (on-moved-onto-square) in onAfterMove
    • Co-located test: 5 tests including priority order between mine + portal-end

    Must NOT do:

    • Fire for markers spawned during this same trigger arm (use snapshot of pre-move marker positions)

    Recommended Agent Profile:

    • Category: deep
      • Reason: priority-resolution + position-diff dispatch
    • Skills: []

    Parallelization: Wave 4.

    References:

    • T10 — engine.getMarkersAtSquare
    • packages/chess/src/modifiers/triggers.ts:446-460 — fireOnMovedOntoSquareHooks pattern (similar)

    Acceptance Criteria:

    • bun test on-piece-entered-marker.test.ts passes (5 tests)
    • Priority order test green

    QA Scenarios:

    Scenario: Mine + portal both at sq, portal fires first (priority 1 < 3)
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/modifiers/primitives/on-piece-entered-marker.test.ts -t "priority"
      Expected Result: pass; portal effect observed before mine effect
      Evidence: .sisyphus/evidence/task-18-marker-priority.txt
    

    Commit: YES

    • Message: feat(chess): on-piece-entered-marker trigger
  • 19. on-marker-expire trigger + lifetime decrementer

    What to do:

    • Create packages/chess/src/modifiers/primitives/on-marker-expire.ts: kind "on-marker-expire", paramsSchema z.object({ markerKind: z.enum([...]); primitives: z.array(NodeSchema) })
    • Add OnMarkerExpireHooks: readonly { markerKind: MarkerKind; primitives }[] attr
    • In integration preset's onAfterMove (new stage): walk all marker entities; for each whose lifetime is { kind: "moves", expiresAtMove } and expiresAtMove <= currentMoveCount → fire matching on-marker-expire hooks → call engine.removeMarker(id)
    • For one-shot lifetimes: removed by on-piece-entered-marker after firing (T18 must update; cross-task contract documented)
    • Co-located test: 5 tests (N-moves expires, one-shot consumed, permanent never expires, hook fires once)

    Must NOT do:

    • Remove markers BEFORE firing on-marker-expire (event needs marker still in session for context)

    Recommended Agent Profile:

    • Category: deep
      • Reason: lifetime state machine; cross-cutting with T18
    • Skills: []

    Parallelization: Wave 4.

    References: same as T18

    Acceptance Criteria:

    • bun test on-marker-expire.test.ts passes
    • Integration: marker auto-removed after lifetime, on-marker-expire fires once

    QA Scenarios: parallel to T18.

    Commit: YES

    • Message: feat(chess): on-marker-expire trigger + lifetime decrementer
  • 20. Move-gen suppressTriggers flag wired through registry

    What to do:

    • Edit packages/chess/src/presets/registry.ts: extend getLegalMoveModifiers and movement-related hooks to accept opts: { suppressTriggers: boolean } parameter
    • Edit packages/chess/src/move-generator.ts (or wherever move-gen entry is): pass suppressTriggers: true when running in dry mode (e.g. for check detection)
    • Edit triggers.ts runPrimitives: when ctx.suppressTriggers === true, skip imperative primitives that would mutate state (no-op return)
    • Co-located test: dry-mode invocation does not fire triggers, real move commit does

    Must NOT do:

    • Apply suppress flag globally — only during move-gen
    • Skip non-mutating primitives in dry mode

    Recommended Agent Profile:

    • Category: deep
      • Reason: move-gen path is performance-critical and central to engine
    • Skills: []

    Parallelization: Wave 4.

    References:

    • packages/chess/src/presets/registry.ts — getLegalMoveModifiers contract
    • packages/chess/src/move-generator.ts (or equivalent) — entry point

    Acceptance Criteria:

    • Existing move-gen tests pass byte-identical
    • New test: dry-mode does not invoke imperative primitives

    QA Scenarios:

    Scenario: Dry-mode suppresses imperative primitives
      Tool: Bash
      Steps:
        1. bun test packages/chess/src/move-generator.test.ts -t "suppressTriggers"
      Expected Result: pass
      Evidence: .sisyphus/evidence/task-20-suppress-triggers.txt
    

    Commit: YES

    • Message: feat(chess): move-gen suppressTriggers flag for dry-mode

WAVE 5 PRIMITIVES TEMPLATE NOTE: Tasks 21-27 are imperative piece-mutation primitives. Each follows the same template: create <kind>.ts (~50-80 lines), add paramsSchema, register in registry, add to PrimitiveKind union in types.ts, add side-effect import in index.ts, add SAMPLE_PARAMS entry in ParamField.snapshot.test.tsx, add narrate.ts entry, add palette category. Each ships with a co-located test (5+ assertions). Each task is one atomic commit.

  • 21. place-piece primitive

    What to do:

    • kind: "place-piece", schema: { pieceType: PieceType, color: Color | { ctx-attr } | { $var }, square: Square | { $var } | { ctx-build }, replaceExisting: boolean (default false) }
    • apply(): resolve params via param-resolver; if replaceExisting=false and square occupied → no-op; else engine.spawnPiece(pieceType, color, square)
    • Imperative-only (validator rejects in passive)

    Must NOT do: spawn at occupied square unless replaceExisting=true; auto-fire on-rule-activated for spawned piece's modifiers (cascade via deferred queue from T15)

    Recommended Agent Profile: unspecified-high. Skills: [].

    Parallelization: Wave 5 (parallel with 22-27).

    References: packages/chess/src/modifiers/primitives/seed-attribute.ts (similar structure), engine.spawnPiece (T6, engine.ts:741-769)

    Acceptance Criteria: bun test place-piece.test.ts passes (5 tests); validator rejects at top level

    QA Scenarios:

    Scenario: Place + replace + occupied no-op
      Tool: Bash
      Steps: bun test packages/chess/src/modifiers/primitives/place-piece.test.ts
      Expected: pass
      Evidence: .sisyphus/evidence/task-21-place-piece.txt
    

    Commit: YES — feat(chess): place-piece imperative primitive

  • 22. destroy-piece primitive

    What to do: kind: "destroy-piece", schema: { target: TargetResolver | { $var } }. apply(): resolve target → for each entity → retract all piece facts via engine.session.retract(id, attr) for piece attrs (PieceType, Color, Position, HasMoved, Hp, etc.). Special-case: if target is king, no-op (kings invulnerable to destroy-piece by convention; on-captured handled separately) Must NOT do: destroy markers (filter EntityKind === "piece"); destroy GAME_ENTITY Recommended Agent Profile: unspecified-high Parallelization: Wave 5 References: T11 bindings, target resolver in packages/chess/src/modifiers/primitives/context.ts Acceptance Criteria: tests pass (5+); validator rejects at top level QA Scenarios: bun test destroy-piece.test.ts → .sisyphus/evidence/task-22-destroy-piece.txt Commit: YES — feat(chess): destroy-piece imperative primitive

  • 23. move-piece primitive

    What to do: kind: "move-piece", schema: { from: Square | { $var }, to: Square | { $var }, allowCapture: boolean (default false) }. apply(): if from empty → no-op; if to occupied and !allowCapture → no-op; if to occupied and allowCapture → enqueue on-captured event via T15 deferred queue, then move; update Position via session.insert Must NOT do: bypass check detection (use raw fact updates; check resolution happens at next move-gen) Recommended Agent Profile: unspecified-high Parallelization: Wave 5 References: same as T22 Acceptance Criteria: 6 tests pass (move + capture + empty-from + occupied-to + cascade) QA Scenarios: bun test move-piece.test.ts → .sisyphus/evidence/task-23-move-piece.txt Commit: YES — feat(chess): move-piece imperative primitive

  • 24. swap-pieces primitive

    What to do: kind: "swap-pieces", schema: { a: Square | { $var }, b: Square | { $var } }. apply(): get pieces at a + b; insert positions swapped; both Position attrs updated atomically Must NOT do: swap with markers (skip if EntityKind !== piece on either side) Recommended Agent Profile: unspecified-high Parallelization: Wave 5 References: T22, T23 Acceptance Criteria: 4 tests pass (both occupied, one empty no-op, both empty no-op, marker on square excluded) QA Scenarios: bun test swap-pieces.test.ts → .sisyphus/evidence/task-24-swap-pieces.txt Commit: YES — feat(chess): swap-pieces imperative primitive

  • 25. convert-piece-type primitive

    What to do: kind: "convert-piece-type", schema: { target: TargetResolver | { $var }, newType: PieceType | { $var } }. apply(): for each resolved target, retract PieceType, insert newType. Preserves Color, Position, HasMoved, all custom attrs Must NOT do: convert-to-king (special-case rejected; document) Recommended Agent Profile: unspecified-high Parallelization: Wave 5 References: T22 Acceptance Criteria: 5 tests pass (queen→bishop, pawn→knight, multiple targets, king target rejected, non-piece target skipped) QA Scenarios: bun test convert-piece-type.test.ts → .sisyphus/evidence/task-25-convert.txt Commit: YES — feat(chess): convert-piece-type imperative primitive

  • 26. set-piece-attr primitive (generic, with target binding)

    What to do: kind: "set-piece-attr", schema: { target: TargetResolver | { $var }, attr: string, value: unknown | { $var } | { ctx-attr } }. apply(): resolve target, attr, value; insert fact. Validates attr is in ChessAttrMap Must NOT do: set on markers; allow attr name not in ChessAttrMap Recommended Agent Profile: unspecified-high Parallelization: Wave 5 References: T22, T25, schema.ts ChessAttrMap Acceptance Criteria: 5 tests (set Hp, set Color, attr not in map → reject, target piece, target marker → skip) QA Scenarios: bun test set-piece-attr.test.ts → .sisyphus/evidence/task-26-set-piece-attr.txt Commit: YES — feat(chess): set-piece-attr generic mutator

  • 27. cancel-capture primitive

    What to do: kind: "cancel-capture", schema: {} (no params; reads event from ctx). apply(): assert ctx.event.kind === "capture" → restore defender by reverting all retractions performed during capture. Implementation: integration preset records pre-capture defender facts in PRESET_STATE_ENTITY; cancel-capture reads + restores. If no capture event in ctx → throw. Must NOT do: revert if event kind ≠ capture; allow at top level (validator rejects) Recommended Agent Profile: deep Parallelization: Wave 5 (alone in deep tier of this wave) References: packages/chess/src/modifiers/triggers.ts:476-510 — fireOnCapturedHooks (where event is constructed); event field on PrimitiveApplyContext from context.ts Acceptance Criteria: 4 tests (cancel restores defender, throws when not in capture event, validator rejects at top, parry rule integration sanity-check) QA Scenarios: bun test cancel-capture.test.ts → .sisyphus/evidence/task-27-cancel-capture.txt Commit: YES — feat(chess): cancel-capture interrupt primitive

WAVE 6 PRIMITIVES: Marker primitives + iteration. Same atomic-commit template per task.

  • 28. spawn-marker primitive

    What to do: kind: "spawn-marker", schema: { markerKind: MarkerKind, square: Square | { $var } | { ctx-build }, lifetime: MarkerLifetime, owner?: Color | { $var } | { ctx-attr } }. apply(): engine.spawnMarker(markerKind, square, { lifetime, owner }). Imperative-only. Must NOT do: spawn on a square that already has same-kind marker (no-op; document); spawn outside trigger Recommended Agent Profile: unspecified-high Parallelization: Wave 6 References: T10 spawnMarker Acceptance Criteria: 5 tests (spawn permanent / N-moves / one-shot, owner respected, dedup same-kind) QA Scenarios: bun test spawn-marker.test.ts → .sisyphus/evidence/task-28-spawn-marker.txt Commit: YES — feat(chess): spawn-marker primitive

  • 29. spawn-marker-pair primitive (portal pairs)

    What to do: kind: "spawn-marker-pair", schema: { markerKind: "portal-end" (literal), squareA: Square | { $var }, squareB: Square | { $var }, lifetime: MarkerLifetime }. apply(): spawn 2 markers, then update each's MarkerLinks to reference the other's id Must NOT do: allow non-portal-end pair kinds in V1; allow same square twice Recommended Agent Profile: unspecified-high Parallelization: Wave 6 References: T28 Acceptance Criteria: 4 tests (pair created, links bidirectional, same-square rejected, non-portal-end rejected) QA Scenarios: bun test spawn-marker-pair.test.ts → .sisyphus/evidence/task-29-portal-pair.txt Commit: YES — feat(chess): spawn-marker-pair primitive

  • 30. destroy-marker primitive

    What to do: kind: "destroy-marker", schema: { target: { id: EntityId | { $var } } | { kind: MarkerKind, square: Square | { $var } } }. apply(): resolve to one or more marker ids → engine.removeMarker(id). If marker has paired link, do NOT auto-destroy partner (orphan remains; document; partner will eventually expire via lifetime) Must NOT do: cascade-destroy paired markers (V1 explicit decision; V2 may revisit) Recommended Agent Profile: unspecified-high Parallelization: Wave 6 References: T10 Acceptance Criteria: 5 tests (destroy by id, destroy by kind+square, partner preserved, non-marker target no-op, validator top-level rejected) QA Scenarios: bun test destroy-marker.test.ts → .sisyphus/evidence/task-30-destroy-marker.txt Commit: YES — feat(chess): destroy-marker primitive

  • 31. for-each-piece (filter + binding)

    What to do: kind: "for-each-piece", schema: { filter: PieceFilter, bind: string, then: EffectPrimitiveNode[] }. PieceFilter shape: { pieceType?: PieceType[], color?: Color, relation?: "ally"|"enemy", excludeKing?: boolean, square?: Square }. apply(): iterate matching pieces (snapshot to avoid mutation-during-iteration); for each, recurse runPrimitives with withBinding(ctx, bindParam, pieceId) Must NOT do: iterate live (snapshot first); fire reactive triggers during iteration (use deferred queue) Recommended Agent Profile: deep Parallelization: Wave 6 References: T11 bindings, T15 deferred queue Acceptance Criteria: 6 tests (filter by type, by color, by relation, snapshot semantics, binding accessible in then, empty filter no-op) QA Scenarios: bun test for-each-piece.test.ts → .sisyphus/evidence/task-31-for-each-piece.txt Commit: YES — feat(chess): for-each-piece iteration primitive

  • 32. for-each-square (filter + binding)

    What to do: kind: "for-each-square", schema: { filter: SquareFilter, bind: string, then: EffectPrimitiveNode[] }. SquareFilter shape: { kind: "all" } | { kind: "files", files: number[] } | { kind: "ranks", ranks: number[] } | { kind: "color", color: "light"|"dark" } | { kind: "occupied", value: boolean }. apply(): iterate matching squares (0..63); bind square number; recurse Must NOT do: iterate beyond board (filter range) Recommended Agent Profile: deep Parallelization: Wave 6 References: T31 Acceptance Criteria: 7 tests (each filter kind + binding + empty match) QA Scenarios: bun test for-each-square.test.ts → .sisyphus/evidence/task-32-for-each-square.txt Commit: YES — feat(chess): for-each-square iteration primitive

  • 33. for-each-adjacent (target + filter + binding)

    What to do: kind: "for-each-adjacent", schema: { target: TargetResolver | { $var }, filter: PieceFilter, bind: string, then: EffectPrimitiveNode[] }. apply(): resolve target → for each, find 8 adjacent squares (king-move neighborhood) → check piece occupancy + filter → bind + recurse Must NOT do: include diagonal-only or orthogonal-only (always 8-direction); include the target itself Recommended Agent Profile: deep Parallelization: Wave 6 References: T31, T32 Acceptance Criteria: 5 tests (filter applies, edge piece has 5 neighbors, corner piece has 3, ally/enemy filter, binding accessible) QA Scenarios: bun test for-each-adjacent.test.ts → .sisyphus/evidence/task-33-for-each-adjacent.txt Commit: YES — feat(chess): for-each-adjacent iteration primitive

  • 34. for-each-marker (filter + binding)

    What to do: kind: "for-each-marker", schema: { filter: { markerKind?: MarkerKind, owner?: Color, square?: Square }, bind: string, then: EffectPrimitiveNode[] }. apply(): walk all marker entities (EntityKind === "marker"); apply filter; bind id; recurse Must NOT do: include piece entities Recommended Agent Profile: unspecified-high Parallelization: Wave 6 References: T10 Acceptance Criteria: 5 tests QA Scenarios: bun test for-each-marker.test.ts → .sisyphus/evidence/task-34-for-each-marker.txt Commit: YES — feat(chess): for-each-marker iteration primitive

  • 35. for-column / for-row primitives

    What to do: Two primitives. for-column: schema { columns: number[] | { $var }, bind: string, then: ... } iterates given columns. for-row: schema { rows: number[] | { $var }, bind: string, then: ... } iterates given rows. Bound value: column index 0-7 or row index 0-7. Must NOT do: iterate beyond 0-7 range; mix column and row in single primitive Recommended Agent Profile: unspecified-high Parallelization: Wave 6 References: T32 Acceptance Criteria: 4 tests (each + cross-product use case via nesting) QA Scenarios: bun test for-column.test.ts for-row.test.ts → .sisyphus/evidence/task-35-for-col-row.txt Commit: YES — feat(chess): for-column + for-row primitives

WAVE 7 PRIMITIVES: RNG + restriction + movement-replacement.

  • 36. with-probability primitive

    What to do: kind: "with-probability", schema: { p: z.number().min(0).max(1), then: NodeArray, else?: NodeArray }. apply(): draw engine.rng().next() → if < p, run then arm; else run else arm (or no-op if absent). RNG draw happens BEFORE any nested primitive (locked V1 invariant — no draws after suspension) Must NOT do: draw RNG inside nested primitive arms (only at top of with-probability); nest request-choice in then/else (validator rejects — V1 simplification: no draws-then-suspend interleaving) Recommended Agent Profile: unspecified-high Parallelization: Wave 7 References: T9 RNG; T0 ADR for "draws before suspension" invariant Acceptance Criteria: 5 tests (p=0 always else, p=1 always then, mid-p distribution test 1000 trials, then runs nested, else absent no-op) QA Scenarios: bun test with-probability.test.ts → .sisyphus/evidence/task-36-with-probability.txt Commit: YES — feat(chess): with-probability primitive

  • 37. random-pick primitive (with binding)

    What to do: kind: "random-pick", schema: { from: TargetResolver | { kind: "squares", filter: SquareFilter } | { kind: "markers", filter }, count: number (default 1), bind: string, then: NodeArray }. apply(): resolve from to candidate set → use engine.rng().pick() count times (without replacement) → bind picks (single id or array depending on count) → recurse Must NOT do: pick with replacement; pick from empty set (no-op rather than error per L0 ADR); allow count > candidate set size (clamp to size) Recommended Agent Profile: deep Parallelization: Wave 7 References: T9, T11 Acceptance Criteria: 6 tests (count=1 single id, count=N array, empty no-op, deterministic with seed, without replacement, binding accessible in then) QA Scenarios: bun test random-pick.test.ts → .sisyphus/evidence/task-37-random-pick.txt Commit: YES — feat(chess): random-pick primitive

  • 38. must-class primitive (capture/advance/move-to)

    What to do: kind: "must-class", schema: { class: "capture-if-possible" | "advance-if-possible" | "move-to-square", color?: Color, square?: Square (for move-to) }. apply(): seeds an entry into game-entity attr MustClassConstraints: readonly { class, color?, square? }[]. Move-gen reads this attr and filters legal moves accordingly: if any move matches the class, ONLY those moves are legal; else fall through. Must NOT do: enforce in primitive; just seed (logic is in move-gen) Recommended Agent Profile: deep Parallelization: Wave 7 References: existing BlockedMoveTypes pattern in modify-movement-range (similar move-gen constraint) Acceptance Criteria: 5 tests (each class type + interaction with move-gen) QA Scenarios: bun test must-class.test.ts → .sisyphus/evidence/task-38-must-class.txt Commit: YES — feat(chess): must-class restriction primitive

  • 39. block-by-piece-type primitive

    What to do: kind: "block-by-piece-type", schema: { pieceTypes: PieceType[] }. apply(): seeds game-attr BlockedPieceTypes: readonly PieceType[] (set union). Move-gen filters: pieces of these types have no legal moves Must NOT do: block king (game becomes unwinnable; validator rejects at descriptor time) Recommended Agent Profile: unspecified-high Parallelization: Wave 7 References: T38 Acceptance Criteria: 4 tests QA Scenarios: bun test block-by-piece-type.test.ts → .sisyphus/evidence/task-39-block-piece-type.txt Commit: YES — feat(chess): block-by-piece-type primitive

  • 40. set-moves-as / set-moves-also-as primitives

    What to do: Two primitives. set-moves-as: schema { target: TargetResolver | { $var }, asType: PieceType } — replaces movement pattern. set-moves-also-as: same schema — adds secondary pattern (additive). apply(): seeds MovesAs / MovesAlsoAs attr on target. Move-gen consults these BEFORE PieceType for movement generation. Must NOT do: seed on king with conflicting MovesAs (would prevent castling; validator warns); seed MovesAs: "king" on pawn (special-case rejected — pawn promotion semantics break; document) Recommended Agent Profile: deep Parallelization: Wave 7 References: existing PIECE_TYPE_REGISTRY in packages/chess/src/ Acceptance Criteria: 6 tests (replace, additive, invalid targets rejected, move-gen integration) QA Scenarios: bun test set-moves-as.test.ts → .sisyphus/evidence/task-40-set-moves-as.txt Commit: YES — feat(chess): set-moves-as + set-moves-also-as primitives

  • 41. pawn-pushes-pieces primitive

    What to do: kind: "pawn-pushes-pieces", schema: {}. apply(): seeds game-attr PawnPushesPieces: true. Move-gen: when pawn moves into occupied square, generate "push" move where occupant moves forward 1; chain reaction (each pushed piece pushes next); piece pushed off-board is destroyed via deferred event Must NOT do: push king (ends game; rejected at gen time); chain length > 7 (board height) Recommended Agent Profile: unspecified-high Parallelization: Wave 7 References: ThressGame's pawns_learned_strength (lines 1707-1739 of ruleHooks.js) Acceptance Criteria: 5 tests (single push, chain, off-board destroy, king cannot be pushed, regression: existing pawn moves unchanged when flag absent) QA Scenarios: bun test pawn-pushes-pieces.test.ts → .sisyphus/evidence/task-41-pawn-push.txt Commit: YES — feat(chess): pawn-pushes-pieces primitive

  • 42. lifetime field on imperative primitives

    What to do: Update schemas of seed-attribute, set-piece-attr, spawn-marker, spawn-marker-pair to ALL accept optional top-level lifetime: MarkerLifetime field. Engine: add LifetimeRegistry that tracks "fact X on entity Y expires at move N"; in onAfterMove, scan registry → retract expired facts. Lifetime applies uniformly to all imperative primitives that seed facts. Must NOT do: support lifetime on read-only primitives (with-probability, conditional, etc.); add lifetime to existing modifier-bonus attrs (RangeBonus etc.) Recommended Agent Profile: deep Parallelization: Wave 7 (after T28-T30 land lifetime field in spawn-marker; this generalizes the pattern) References: T28, T17 (on-rule-expire pattern) Acceptance Criteria: 5 tests (lifetime on each kind expires correctly, permanent never expires, immediate-expire = next-turn) QA Scenarios: bun test lifetime-registry.test.ts → .sisyphus/evidence/task-42-lifetime.txt Commit: YES — feat(chess): unified lifetime field on imperative primitives

WAVE 8 — WS PROTOCOL v2 + SUSPENDED EXECUTION: highest-risk wave. Each task is its own commit; integration tests at the end.

  • 43. WS protocol v2 schema

    What to do:

    • Edit packages/server/src/protocol.ts: add new message types RequestChoiceMessage (server→client: { kind: "request-choice", choiceId: string, prompt: { kind: "piece"|"square"|"column"|"row"|"coin-flip"|"rps", filter?, forPlayer: Color, timeout?: number } }) and SubmitChoiceMessage (client→server: { kind: "submit-choice", choiceId: string, value: unknown })
    • Add protocolVersion: number field to existing JoinRoom message; default 1; new client sends 2
    • Server stores client's protocolVersion; if version mismatch and request-choice would fire → server falls back to "auto-resolve with first option" path AND emits a warning
    • Co-located test: contract round-trip; version mismatch graceful Must NOT do: break v1 message shapes (additive only); introduce required new fields on existing messages (use optional) Recommended Agent Profile: deep Parallelization: Wave 8 References: packages/server/src/protocol.ts:540-548 (existing wire schema patterns) Acceptance Criteria: bun test packages/server/src/protocol.test.ts -t "v2|version" passes (4+ tests); v1 tests unchanged QA Scenarios: .sisyphus/evidence/task-43-protocol-v2.txt Commit: YES — feat(server): WS protocol v2 schema (request-choice + version negotiation)
  • 44. Server-side request-choice broadcast + validation

    What to do:

    • Edit packages/server/src/ws.ts (or equivalent ws handler): when game state has a pendingChoices entry, server sends RequestChoiceMessage to the targeted player on connect/reconnect
    • When SubmitChoiceMessage arrives: validate choiceId matches top of stack on game's GAME_ENTITY pendingChoices, validate sender is targeted player, validate value matches choice kind/filter, then dispatch submit-choice PlayerAction to engine
    • If invalid (mismatched id, wrong player, value violates filter): respond with versioned ILLEGAL_ACTION error code choice.invalid Must NOT do: broadcast choice to non-targeted players (privacy); accept submission for non-top-of-stack choice (must be LIFO) Recommended Agent Profile: deep Parallelization: Wave 8 References: packages/server/src/ws.ts; T43 Acceptance Criteria: bun test packages/server/src/ws.request-choice.test.ts passes (5+ tests) QA Scenarios: .sisyphus/evidence/task-44-server-choice.txt Commit: YES — feat(server): request-choice broadcast + validation
  • 45. Stack-based pendingChoices state on GAME_ENTITY + serializer

    What to do:

    • Add attr PendingChoices: readonly PendingChoice[] to ChessAttrMap. PendingChoice = { choiceId: string, descriptorId: string, triggerPath: readonly number[], primitiveIndex: number, bindings: Record<string, JsonValue>, kind, prompt, forPlayer, timeout?: number, expiresAtTimestamp?: number }
    • Helpers in engine: engine.pushPendingChoice(choice), engine.popPendingChoice(): PendingChoice | null, engine.peekPendingChoice(): PendingChoice | null
    • Serializer: assert all fields are POJO-serializable (no closures, no functions); test by JSON round-trip Must NOT do: store function references in PendingChoice; allow >8 deep stack (cascade depth limit) Recommended Agent Profile: deep Parallelization: Wave 8 References: T15 cascade depth, T6 attr registration Acceptance Criteria: 5 tests pass; JSON round-trip preserves byte-identical QA Scenarios: .sisyphus/evidence/task-45-pending-choices.txt Commit: YES — feat(chess): pendingChoices stack on GAME_ENTITY
  • 46. Suspended-execution resume in integration preset

    What to do:

    • Edit integration preset's performAction hook: when action is submit-choice, pop top PendingChoice, restore bindings into a fresh PrimitiveApplyContext, resume runPrimitives at saved triggerPath + primitiveIndex + 1 (skip past the request-choice that caused suspension), inject the submitted value as binding (key matches request-choice's bind param)
    • Triggers must be re-entrant: runPrimitives accepts an optional resumeAt: { primitiveIndex: number; bindings: ... } that fast-forwards to that point Must NOT do: re-fire the request-choice itself; advance turn before resume completes; lose deferred trigger queue across suspension (preserve in PendingChoice) Recommended Agent Profile: deep Parallelization: Wave 8 References: packages/chess/src/presets/transferable-royalty.ts:143-161 (performAction pattern); T11, T15 Acceptance Criteria: 5 tests (basic resume, nested resume, binding injection, deferred-queue preserved across suspension, error if no pending) QA Scenarios: .sisyphus/evidence/task-46-resume.txt Commit: YES — feat(chess): suspended execution resume
  • 47. request-choice primitive

    What to do:

    • Create packages/chess/src/modifiers/primitives/request-choice.ts: kind "request-choice", schema { kind: "piece"|"square"|"column"|"row"|"coin-flip"|"rps", forPlayer: "chooser"|"opponent"|"both", filter?, bind: string, then: NodeArray }
    • apply(): generate choiceId (deterministic from descriptorId + step), capture current binding context, push PendingChoice on GAME_ENTITY, then RETURN (do not execute then; resume happens later)
    • Special case forPlayer: "both": TWO PendingChoice entries pushed; both must submit; resume only when both received
    • Special case kind: "rps": each player submits rock/paper/scissors privately; resolution via resolveRPS helper; binding receives both choices for then arm to evaluate Must NOT do: execute then arm immediately (suspend instead); allow submitting other-player's choice Recommended Agent Profile: deep Parallelization: Wave 8 (after T43, T45, T46) References: T46, T11; T0 paper exercise rule 1 (parry — RPS use case) Acceptance Criteria: 8 tests (each kind + both-players + binding + cancellation + RPS resolution + nested suspension + max-depth) QA Scenarios: .sisyphus/evidence/task-47-request-choice.txt Commit: YES — feat(chess): request-choice primitive
  • 48. Deterministic auto-resolver test transport

    What to do:

    • Create packages/chess/src/__fixtures__/test-choice-resolver.ts: a test-only WS transport mock that auto-resolves PendingChoices according to a deterministic policy:
      • kind: "piece"|"square"|"column"|"row": pick first valid option
      • kind: "coin-flip": alternate heads/tails per call (cycle starts at heads)
      • kind: "rps": alternate rock/paper/scissors per call
    • Co-located test: 3 invocations of each kind produce expected sequence
    • Used by: T59-T66 parity tests + T68 e2e tests Must NOT do: use Math.random; allow non-deterministic order; ship in production bundle Recommended Agent Profile: unspecified-high Parallelization: Wave 8 References: T47 Acceptance Criteria: 4 tests; resolver is pure deterministic QA Scenarios: .sisyphus/evidence/task-48-test-resolver.txt Commit: YES — test(chess): deterministic auto-resolver test transport
  • 49. Choice timeout + disconnect handler

    What to do:

    • Edit ws.ts: when PendingChoice has timeout field, server schedules a timer; on expiry, server auto-submits the "first valid option" as the choice and resumes
    • When player disconnects: check if game has pendingChoices for that player. If timeout mode = timeout-with-default → forfeit the game (set GameStatus to opponent-wins). If mode = no-timeout → set game state to "paused" (no auto-action; resume on reconnect)
    • Co-located test: timer fires after timeout; default-option resolution; forfeit; pause+reconnect Must NOT do: cancel pending choice on disconnect mid-choice without policy decision; forfeit in no-timeout mode Recommended Agent Profile: deep Parallelization: Wave 8 References: T44, T50 Acceptance Criteria: 6 tests QA Scenarios: .sisyphus/evidence/task-49-timeout-disconnect.txt Commit: YES — feat(server): choice timeout + disconnect handler
  • 50. Game settings: choiceTimeout in CreateGameRequest

    What to do:

    • Edit packages/server/src/protocol.ts CreateGameRequest schema: add choiceTimeout: { mode: "timeout-with-default", seconds: number } | { mode: "no-timeout" } field; default = { mode: "timeout-with-default", seconds: 60 }
    • Edit client (packages/chess/src/ui/... create-game form): add radio + numeric input
    • Server stores in game state; T49 reads from this
    • Co-located test: schema validation; defaults; round-trip via WS Must NOT do: hardcode timeout in server; allow negative seconds; allow seconds < 5 (UX guard) Recommended Agent Profile: unspecified-high Parallelization: Wave 8 References: T49 Acceptance Criteria: 5 tests QA Scenarios: .sisyphus/evidence/task-50-game-settings.txt Commit: YES — feat: choiceTimeout per-game setting

WAVE 9 — UI / EDITOR: parallel with Waves 5-8. Each task atomic.

  • 51. Palette taxonomy update (5 categories)

    What to do:

    • Edit packages/chess/src/ui/visual-builder/VisualBuilderPane.tsx and BlockCard.tsx: replace existing 3 categories (State, Mechanic, Trigger) with 5: State (existing 5 kinds), Mechanic (existing 5 kinds), Trigger (existing 9 + 4 new = 13 kinds), Imperative (place-piece, destroy-piece, move-piece, swap-pieces, convert-piece-type, set-piece-attr, cancel-capture, spawn-marker, spawn-marker-pair, destroy-marker), Iteration (for-each-piece, for-each-square, for-each-adjacent, for-each-marker, for-column, for-row), Restriction (must-class, block-by-piece-type, set-moves-as, set-moves-also-as, pawn-pushes-pieces) — total 6 categories
    • Each category gets distinct color: State=blue (existing), Mechanic=emerald (existing), Trigger=violet (existing), Imperative=amber, Iteration=cyan, Restriction=rose
    • Update CATEGORIES const to single source of truth (currently duplicated in BlockCard + VisualBuilderPane — DRY) Must NOT do: add primitives not in the locked list of 28; expand to 7+ categories Recommended Agent Profile: visual-engineering. Skills: [frontend-ui-ux]. Parallelization: Wave 9 References: existing CATEGORIES in packages/chess/src/ui/visual-builder/BlockCard.tsx:41-78 and VisualBuilderPane.tsx:47-83 (notepad mentions duplication: learnings.md~) Acceptance Criteria: SSR snapshot tests pass; new palette buttons present for all 28 new primitives QA Scenarios:
    Scenario: All 28 new primitive palette buttons render
      Tool: Bash
      Steps: bun test packages/chess/src/ui/visual-builder/VisualBuilderPane.test.tsx -t "palette"
      Expected: pass; assert all 50 testids present
      Evidence: .sisyphus/evidence/task-51-palette.txt
    

    Commit: YES — feat(ui): 6-category palette taxonomy with new primitives

  • 52. narrate.ts entries for new primitives + triggers

    What to do:

    • Edit packages/chess/src/ui/narrate.ts: extend KIND_NARRATORS map with entries for all 28 new primitives + 4 new triggers (32 total entries)
    • Each narrator returns a one-sentence English description of what the primitive does, properly templated with params
    • Edit narrate.test.ts: add per-primitive golden assertion (32 new tests, exact strings)
    • Update ParamField.snapshot.test.tsx SAMPLE_PARAMS map to include all 28 + 4 new kinds Must NOT do: re-narrate existing 22 primitives ("while we're in there"); use template variables that don't resolve from params Recommended Agent Profile: writing. Skills: []. Parallelization: Wave 9 References: packages/chess/src/ui/narrate.ts:KIND_NARRATORS; notepad learnings.md:200-238 (T15 narrate pattern) Acceptance Criteria: narrate.test.ts: 34 → 66 tests pass (32 new); ParamField.snapshot tests still 50 pass QA Scenarios: .sisyphus/evidence/task-52-narrate.txt Commit: YES — feat(ui): narrate entries for new primitives + triggers
  • 53. ParamField renderer: square picker

    What to do:

    • Add to packages/chess/src/ui/ParamField.tsx: when a param's Zod schema is z.number().int().min(0).max(63) AND param key is square|target.square|squareA|squareB, render a small 8×8 grid of clickable squares (current value highlighted); click sets the value
    • Co-located snapshot test: rendering deterministic
    • Visual: 200px × 200px grid, light/dark squares alternating Must NOT do: render for non-square numeric params (need explicit kind detection); animate Recommended Agent Profile: visual-engineering. Skills: [frontend-ui-ux]. Parallelization: Wave 9 References: existing AttrCombobox in ParamField.tsx (similar custom renderer pattern) Acceptance Criteria: Snapshot test deterministic; param with square key renders grid (testid="param-square-picker") QA Scenarios: .sisyphus/evidence/task-53-square-picker.txt Commit: YES — feat(ui): ParamField square picker renderer
  • 54. ParamField renderer: piece picker

    What to do:

    • Add to ParamField.tsx: when param key is pieceType|target.pieceType|asType|newType and Zod is z.enum([...PieceTypes]), render a row of 6 piece icons (king, queen, rook, bishop, knight, pawn); click selects
    • Use unicode chess symbols or the existing piece SVGs Must NOT do: render for non-piece-type enums; require image assets (use unicode if no asset exists) Recommended Agent Profile: visual-engineering. Skills: [frontend-ui-ux]. Parallelization: Wave 9 References: T53 Acceptance Criteria: snapshot test; testid="param-piece-picker" present for matching keys QA Scenarios: .sisyphus/evidence/task-54-piece-picker.txt Commit: YES — feat(ui): ParamField piece picker renderer
  • 55. ParamField renderer: marker-kind enum

    What to do:

    • Add to ParamField.tsx: when param key is markerKind and Zod is the marker-kind enum, render dropdown OR icon grid (8 marker kinds with mini-icons)
    • Each marker kind gets a small inline icon: mine=💣, pit=⬛, portal-end=🌀, frozen-square=❄, treasure=🏆, death-square=☠, tornado=🌪, blocked=🧱 (use these emoji fallbacks; replace with SVG later if user adds assets) Must NOT do: require new icon assets; render as plain text Recommended Agent Profile: visual-engineering. Skills: [frontend-ui-ux]. Parallelization: Wave 9 References: T53, T54 Acceptance Criteria: snapshot test; all 8 kinds renderable QA Scenarios: .sisyphus/evidence/task-55-marker-kind-picker.txt Commit: YES — feat(ui): ParamField marker-kind enum renderer
  • 56. ParamField renderer: lifetime config

    What to do:

    • Add to ParamField.tsx: when param shape matches MarkerLifetime discriminated union, render a kind-selector (radio: permanent/moves/one-shot) + conditional numeric input for moves count (visible only when kind=moves)
    • Form validates: count ≥ 1 when kind=moves Must NOT do: allow count when kind ≠ moves (hide the field; clear value) Recommended Agent Profile: visual-engineering. Skills: [frontend-ui-ux]. Parallelization: Wave 9 References: T53; existing discriminated-union pattern in packages/chess/src/modifiers/primitives/on-moved-onto-square.ts SquareFilter Acceptance Criteria: snapshot test; all 3 lifetime kinds renderable; count input visible only when relevant QA Scenarios: .sisyphus/evidence/task-56-lifetime-renderer.txt Commit: YES — feat(ui): ParamField lifetime config renderer
  • 57. Client marker rendering on chessboard

    What to do:

    • Edit chessboard component (packages/chess/src/ui/board/... — find via glob): add a marker overlay layer that renders on top of squares
    • For each marker entity, render a small icon at its Position square (using emoji from T55 + tooltip showing marker kind + lifetime + owner)
    • Tooltips appear on hover (CSS); accessible via aria-label
    • Markers render BELOW pieces (z-order); piece on marker still visible Must NOT do: hide/replace pieces; render markers larger than 50% of square; animate Recommended Agent Profile: visual-engineering. Skills: [frontend-ui-ux]. Parallelization: Wave 9 References: existing chessboard component Acceptance Criteria: SSR snapshot of board with 3 markers shows all 3 + tooltips; pieces still visible QA Scenarios: .sisyphus/evidence/task-57-board-markers.txt Commit: YES — feat(ui): client marker rendering on chessboard
  • 58. Client request-choice modal

    What to do:

    • New component packages/chess/src/ui/RequestChoiceModal.tsx: receives a PendingChoice via props; renders kind-specific UI:
      • kind=piece: clickable piece list (filtered by prompt.filter)
      • kind=square: 8×8 grid (reuse T53 component)
      • kind=column/row: 8 buttons
      • kind=coin-flip: 2 buttons (heads/tails) + visualized flip animation OR just buttons with prior-result visible
      • kind=rps: 3 buttons (rock/paper/scissors)
    • On selection, dispatches submit-choice WS message + closes modal
    • Disabled while WS submission in flight; error state if server rejects
    • Listens to game state for pendingChoices change; auto-opens when player has a pending choice Must NOT do: allow non-targeted player to interact (modal hidden if forPlayer ≠ self); animate transitions beyond simple fade Recommended Agent Profile: visual-engineering. Skills: [frontend-ui-ux]. Parallelization: Wave 9 References: T44, T45, T47; existing modal patterns in chess UI Acceptance Criteria: SSR snapshot for each kind variant; aria-modal=true; ESC close handled; submit-choice WS message sent on selection (test with mock WS) QA Scenarios: .sisyphus/evidence/task-58-choice-modal.txt Commit: YES — feat(ui): request-choice modal

WAVE 10 — TEST DESCRIPTORS + E2E: Each parity test runs the descriptor in our engine, drives the same scenario in a ThressGame-equivalent reference (or hand-crafted oracle), asserts state-hash equality. Each task is one descriptor + one parity test, atomic commit.

  • 59. minefield descriptor + parity test

    What to do:

    • Create packages/chess/src/__fixtures__/thressgame-parity/minefield.descriptor.json: descriptor using on-rule-activated → random-pick(empty squares, count: 2) → spawn-marker(mine, lifetime: one-shot)
    • Create parity test packages/chess/e2e/thressgame-parity-minefield.spec.ts: load descriptor, simulate 5-10 moves where pieces step on mines, assert post-state matches a checked-in reference state hash Must NOT do: use Math.random in descriptor (must use random-pick + seeded RNG); skip mid-game state checks Recommended Agent Profile: unspecified-high Parallelization: Wave 10 (after T28, T37, T18) References: ThressGame ruleHooks.js minefield (lines 408-444 of source); T28, T37 Acceptance Criteria: bun x playwright test e2e/thressgame-parity-minefield.spec.ts passes (3+ scenarios); descriptor validates clean QA Scenarios:
    Scenario: Mine destroys piece, marker consumed
      Tool: Playwright
      Steps:
        1. Load minefield descriptor
        2. Apply moves stepping on mine
        3. Assert piece destroyed + marker removed via state hash
      Evidence: .sisyphus/evidence/task-59-minefield-parity.png
    

    Commit: YES — test(parity): minefield ThressGame rule

  • 60. mr_freeze descriptor + parity test (uses request-choice)

    What to do:

    • Descriptor: on-rule-activated → request-choice(kind: column, forPlayer: chooser, bind: $col, then: for-row(rows: [0..7], bind: $row, then: spawn-marker(frozen-square, square: ctx-build($col, $row), lifetime: { kind: moves, count: 9 }, owner: chooser-color)))
    • Parity test: descriptor activates, T48 auto-resolver picks first valid column, 8 frozen-square markers spawn, opponent moves blocked from frozen column for 9 moves, markers expire Must NOT do: hardcode chooser color; manually skip request-choice (use auto-resolver) Recommended Agent Profile: unspecified-high Parallelization: Wave 10 (after T47, T48, T49) References: ThressGame mr_freeze (~lines 1042-1073 of source); T0 paper exercise rule 5 Acceptance Criteria: e2e passes; column blocked correctly; markers all expire at move 9 QA Scenarios: .sisyphus/evidence/task-60-mr-freeze-parity.png Commit: YES — test(parity): mr_freeze ThressGame rule
  • 61. parry descriptor + parity test (request-choice + cancel-capture + RPS)

    What to do:

    • Descriptor: on-captured(target: self) → request-choice(kind: rps, forPlayer: both, bind: $rps, then: conditional(condition: rps-eval($rps, expected: defender), then: cancel-capture, else: noop))
    • Parity test: trigger 5 captures, verify RPS resolves correctly per T48 alternating policy, verify defender survives when policy says defender wins
    • Requires new conditional kind rps-eval (added by this task or T0 ADR captured separately) Must NOT do: use Math.random for RPS resolution (use seeded RNG); allow non-defender outcome to result in cancellation Recommended Agent Profile: unspecified-high Parallelization: Wave 10 (after T27, T47, T48) References: ThressGame parry (~lines 1592-1594 + RPS in packages/server/src/utils/rps); T0 paper exercise rule 1 Acceptance Criteria: e2e passes; defender survival rate matches expected (with seeded resolver) QA Scenarios: .sisyphus/evidence/task-61-parry-parity.png Commit: YES — test(parity): parry ThressGame rule
  • 62. all_on_red descriptor + parity test (with-probability)

    What to do:

    • Descriptor: on-turn-start(color: both) → with-probability(p: 0.5, then: seed-attribute(BlockAllExceptKing, true, lifetime: { kind: moves, count: 1 }))
    • Parity test: 20 turns, count how often only king moves are legal; verify ~50% with seeded RNG (deterministic — exact count expected) Must NOT do: assert exact 10/20 (allow ±2 for seed variance); use Math.random Recommended Agent Profile: unspecified-high Parallelization: Wave 10 (after T36) References: ThressGame all_on_red (~lines 996-1017); T0 paper exercise rule 2 Acceptance Criteria: e2e passes; deterministic distribution matches expected counts QA Scenarios: .sisyphus/evidence/task-62-all-on-red-parity.png Commit: YES — test(parity): all_on_red ThressGame rule
  • 63. religious_conversion descriptor + parity test

    What to do:

    • Descriptor: on-move(target: self) → conditional(condition: attr-eq(self, PieceType, bishop), then: for-each-adjacent(target: self, filter: { pieceType: pawn, relation: enemy }, bind: $pawn, then: set-piece-attr(target: $pawn, attr: Color, value: { ctx-attr: { entity: self, attr: Color } })))
    • Parity test: bishop moves; adjacent enemy pawns convert; non-bishop pieces don't trigger Must NOT do: convert non-pawn pieces; convert ally pawns Recommended Agent Profile: unspecified-high Parallelization: Wave 10 (after T26, T33) References: ThressGame religious_conversion (~lines 1185-1204); T0 paper exercise rule 3 Acceptance Criteria: e2e passes; only adjacent enemy pawns flip QA Scenarios: .sisyphus/evidence/task-63-religious-conv.png Commit: YES — test(parity): religious_conversion ThressGame rule
  • 64. ice_physics descriptor + parity test

    What to do:

    • Descriptor: on-rule-activated → for-each-piece(filter: { pieceType: [bishop, rook, queen] }, bind: $p, then: set-piece-attr(target: $p, attr: SlideMustBeMaxDistance, value: true, lifetime: permanent))
    • Parity test: bishop's legal moves all max-distance only; queen's all max-distance only; non-sliders unaffected; pawns unaffected
    • Verifies T40's move-gen integration with SlideMustBeMaxDistance flag Must NOT do: affect non-sliding pieces; remove the constraint after activation Recommended Agent Profile: unspecified-high Parallelization: Wave 10 (after T8, T26, T31) References: ThressGame ice_physics (~lines 1469-1497); T0 paper exercise rule 4 Acceptance Criteria: e2e passes; legal-moves count matches expected (sliding pieces have ~80% fewer legal moves) QA Scenarios: .sisyphus/evidence/task-64-ice-physics-parity.png Commit: YES — test(parity): ice_physics ThressGame rule
  • 65. kamikaze descriptor + parity test (with-probability + for-each-adjacent)

    What to do:

    • Descriptor: on-capture(target: self) → with-probability(p: 0.25, then: for-each-adjacent(target: self, filter: { excludeKing: true }, bind: $adj, then: destroy-piece(target: $adj)))
    • Parity test: 100 captures with seeded RNG; ~25% of captures trigger AOE; AOE never kills kings Must NOT do: kill kings via AOE; trigger on non-capture moves Recommended Agent Profile: unspecified-high Parallelization: Wave 10 (after T22, T33, T36) References: ThressGame kamikaze (~lines 1129-1148) Acceptance Criteria: e2e passes; AOE rate ~25% deterministic with seed; king never destroyed QA Scenarios: .sisyphus/evidence/task-65-kamikaze-parity.png Commit: YES — test(parity): kamikaze ThressGame rule
  • 66. mind_control descriptor + parity test (request-choice on both players)

    What to do:

    • Descriptor: on-rule-activated → request-choice(kind: piece, forPlayer: both, filter: { relation: enemy, excludeKing: true }, bind: $targets, then: for-each-piece(filter: $targets, bind: $piece, then: set-piece-attr(target: $piece, attr: Color, value: { ctx-attr: { entity: chooser, attr: Color } })))
    • Parity test: both players pick targets via T48 auto-resolver, both targets convert to chooser's color; kings excluded Must NOT do: allow king conversion; allow only-one-player resolution before both submit Recommended Agent Profile: unspecified-high Parallelization: Wave 10 (after T26, T31, T47, T48) References: ThressGame mind_control (~lines 706-740); requires both-players choice (most complex test) Acceptance Criteria: e2e passes; both pieces converted; LIFO stack resolution observed (player A's choice first, then player B's) QA Scenarios: .sisyphus/evidence/task-66-mind-control-parity.png Commit: YES — test(parity): mind_control ThressGame rule
  • 67. 6 template descriptors shipped in modifier library

    What to do:

    • Add 6 templates to packages/chess/src/modifiers/library.ts (or wherever templates are stored): simple-mine (1 mine spawns at center), vampire-on-capture (Hp+1 on capture; existing primitive used), frozen-column (player picks column, spawns 8 frozen-square markers), coin-flip-restriction (50% chance: only kings move next turn), religious-bishop (T63 packaged), no-mans-land (player picks column, blocked permanent)
    • Each template has name, description, narrative, descriptor JSON
    • Co-located test: each template loads + validates clean Must NOT do: include templates not in the locked list of 6; allow validation errors on any Recommended Agent Profile: writing. Skills: []. Parallelization: Wave 10 (after T59-T66 in case any descriptor pattern changes) References: existing template structure in packages/chess/src/modifiers/library.ts Acceptance Criteria: 6 templates load + validate; appear in modifier picker UI (manual via SSR snapshot of picker) QA Scenarios: .sisyphus/evidence/task-67-templates.txt Commit: YES — feat(chess): 6 ThressGame template descriptors
  • 68. Playwright: request-choice round-trip e2e (3 flows)

    What to do:

    • New file packages/chess/e2e/request-choice.spec.ts with 3 distinct e2e tests:
      1. Single player choice (mr_freeze): activate descriptor → modal appears → click column → game proceeds with frozen markers
      2. Both-player choice (mind_control): activate → both modals appear (one per player tab) → each clicks → game proceeds
      3. Nested choice (RPS over capture, parry rule): trigger capture → both RPS modals → choices resolve → conditional cancels capture if defender wins
    • Each test uses real WS connection (not mocked); 2 browser contexts (one per player) Must NOT do: skip the actual choice modal (must click in real UI); use Math.random Recommended Agent Profile: unspecified-high. Skills: [playwright]. Parallelization: Wave 10 (after T58, T48, T49) References: existing e2e patterns in packages/chess/e2e/ Acceptance Criteria: All 3 e2e tests pass; screenshots captured; 0 flake across 5 reruns QA Scenarios:
    Scenario: Both-player choice round-trip
      Tool: Playwright
      Steps:
        1. Open 2 browser contexts as player 1 + 2
        2. Player 1 activates mind_control descriptor
        3. Both contexts see request-choice modal
        4. Each clicks a piece
        5. Assert both pieces converted
      Evidence: .sisyphus/evidence/task-68-mind-control-e2e.png
    

    Commit: YES — test(e2e): request-choice round-trip flows

  • 69. Performance budget test (100 markers, p99 < 50ms)

    What to do:

    • New test packages/chess/src/__fixtures__/perf/markers-perf.test.ts: spawn 100 markers across the board, run 1000 moves with mixed marker triggers, measure per-move latency, assert p99 < 50ms
    • Document the budget in .sisyphus/notepads/thressgame-coverage/perf-budget.md
    • Run via bun test --bail Must NOT do: use --headless or platform-specific timing assertions Recommended Agent Profile: unspecified-high Parallelization: Wave 10 (after all primitives + triggers land) References: existing perf patterns from narrate.ts (notepad learnings.md:218-220) Acceptance Criteria: test passes; p99 < 50ms documented in perf-budget.md QA Scenarios: .sisyphus/evidence/task-69-perf.txt Commit: YES — test(perf): 100-marker move latency benchmark

Final Verification Wave (MANDATORY — after ALL implementation tasks)

4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.

Do NOT auto-proceed after verification. Wait for user's explicit approval before marking work complete.

  • F1. Plan Compliance Audit — oracle Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, run command). For each "Must NOT Have": grep codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan. Output: Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT

  • F2. Code Quality Review — unspecified-high Run bun run check (lint + typecheck + tests). Review all changed files for: as any/@ts-ignore, empty catches, console.log in prod, commented-out code, unused imports, AI slop (excessive comments, over-abstraction, generic names like data/result/item/temp). Output: Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT

  • F3. Real Manual QA — unspecified-high (+ playwright skill) Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration: 8 parity rules + 3 request-choice e2e flows. Test edge cases: empty state, invalid input, rapid actions, marker collisions, cascade limits. Save to .sisyphus/evidence/final-qa/. Output: Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT

  • F4. Scope Fidelity Check — deep For each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built, nothing beyond spec was built. Check "Must NOT do" compliance. Detect cross-task contamination. Flag unaccounted changes. Output: Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT


Commit Strategy

Atomic commits per primitive family / trigger / protocol message-pair / descriptor. No "WIP" commits. No multi-feature commits. Commits ordered strictly by dependency layer.

  • 0: docs(adr): thressgame-coverage architecture decisions — .sisyphus/notepads/thressgame-coverage/decisions.md, bun run check
  • 1: test(chess): backward-compat baseline fixture — fixtures + golden hashes
  • 2-5: per audit/harness — single commit each
  • 6-10: feat(chess): {family} infrastructure — one commit per data-structure family
  • 11-12: feat(chess): param walker binding resolution + scope stack
  • 13-14: feat(chess): validator extensions for v2 primitives
  • 15: feat(chess): deferred trigger queue + cascade depth guard
  • 16-20: per trigger — one commit each
  • 21-27: per imperative primitive — one commit each (test + impl + narrate + palette + schema)
  • 28-35: per marker / iteration primitive — one commit each
  • 36-42: per RNG / restriction / movement primitive
  • 43: feat(server): WS protocol v2 schema + version negotiation
  • 44-50: per protocol/state-machine concern
  • 51-58: per UI piece — one commit each
  • 59-66: per parity descriptor + test — one commit each
  • 67: feat(chess): 6 template descriptors
  • 68: test(e2e): request-choice round-trip flows
  • 69: test(perf): 100-marker benchmark
  • FINAL: no commit — verification only

Success Criteria

Verification Commands

bun run check                                         # 0 errors, ~2200 tests pass
bun x playwright test e2e/thressgame-parity.spec.ts   # 8 rules, 24+ scenarios
bun x playwright test e2e/request-choice.spec.ts      # 3 flows
bun test packages/chess/src/modifiers/primitives/     # ~250 new tests pass
bun test --golden-hash packages/chess/src/__fixtures__/determinism/  # N=100, byte-identical

Final Checklist

  • All 28 new primitives shipped with tests, narrate, palette, schema
  • All 4 new triggers wired into 14-stage dispatcher
  • Marker entity subsystem complete with priority-based collision
  • Seeded PRNG verified deterministic across N=100 replays
  • WS protocol v2 with version negotiation tested old↔new
  • Stack-based suspended execution serializes/restores cleanly
  • All 8 ThressGame parity tests green
  • All 6 template descriptors load + apply cleanly
  • 100-marker performance test under p99 50ms
  • All "Must NOT Have" guardrails verified absent (grep evidence)
  • Backward compat: 22 existing primitives + ~1957 existing tests unchanged