houserules/.sisyphus/plans/visual-modifier-builder.md
Joey Yakimowich-Payne 88581ff6a9
fix(thressgame-coverage): F1+F4 remediation (T58 RequestChoiceModal + coin-flip kind)
Final Verification Wave found two real blockers:
1. T58 RequestChoiceModal.tsx was marked complete but did NOT exist on disk.
2. request-choice locked 6-kind enum was shipped as 5 (missing 'coin-flip').

Remediation:
- Build RequestChoiceModal.tsx with role=dialog, aria-modal=true, ESC/backdrop close, kind-specific input UI for all 6 kinds (rps / coin-flip / piece / square / column / row); 4 tests
- Add 'coin-flip' to:
  - request-choice primitive paramsSchema enum
  - PendingChoice.kind union (schema.ts + util/pending-choices.ts)
  - WS protocol ChoiceKindSchema (server/protocol.ts)
  - choice-timeout.firstDefaultForKind (defaults to 'heads')
  - broadcast.isValidChoiceValue (accepts 'heads' | 'tails')
- AutoChoiceResolver: deterministic alternating heads/tails for coin-flip

T68 e2e tests remain .skip()'d pending UI integration (modal-into-GameView wiring + activate-descriptor UI) — a follow-up task. The sentinel test asserts the gap exists so when integration lands, skips lift in the documented order.

Tests: 2740 -> 2744 (+4). bun run check exit 0.
2026-04-26 14:09:28 -06:00

104 KiB
Raw Permalink Blame History

Visual Modifier Builder + DSL Trigger Expansion

TL;DR

Quick Summary: Add a hybrid "Visual" mode to the existing CustomModifierEditor (block/stack editor + live preview) and extend the T3 modifier DSL with 7 new trigger primitives (on-move, on-turn-end, on-promotion, on-check-received, on-check-delivered, on-moved-onto-square, on-captured) plus a primitive-context extension for target redirection.

Deliverables:

  • 7 new trigger primitives with full DSL pipeline (schema, validator, apply, evaluator, dispatch, consumer registration)
  • PrimitiveApplyContext extension: target redirection resolver (self/ally/enemy/square) + per-trigger event context
  • Block/stack visual builder (dnd-kit) with live preview (narrative + JSON + mini board diagram)
  • Mode toggle ("Form" ↔ "Visual") inside existing CustomModifierEditor.tsx
  • Extract ParamField from existing inspector (pure refactor)
  • New narrate.ts pure module
  • 2+ new recipes showcasing new triggers
  • Updated docs: RULES.md, PRESET-API.md, user-facing trigger semantics

Estimated Effort: XL (~30 atomic tasks across 4 waves) Parallel Execution: YES — 4 waves with 6-9 parallel tasks each Critical Path: Context extension (T1) → Attr/consumer/schema (T2–T4) → Primitives (T5–T11 parallel) → Dispatchers (T12) → UI wiring (T21–T25) → E2E + docs → Final Verification


Context

Original Request

"we have a chess game implemented with a rules engine. We have a custom modifier dsl. I want to make it easier to use. Right now, you have to type some json. Plan out how to implement some sort of visual builder for all primitives (as well as adding more stuff like 'on move')"

Interview Summary

Key Discussions:

  • Relationship to existing editor: Complement, not replace — add Form/Visual mode toggle inside CustomModifierEditor.tsx
  • Paradigm: Hybrid — block/stack editor + live preview (narrative + JSON + mini board diagram)
  • New triggers: 7 (on-move, on-turn-end, on-promotion, on-check-received, on-check-delivered, on-moved-onto-square, on-captured)
  • on-promotion: exposes both old + new PieceType → requires primitive context extension
  • on-captured: requires target redirection (self/ally/enemy/square) → all primitive contexts get a target resolver
  • Check semantics split into on-check-received (edge-triggered, attached to royal) + on-check-delivered (attached to attacker, discovered check = attacking piece fires)
  • on-moved-onto-square predicate: { squares: Square[] } OR { file?, rank? }; color dropped (ambiguous)
  • on-move fires on any Position change (captures too — both on-capture and on-move fire, dispatched in order)
  • DSL version: 1 kept — new kinds are additive; existing descriptors load unchanged
  • T1 ModifierProfileEditor out of scope
  • Multiplayer onShareWithRoom preserved (descriptor shape unchanged)
  • DnD: @dnd-kit/core + sortable + utilities (new deps)
  • Test strategy: TDD (RED-GREEN-REFACTOR) per primitive

Research Findings:

  • Existing 14 T3 primitives, 4 triggers: on-turn-start, on-capture, on-damaged, conditional — canonical pattern to mirror
  • MAX_RECURSION_DEPTH = 3 in validator; runtime walker caps at 8. Stays at 3.
  • MAX_PRIMITIVE_COUNT = 50 in 3 places (schema, validator, server wire) — cross-package parity test Q4.2 protects drift
  • Dispatch pattern: integration preset in modifiers/apply.ts, onAfterMove calls fire*Hooks() from triggers.ts
  • Attr consumer pattern: registerAttrConsumer(attrKey) in apply.ts; boot-time integrity check fails if any primitive-seeded attr lacks a consumer
  • Pre-move snapshots: PRE_MOVE_HP_SNAPSHOTS, PRE_MOVE_CAPTURE_ATTACKERS via per-engine WeakMaps set in onBeforeMove
  • Existing editor: packages/chess/src/ui/CustomModifierEditor.tsx (872 lines) — 3-col palette/tree/inspector, Zod-introspecting forms
  • Frontend: React 19, Tailwind 4, Vitest + happy-dom, Playwright e2e. No DnD library currently.
  • Storage: houserules:custom-modifiers:v1 localStorage (20 entries FIFO, starred exempt)
  • packages/rete = pure rules engine, NOT a visual node-graph editor

Metis Review

Identified Gaps (addressed):

  • on-promotion timing → user chose "both types in context" → drives context extension (T1)
  • on-check-received edge vs level → edge-triggered (prevents infinite sustain)
  • on-move firing rule → "any Position fact change" (clean single rule)
  • on-moved-onto-square predicate color ambiguity → dropped
  • on-captured self-only limitation → user opted for target redirection → drives context extension (T1)
  • Dispatch ordering → Metis-proposed order locked by explicit test
  • on-move + on-capture overlap → both fire
  • DnD-kit SSR/happy-dom/a11y concerns → addressed via DragOverlay, KeyboardSensor, test via Playwright (not happy-dom pointer sim)

Metis directives incorporated:

  • ParamField extraction is a PURE REFACTOR with golden snapshots (T14)
  • narrate.ts is a PURE module (no session/engine access) (T15)
  • Backward compat test with old-kinds-only fixture (T20)
  • Server wire schema parity test unxfailed after new kinds added (T13)
  • Recipes showcasing new triggers (T26)
  • Each primitive TDD: RED test → GREEN impl → commit
  • Atomic commit hygiene: no mixing refactor + feature

Work Objectives

Core Objective

Make the chess modifier DSL authorable without writing JSON by adding a visual block/stack editor with live preview, AND extend the DSL with 7 new trigger primitives plus a target-redirection mechanism so death-rattle and ally-buff patterns become expressible.

Concrete Deliverables

DSL Extensions (backend):

  • packages/chess/src/modifiers/primitives/context.ts — new TargetResolver type + resolveTargets() helper
  • 7 new primitive files in packages/chess/src/modifiers/primitives/:
    • on-move.ts + .test.ts
    • on-turn-end.ts + .test.ts
    • on-promotion.ts + .test.ts
    • on-check-received.ts + .test.ts
    • on-check-delivered.ts + .test.ts
    • on-moved-onto-square.ts + .test.ts
    • on-captured.ts + .test.ts
  • Updated primitives/types.ts — PrimitiveKind union extended; PrimitiveApplyContext gains target field + event field
  • Updated primitives/index.ts — side-effect imports for new primitives
  • Updated schema.ts — 7 new ChessAttrMap entries (OnMoveHooks, OnTurnEndHooks, OnPromotionHooks, OnCheckReceivedHooks, OnCheckDeliveredHooks, OnMovedOntoSquareHooks, OnCapturedHooks)
  • Updated modifiers/triggers.ts — 7 new fire*Hooks() evaluator functions
  • Updated modifiers/apply.ts — 7 new registerAttrConsumer() calls, dispatch ordering in onAfterMove, pre-move state snapshots for check/promotion diff
  • Updated modifiers/custom/recipes.ts — 2+ new recipes using new triggers
  • Updated server wire schema (packages/server/src/wire/custom-modifier.ts or equivalent) — mirror new kinds if server validates
  • Updated packages/chess/RULES.md — new trigger semantics section
  • Updated packages/chess/docs/PRESET-API.md — context target-redirection + event context docs

UI (frontend):

  • packages/chess/src/ui/ParamField.tsx — extracted pure-presentational component (from CustomModifierEditor inspector)
  • packages/chess/src/ui/narrate.ts + .test.ts — pure descriptor → English narrative module
  • packages/chess/src/ui/visual-builder/BlockCard.tsx + .test.tsx — single primitive block renderer
  • packages/chess/src/ui/visual-builder/BlockList.tsx + .test.tsx — dnd-kit SortableContext tree with nested trigger children
  • packages/chess/src/ui/visual-builder/VisualBuilderPane.tsx — composes BlockList + PreviewPane, replaces center+right when mode=visual
  • packages/chess/src/ui/visual-builder/preview/PreviewPane.tsx — tabs: Narrative / JSON / Board
  • packages/chess/src/ui/visual-builder/preview/NarrativeView.tsx
  • packages/chess/src/ui/visual-builder/preview/JsonView.tsx
  • packages/chess/src/ui/visual-builder/preview/BoardDiagramView.tsx — 8x8 SVG showing affected squares/auras
  • Updated packages/chess/src/ui/CustomModifierEditor.tsx — add Form/Visual mode toggle + persist choice via localStorage key houserules:custom-modifier-editor-mode:v1

Tests:

  • Unit tests co-located with each source file (Vitest + happy-dom)
  • E2E: packages/chess/e2e/custom-modifier-visual.spec.ts — Playwright flow
  • Integration: packages/chess/src/modifiers/apply.test.ts — dispatch order assertion for all 11 triggers
  • Backward-compat fixture: packages/chess/src/modifiers/custom/__fixtures__/legacy-descriptor.json
  • Server wire parity fixture for new kinds

Definition of Done

Each criterion is runnable by an agent (no "user confirms").

  • bun run check (from repo root) exits 0 — typecheck + lint + tests
  • bun test packages/chess — all 22 primitives registered (15 pre-existing + 7 new), all trigger tests pass
  • bun test packages/chess/src/modifiers/apply.test.ts — dispatch ordering test proves 12-stage order
  • bun test packages/chess/src/ui/narrate.test.ts — golden-output tests for all 21 kinds
  • bun test packages/chess/src/modifiers/custom/validate.test.ts — depth-3 accepted, depth-4 rejected, cycles rejected
  • cd packages/chess && bun x playwright test e2e/custom-modifier-visual.spec.ts — passes
  • Legacy descriptor fixture (15 old kinds only) parses + validates + applies identically (round-trip byte-equal)
  • Server wire schema includes 21 kinds; cross-package parity test green
  • Bundle size delta (chess package) ≤ 25kb gz after dnd-kit + new primitives (measure via bun run build)
  • Mode toggle round-trip: Form → Visual → Form yields JSON.stringify-equal descriptor
  • All 7 new hook attrs appear in getStaticPrimitiveSeedManifest() result
  • Every new hook attr has a registerAttrConsumer() call — assertSeedConsumerIntegrity() passes at boot
  • 2+ new recipes in custom/recipes.ts validate via validateCustomDescriptor → {ok: true}

Must Have

  • 7 new trigger primitives, fully wired through the DSL pipeline
  • PrimitiveApplyContext extension: target resolver + per-event event field
  • Visual block editor (dnd-kit based) with keyboard-accessible reorder
  • Live preview with 3 tabs: Narrative, JSON, Board diagram
  • Mode toggle in existing CustomModifierEditor.tsx
  • Backward compat: existing saved descriptors load + validate + apply unchanged
  • TDD per primitive; golden snapshots for narrate.ts; pure-refactor proof for ParamField
  • E2E Playwright coverage of visual-mode authoring flow

Must NOT Have (Guardrails)

Scope creep locks (explicit):

  • MUST NOT change CustomModifierDescriptor.version from 1
  • MUST NOT change uiForm: 'primitive-composer' or source: 'custom' literals
  • MUST NOT raise MAX_RECURSION_DEPTH from 3 or MAX_PRIMITIVE_COUNT from 50
  • MUST NOT touch packages/rete/** (pure engine, orthogonal)
  • MUST NOT touch ModifierProfileEditor beyond incidental (T1 modifiers out of scope)
  • MUST NOT change onShareWithRoom wire format
  • MUST NOT refactor existing triggers' behavior byte for byte — only add new ones
  • MUST NOT add runtime deps beyond @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities
  • MUST NOT introduce a state-management library (no Redux/Zustand/Jotai)
  • MUST NOT add CSS-in-JS — Tailwind classes only
  • MUST NOT add undo/redo inside visual builder (existing editor lacks it too)
  • MUST NOT add a "simulator" preview mode (narrate + JSON + static diagram only)
  • MUST NOT add copy-paste, multi-select, or bulk-delete in block list
  • MUST NOT add keyboard shortcuts beyond standard a11y (Space/Arrow for dnd-kit reorder)
  • MUST NOT add mobile-first responsive redesign (document as explicit limitation)
  • MUST NOT merge ParamField extraction and visual-mode wiring into one commit

AI slop patterns to avoid:

  • Generic variable names (data, result, temp, item)
  • Empty catch blocks — if something can fail, handle or propagate
  • as any or @ts-ignore — use structural casts with narrow types (existing pattern in CustomModifierEditor line 745)
  • Over-commented code — comments explain WHY, not WHAT
  • Adding JSDoc to trivial functions
  • Creating "utility" files with one-off helpers — inline if used once
  • Extracting abstractions that have only one consumer
  • console.log left in production code

Verification Strategy (MANDATORY)

ZERO HUMAN INTERVENTION — ALL verification is agent-executed. No exceptions.

Test Decision

  • Infrastructure exists: YES
  • Automated tests: YES (TDD per primitive + test-after for UI)
  • Framework: Vitest + happy-dom (unit), Playwright (e2e)
  • TDD discipline: Each of the 7 primitives: RED (failing test) → GREEN (minimal impl) → commit. Dispatchers and context extension also TDD.

QA Policy

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

  • Backend DSL: Use Bash (bun test) — run targeted tests, capture output with pass/fail counts
  • UI components: Use Bash (vitest) for unit tests + Playwright for e2e
  • Visual builder E2E: Use Playwright — open editor, toggle to Visual mode, drag blocks, save, reopen, assert state
  • Bundle size: Use Bash — bun run build then measure dist output bytes
  • Backward compat: Use Bash — load legacy fixture via Node, assert parse result

Execution Strategy

Parallel Execution Waves

Target: 5-8 tasks per wave. Wave 1 unblocks Wave 2. Wave 2 unblocks Wave 3. Wave 4 is verification.

Wave 1 (START — foundations, 6 parallel):
├── T1:  PrimitiveApplyContext extension (target + event fields) [deep]
├── T2:  ChessAttrMap 7 new hook attrs [quick]
├── T3:  registerAttrConsumer calls + manifest consumer integrity [quick]
├── T4:  Pre-move state snapshots (check lines, promotion flag) [unspecified-high]
├── T13: Server wire schema mirror + parity fixture [unspecified-high]
├── T14: ParamField extraction (pure refactor + golden snapshot) [artistry]
└── T15: narrate.ts pure module + unit tests [deep]

Wave 2 (AFTER Wave 1 — primitives + UI parts, 9 parallel):
├── T5:  on-move primitive + TDD tests [deep]
├── T6:  on-turn-end primitive + TDD tests [deep]
├── T7:  on-promotion primitive + TDD tests [deep]
├── T8:  on-check-received primitive + TDD tests [deep]
├── T9:  on-check-delivered primitive + TDD tests [deep]
├── T10: on-moved-onto-square primitive + TDD tests [deep]
├── T11: on-captured primitive + TDD tests [deep]
├── T16: BlockCard component + unit tests [visual-engineering]
└── T17: PreviewPane (Narrative/JSON/Board) + unit tests [visual-engineering]

Wave 3 (AFTER Wave 2 — integration + wiring, 6 parallel):
├── T12: 7 fire*Hooks evaluators in triggers.ts [deep]
├── T18: BlockList with dnd-kit SortableContext (nested) + tests [visual-engineering]
├── T19: VisualBuilderPane composition [visual-engineering]
├── T20: Backward-compat legacy descriptor fixture + round-trip test [unspecified-high]
├── T21: Dispatcher wiring in apply.ts onAfterMove + ordering test [deep]
└── T22: CustomModifierEditor mode toggle + localStorage persistence [visual-engineering]

Wave 4 (AFTER Wave 3 — polish, docs, e2e):
├── T23: Mode toggle round-trip test (Form↔Visual byte-equal) [quick]
├── T24: Depth-3 composition test (trigger-in-conditional-in-trigger) [quick]
├── T25: Playwright e2e visual authoring flow [unspecified-high]
├── T26: 2+ new recipes showcasing new triggers [quick]
├── T27: RULES.md + PRESET-API.md updates [writing]
└── T28: Bundle size verification + registry count assertion [quick]

Wave FINAL (AFTER ALL — 4 parallel reviews, then user okay):
├── F1: Plan compliance audit (oracle)
├── F2: Code quality review (unspecified-high)
├── F3: Real manual QA end-to-end (unspecified-high + playwright skill)
└── F4: Scope fidelity check (deep)
→ Present consolidated results → await explicit user okay

Critical Path: T1 → T2 → T3 → T5–T11 (parallel) → T12 → T21 → T25 → F1–F4
Parallel Speedup: ~65% faster than sequential
Max Concurrent: 9 (Wave 2)

Dependency Matrix

  • T1 (context ext): depends ∅ → unblocks T5–T11, T21
  • T2 (attrs): depends ∅ → unblocks T3, T5–T12
  • T3 (consumers): depends T2 → unblocks T5–T12 runtime
  • T4 (snapshots): depends ∅ → unblocks T7, T8, T9 (need pre-move check/promotion state)
  • T5 on-move: depends T1, T2, T3 → unblocks T12
  • T6 on-turn-end: depends T1, T2, T3 → unblocks T12
  • T7 on-promotion: depends T1, T2, T3, T4 → unblocks T12
  • T8 on-check-received: depends T1, T2, T3, T4 → unblocks T12
  • T9 on-check-delivered: depends T1, T2, T3, T4 → unblocks T12
  • T10 on-moved-onto-square: depends T1, T2, T3 → unblocks T12
  • T11 on-captured: depends T1, T2, T3 → unblocks T12
  • T12 evaluators: depends T5–T11 → unblocks T21
  • T13 server wire: depends ∅ → unblocks T20
  • T14 ParamField: depends ∅ → unblocks T16, T22
  • T15 narrate: depends ∅ → unblocks T17
  • T16 BlockCard: depends T14, T15 → unblocks T18
  • T17 PreviewPane: depends T15 → unblocks T19
  • T18 BlockList: depends T16 → unblocks T19
  • T19 VisualBuilderPane: depends T17, T18 → unblocks T22
  • T20 legacy compat: depends T13 → unblocks T25
  • T21 dispatcher wiring: depends T12 → unblocks T25
  • T22 mode toggle: depends T14, T19 → unblocks T23, T25
  • T23 round-trip test: depends T22 → unblocks F3
  • T24 depth composition: depends T21 → unblocks F1
  • T25 e2e: depends T20, T21, T22 → unblocks F3
  • T26 recipes: depends T21 → unblocks F1
  • T27 docs: depends T21 → unblocks F1
  • T28 bundle check: depends T22 → unblocks F2

Agent Dispatch Summary

  • Wave 1: 7 tasks — T1 → deep, T2 → quick, T3 → quick, T4 → unspecified-high, T13 → unspecified-high, T14 → artistry, T15 → deep
  • Wave 2: 9 tasks — T5–T11 → deep (7×), T16 → visual-engineering, T17 → visual-engineering
  • Wave 3: 6 tasks — T12 → deep, T18 → visual-engineering, T19 → visual-engineering, T20 → unspecified-high, T21 → deep, T22 → visual-engineering
  • Wave 4: 6 tasks — T23 → quick, T24 → quick, T25 → unspecified-high + playwright skill, T26 → quick, T27 → writing, T28 → quick
  • FINAL: 4 tasks — F1 → oracle, F2 → unspecified-high, F3 → unspecified-high + playwright skill, F4 → deep

TODOs

  • 1. packages/chess/src/modifiers/primitives/types.ts + context.ts: Extend PrimitiveApplyContext with target resolver and event field

    What to do:

    • Add new TargetResolver type: 'self' | 'attacker' | 'defender' | { squares: Square[] } | { relation: 'ally' | 'enemy', filter?: { pieceType?: PieceType } }
    • Extend PrimitiveApplyContext: add readonly target: TargetResolver (default 'self') and readonly event: PrimitiveEvent | undefined (carries trigger-specific metadata like { promotedFrom: PieceType, promotedTo: PieceType } for on-promotion)
    • Create new file packages/chess/src/modifiers/primitives/context.ts exporting resolveTargets(ctx, target): readonly EntityId[] — walks session to find matching pieces
    • Preserve ctx.pieceId as the CURRENT-apply target; resolveTargets() returns [ctx.pieceId] when target === 'self'
    • Update the 15 existing primitives that use ctx.pieceId to route through resolveTargets() when ctx.target !== 'self' — but default-case path is unchanged so existing behavior is byte-identical
    • Add unit tests: context.test.ts covers each TargetResolver variant against a fixture session

    Must NOT do:

    • Do NOT change the ctx.pieceId field — keep it as the "current iteration target"
    • Do NOT break the existing 14 primitives' behavior when target === 'self' (default) — byte-identical semantics
    • Do NOT add target resolution logic inside individual primitives — centralize in resolveTargets()
    • Do NOT change CustomModifierDescriptor schema (target is per-invocation context, not per-descriptor)

    Recommended Agent Profile:

    • Category: deep
      • Reason: Architectural change touching the primitive-apply contract; requires careful preservation of 15 existing primitives' behavior
    • Skills: []
      • Reason: No external doc lookup needed; pattern is codebase-internal
    • Skills Evaluated but Omitted:
      • context7: No external library involved
      • code-search: Pattern is unique to this codebase

    Parallelization:

    • Can Run In Parallel: YES
    • Parallel Group: Wave 1 (with T2, T3, T4, T13, T14, T15)
    • Blocks: T5, T6, T7, T8, T9, T10, T11, T21 (all need the new context shape)
    • Blocked By: None

    References:

    Pattern References (existing code to follow):

    • packages/chess/src/modifiers/primitives/types.ts:48-55 — current PrimitiveApplyContext interface (must preserve existing fields)
    • packages/chess/src/modifiers/primitives/on-capture.ts — canonical trigger primitive that uses ctx.pieceId
    • packages/chess/src/modifiers/custom/apply.ts — orchestrator that constructs the context; update here to populate new fields
    • packages/chess/src/modifiers/triggers.ts:149-180 — fireOnCaptureHooks — shows pattern of how trigger evaluators iterate pieces

    API/Type References:

    • packages/chess/src/schema.ts:Square — the Square algebraic string type
    • packages/chess/src/schema.ts:PieceType — enum for filter.pieceType
    • @paratype/rete:EntityId — re-exported via primitives/types.ts

    Test References:

    • packages/chess/src/modifiers/primitives/on-capture.test.ts — test structure for primitive behavior
    • packages/chess/src/modifiers/reconcile.test.ts — shows session-fixture pattern for multi-piece tests

    WHY Each Reference Matters:

    • types.ts:48-55: The shape being extended — MUST preserve all existing fields and their optionality/readonly markers
    • on-capture.ts: Template for how trigger primitives consume the context — extension must not break this shape
    • custom/apply.ts: The construction site for PrimitiveApplyContext — this is where defaults for target and event are injected

    Acceptance Criteria:

    • packages/chess/src/modifiers/primitives/context.ts exists and exports TargetResolver, PrimitiveEvent, resolveTargets
    • bun test packages/chess/src/modifiers/primitives/context.test.ts → PASS (≥6 scenarios: self, squares list, ally filter, enemy filter, attacker, defender)
    • bun test packages/chess/src/modifiers/primitives/on-capture.test.ts → PASS (existing test unchanged after type extension)
    • bun test packages/chess/src/modifiers/primitives/on-turn-start.test.ts → PASS (existing test unchanged)
    • bun tsc --noEmit → 0 errors
    • Running grep -r "target: 'self'" packages/chess/src/modifiers/primitives/ shows the default applied in each existing primitive's apply() OR in centralized apply orchestrator

    QA Scenarios:

    Scenario: Default target='self' preserves existing primitive behavior
      Tool: Bash (bun test)
      Preconditions: T1 implementation complete; existing primitive test suites unchanged
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/chess/src/modifiers/primitives
        2. Capture output showing all existing primitive tests pass
        3. Assert: test count ≥ current baseline; 0 failures
      Expected Result: All 15 existing primitive test files pass unchanged
      Failure Indicators: Any existing test breaks; means default target='self' path is not byte-equivalent
      Evidence: .sisyphus/evidence/task-1-existing-primitives-pass.txt
    
    Scenario: Target='ally' resolves to same-color pieces
      Tool: Bash (bun test specific)
      Preconditions: context.test.ts includes "ally" case
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/chess/src/modifiers/primitives/context.test.ts -t "ally"
        2. Assert: PASS; resolveTargets returns all pieces with matching Color attribute excluding self
      Expected Result: Test passes; resolveTargets returns [...ally pieceIds] excluding ctx.pieceId
      Evidence: .sisyphus/evidence/task-1-ally-resolver.txt
    
    Scenario: Target='attacker' requires event context, throws without it
      Tool: Bash (bun test specific)
      Preconditions: context.test.ts includes "attacker-requires-event" case
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/chess/src/modifiers/primitives/context.test.ts -t "attacker"
        2. Assert: resolveTargets with target='attacker' and event=undefined throws explicit error
      Expected Result: Clear error "target='attacker' requires event with attackerId"
      Evidence: .sisyphus/evidence/task-1-attacker-requires-event.txt
    

    Evidence to Capture:

    • task-1-existing-primitives-pass.txt — full bun test output
    • task-1-ally-resolver.txt — specific test output
    • task-1-attacker-requires-event.txt — specific test output

    Commit: YES

    • Message: feat(chess/modifiers): extend PrimitiveApplyContext with target resolver + event field
    • Files: packages/chess/src/modifiers/primitives/{types.ts,context.ts,context.test.ts} + updates to custom/apply.ts
    • Pre-commit: bun run check
  • 2. packages/chess/src/schema.ts: Add 7 new hook attrs to ChessAttrMap

    What to do:

    • Add to ChessAttrMap interface:
      • OnMoveHooks: readonly EffectPrimitiveNode[][]
      • OnTurnEndHooks: readonly EffectPrimitiveNode[][]
      • OnPromotionHooks: readonly EffectPrimitiveNode[][]
      • OnCheckReceivedHooks: readonly EffectPrimitiveNode[][]
      • OnCheckDeliveredHooks: readonly EffectPrimitiveNode[][]
      • OnMovedOntoSquareHooks: readonly OnMovedOntoSquareHookEntry[] where entry = { filter: SquareFilter, primitives: EffectPrimitiveNode[] }
      • OnCapturedHooks: readonly EffectPrimitiveNode[][]
    • Define SquareFilter type: { kind: 'squares', squares: readonly Square[] } | { kind: 'predicate', file?: 'a'|...|'h', rank?: 1|...|8 }
    • Update ChessAttrKey union (auto-derived from ChessAttrMap) — no manual work if union uses keyof

    Must NOT do:

    • Do NOT alter existing ChessAttrMap entries — pure additive change
    • Do NOT add any attrs beyond these 7 + SquareFilter type
    • Do NOT add optional markers — all hook attrs are readonly arrays (empty array is the absent state)

    Recommended Agent Profile:

    • Category: quick
      • Reason: Single-file additive type change; tiny scope
    • Skills: []

    Parallelization:

    • Can Run In Parallel: YES
    • Parallel Group: Wave 1
    • Blocks: T3 (consumer registration needs the attr names), T5–T12
    • Blocked By: None

    References:

    • packages/chess/src/schema.ts — existing ChessAttrMap — add new entries following the pattern of OnTurnStartHooks, OnCaptureHooks, OnDamagedHooks, ConditionalHooks
    • packages/chess/src/modifiers/primitives/types.ts:EffectPrimitiveNode — the array element type

    WHY Each Reference Matters:

    • schema.ts: Single source of truth for attr types — additions here drive type safety in apply/triggers/validate
    • EffectPrimitiveNode: each hook attr's array-of-arrays element type is this structure

    Acceptance Criteria:

    • bun tsc --noEmit → 0 errors
    • grep -c "Hooks:" packages/chess/src/schema.ts → ≥ 11 (4 existing + 7 new)
    • Running bun test packages/chess/src/schema.test.ts → PASS

    QA Scenarios:

    Scenario: Type-check passes with new attrs added
      Tool: Bash (bun tsc)
      Preconditions: T2 implementation complete
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun tsc --noEmit -p packages/chess/tsconfig.json 2>&1 | tee .sisyphus/evidence/task-2-tsc.txt
        2. Assert: Output shows 0 errors
      Expected Result: TypeScript compiles with zero errors
      Evidence: .sisyphus/evidence/task-2-tsc.txt
    
    Scenario: Attr count proves additive extension
      Tool: Bash
      Steps:
        1. Run: grep -c "Hooks:" /home/joey/Projects/rules/packages/chess/src/schema.ts
        2. Assert: Result ≥ 11
      Expected Result: 11 or more Hook attrs in ChessAttrMap
      Evidence: .sisyphus/evidence/task-2-attr-count.txt
    

    Evidence to Capture:

    • task-2-tsc.txt
    • task-2-attr-count.txt

    Commit: YES

    • Message: feat(chess/schema): add 7 new hook attrs to ChessAttrMap
    • Files: packages/chess/src/schema.ts
    • Pre-commit: bun run check
  • 3. packages/chess/src/modifiers/apply.ts: Register 7 new attr consumers

    What to do:

    • Add 7 registerAttrConsumer() calls for the new hook attrs, grouped near existing consumer calls
    • Each call: registerAttrConsumer("OnMoveHooks"), etc.
    • Place at module-load time so boot integrity check sees them before assertSeedConsumerIntegrity() fires
    • Add inline comment explaining which subsystem reads each attr (the evaluator in triggers.ts)

    Must NOT do:

    • Do NOT add consumers for attrs that don't exist in ChessAttrMap — T2 must land first
    • Do NOT register twice — idempotent API but confusing signal

    Recommended Agent Profile:

    • Category: quick
    • Skills: []

    Parallelization:

    • Can Run In Parallel: YES (Wave 1, after T2)
    • Blocks: T5–T12 (primitives will write these attrs; integrity check runs at engine boot during primitive tests)
    • Blocked By: T2

    References:

    • packages/chess/src/modifiers/apply.ts:87-98 — existing registerAttrConsumer calls — add new ones in the same style
    • packages/chess/src/modifiers/primitives/manifest.ts:122-129 — registerAttrConsumer + getRegisteredConsumers + assertSeedConsumerIntegrity

    WHY Each Reference Matters:

    • apply.ts:87-98: The exact pattern and location — new entries follow this idiom
    • manifest.ts:122-129: Shows the contract — every attr any primitive seeds MUST have ≥1 registered consumer or boot fails loudly

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/manifest.test.ts → PASS
    • bun test packages/chess/src/modifiers/primitives/consumer-integration.test.ts → PASS
    • grep "registerAttrConsumer" packages/chess/src/modifiers/apply.ts | wc -l → ≥ 11 (existing 4 + 7 new minimum)

    QA Scenarios:

    Scenario: Consumer integrity check passes with new attrs
      Tool: Bash (bun test)
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/chess/src/modifiers/primitives/consumer-integration.test.ts 2>&1 | tee .sisyphus/evidence/task-3-consumer-integrity.txt
        2. Assert: Output shows "PASS"; no unsatisfied consumer assertion errors
      Expected Result: Integrity check passes; all 7 new attrs have a registered consumer
      Evidence: .sisyphus/evidence/task-3-consumer-integrity.txt
    
    Scenario: Consumer count proves 7 new registrations
      Tool: Bash
      Steps:
        1. Run: grep "registerAttrConsumer" /home/joey/Projects/rules/packages/chess/src/modifiers/apply.ts | wc -l
        2. Assert: Result ≥ 11
      Evidence: .sisyphus/evidence/task-3-consumer-count.txt
    

    Evidence to Capture:

    • task-3-consumer-integrity.txt
    • task-3-consumer-count.txt

    Commit: YES

    • Message: feat(chess/modifiers): register 7 new attr consumers
    • Files: packages/chess/src/modifiers/apply.ts
    • Pre-commit: bun run check
  • 4. packages/chess/src/modifiers/apply.ts: Pre-move state snapshots for check + promotion diff

    What to do:

    • Add per-engine WeakMaps (like existing PRE_MOVE_HP_SNAPSHOTS, PRE_MOVE_CAPTURE_ATTACKERS):
      • PRE_MOVE_CHECK_STATE_SNAPSHOTS: WeakMap<ChessEngine, { white: CheckLines, black: CheckLines }> — captures which piece(s) attack which royal(s) BEFORE the move
      • PRE_MOVE_PROMOTION_FLAGS: WeakMap<ChessEngine, Set<EntityId>> — IDs of pawns eligible to promote this move
    • Populate in onBeforeMove (integration preset)
    • Expose read-only getters: getPreMoveCheckState(engine), getPreMovePromotionFlags(engine)
    • Clear on onAfterMove end (after all triggers fire) to avoid stale state

    Must NOT do:

    • Do NOT leak snapshots across moves — ALWAYS clear at end of onAfterMove
    • Do NOT compute check lines inline — use engine's existing check detection helpers
    • Do NOT modify existing PRE_MOVE_HP_SNAPSHOTS / PRE_MOVE_CAPTURE_ATTACKERS

    Recommended Agent Profile:

    • Category: unspecified-high
      • Reason: Requires understanding engine's check-detection internals + careful lifecycle management
    • Skills: []

    Parallelization:

    • Can Run In Parallel: YES (Wave 1)
    • Blocks: T7 (on-promotion), T8 (on-check-received), T9 (on-check-delivered)
    • Blocked By: None

    References:

    • packages/chess/src/modifiers/apply.ts — search for PRE_MOVE_HP_SNAPSHOTS and PRE_MOVE_CAPTURE_ATTACKERS — mirror the WeakMap + lifecycle pattern
    • packages/chess/src/engine.ts:1350-1376 — promotion resolution in applyMove; opponentInCheck computation line 1371
    • packages/chess/src/rules/ — check-detection helpers

    WHY Each Reference Matters:

    • PRE_MOVE_HP_SNAPSHOTS pattern is the canonical lifecycle for pre-move state; copy structure exactly
    • engine.ts:1350-1376 shows exactly where promotion happens and where to snapshot pre-move state

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/apply.test.ts -t "pre-move-check-snapshot" → PASS
    • bun test packages/chess/src/modifiers/apply.test.ts -t "pre-move-promotion-flag" → PASS
    • After 10 sequential moves, PRE_MOVE_CHECK_STATE_SNAPSHOTS.get(engine) returns clean state (no leak)

    QA Scenarios:

    Scenario: Pre-move check state captured before king move
      Tool: Bash (bun test)
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/chess/src/modifiers/apply.test.ts -t "pre-move-check" 2>&1 | tee .sisyphus/evidence/task-4-check-snapshot.txt
        2. Assert: Test sets up position where white king is in check, plays a move resolving check, snapshot shows attacker IDs BEFORE move
      Expected Result: Snapshot captured correctly; post-move check state differs
      Evidence: .sisyphus/evidence/task-4-check-snapshot.txt
    
    Scenario: Snapshot cleared after onAfterMove
      Tool: Bash (bun test)
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/chess/src/modifiers/apply.test.ts -t "snapshot-cleared" 2>&1 | tee .sisyphus/evidence/task-4-snapshot-cleanup.txt
        2. Assert: After move resolves, `PRE_MOVE_CHECK_STATE_SNAPSHOTS.has(engine) === false`
      Expected Result: Per-move snapshots don't leak across moves
      Evidence: .sisyphus/evidence/task-4-snapshot-cleanup.txt
    

    Evidence to Capture:

    • task-4-check-snapshot.txt
    • task-4-snapshot-cleanup.txt

    Commit: YES

    • Message: feat(chess/modifiers): add pre-move check/promotion state snapshots
    • Files: packages/chess/src/modifiers/apply.ts + apply.test.ts
    • Pre-commit: bun run check
  • 5. packages/chess/src/modifiers/primitives/on-move.ts + .test.ts: Add on-move primitive (TDD)

    What to do:

    • RED first: author on-move.test.ts with 4 scenarios: (a) fires when piece moves to new square, (b) fires on captures too, (c) fires on castling rook, (d) does NOT fire when piece is static
    • GREEN: implement on-move.ts mirroring on-capture.ts structure — paramsSchema { primitives: z.array(EffectPrimitiveNodeSchema).max(10) }, apply() writes OnMoveHooks array to ctx.pieceId, childPrimitives(params) => params.primitives, seedsAttrs: ['OnMoveHooks'], maxDepth: 3, longDescription + ≥2 examples
    • Register in packages/chess/src/modifiers/primitives/index.ts (side-effect import)
    • Extend PrimitiveKind union in types.ts

    Must NOT do: change existing primitives; add new schema fields beyond primitives[]; skip docs metadata

    Recommended Agent Profile: deep — mirrors existing trigger pattern precisely; skills: []

    Parallelization: Wave 2, parallel with T6–T11, T16, T17. Blocks T12, T21. Blocked by T1, T2, T3.

    References:

    • packages/chess/src/modifiers/primitives/on-capture.ts — canonical pattern to mirror structure exactly
    • packages/chess/src/modifiers/primitives/on-capture.test.ts — test shape + fixture setup
    • packages/chess/src/modifiers/primitives/registry.ts — where the new primitive self-registers
    • packages/chess/src/modifiers/primitives/index.ts — barrel import (add import './on-move.js';)

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/on-move.test.ts → PASS (≥4 scenarios)
    • bun test packages/chess/src/modifiers/primitives/registry.test.ts shows 22 primitives registered (15 prev + 7 new, counted at end)
    • bun test packages/chess/src/modifiers/primitives/docs.test.ts → PASS (on-move has longDescription + ≥1 example)
    • Imports side-effect-register at module load (verify by asserting PRIMITIVE_REGISTRY.get('on-move') returns non-undefined)

    QA Scenarios:

    Scenario: Primitive registered and retrievable
      Tool: Bash (bun test)
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/chess/src/modifiers/primitives/on-move.test.ts 2>&1 | tee .sisyphus/evidence/task-5-on-move-tests.txt
        2. Assert: All 4 scenarios PASS
      Expected: Test suite reports 4/4 pass, 0 fail
      Evidence: .sisyphus/evidence/task-5-on-move-tests.txt
    
    Scenario: on-move does NOT fire on static position
      Tool: Bash (bun test specific)
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/chess/src/modifiers/primitives/on-move.test.ts -t "static"
        2. Assert: Test passes; no hook dispatched when piece unchanged
      Expected: Test confirms firing rule = "Position fact changed"
      Evidence: .sisyphus/evidence/task-5-on-move-static.txt
    

    Evidence: task-5-on-move-tests.txt, task-5-on-move-static.txt

    Commit: YES. Message: feat(chess/primitives): add on-move primitive. Files: on-move.ts, on-move.test.ts, index.ts, types.ts. Pre-commit: bun run check

  • 6. packages/chess/src/modifiers/primitives/on-turn-end.ts + .test.ts: Add on-turn-end primitive (TDD)

    What to do:

    • RED first: on-turn-end.test.ts with 3 scenarios: (a) fires at end of mover's turn, (b) does NOT fire at end of opponent's turn for this piece, (c) fires BEFORE next color's on-turn-start (order assertion)
    • GREEN: mirror on-turn-start.ts. paramsSchema = { color: 'white' | 'black' | 'both', primitives: [...] }. Write OnTurnEndHooks attr. seedsAttrs, longDescription, examples.
    • Register in index.ts + types.ts union.

    Must NOT do: merge with on-turn-start (separate attr + separate dispatcher); fire on non-matching color turns

    Recommended Agent Profile: deep. Skills: []

    Parallelization: Wave 2, parallel with T5, T7–T11, T16, T17. Blocks T12, T21. Blocked by T1, T2, T3.

    References:

    • packages/chess/src/modifiers/primitives/on-turn-start.ts — mirror entirely (differs only in attr name + semantics)
    • packages/chess/src/modifiers/triggers.ts:fireOnTurnStartHooks — shows matching-color filter pattern

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/on-turn-end.test.ts → PASS (≥3 scenarios)
    • PRIMITIVE_REGISTRY.get('on-turn-end') returns non-undefined

    QA Scenarios:

    Scenario: Fires at mover's turn end, not opponent's
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-turn-end.test.ts 2>&1 | tee .sisyphus/evidence/task-6-on-turn-end.txt
        2. Assert: Scenario "fires on matching color" passes; scenario "skips non-matching color" passes
      Expected: 3/3 pass
      Evidence: .sisyphus/evidence/task-6-on-turn-end.txt
    
    Scenario: Order — on-turn-end(W) before on-turn-start(B)
      Tool: Bash (bun test with order assertion)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-turn-end.test.ts -t "before on-turn-start"
        2. Assert: Test uses ordered event log; on-turn-end entry precedes on-turn-start entry
      Expected: Order lock confirmed
      Evidence: .sisyphus/evidence/task-6-on-turn-end-order.txt
    

    Evidence: task-6-on-turn-end.txt, task-6-on-turn-end-order.txt

    Commit: YES. Message: feat(chess/primitives): add on-turn-end primitive. Files: on-turn-end.{ts,test.ts}, index.ts, types.ts. Pre-commit: bun run check

  • 7. packages/chess/src/modifiers/primitives/on-promotion.ts + .test.ts: Add on-promotion primitive with old+new PieceType event (TDD)

    What to do:

    • RED first: test that (a) fires when pawn promotes to queen — event carries {promotedFrom: 'pawn', promotedTo: 'queen'}, (b) nested primitive add-to-attribute Hp +1 runs with new PieceType visible via ctx.event, (c) does NOT fire when promotion is disabled by override-promotion with disabled target, (d) fires AFTER PieceType fact flip
    • GREEN: paramsSchema { primitives: [...] }. apply() writes OnPromotionHooks. Uses PrimitiveEvent from T1's context.ts carrying {promotedFrom, promotedTo}.
    • Nested primitives receive ctx.event = { promotedFrom: PieceType, promotedTo: PieceType }

    Must NOT do: fire on non-promotion moves; fire when override-promotion disabled; expose old type AFTER fact flip (it's preserved only in event)

    Recommended Agent Profile: deep. Skills: []

    Parallelization: Wave 2. Blocks T12, T21. Blocked by T1, T2, T3, T4.

    References:

    • packages/chess/src/modifiers/primitives/on-capture.ts — structural mirror
    • packages/chess/src/engine.ts:1350-1376 — promotion resolution in applyMove (event capture point)
    • packages/chess/src/modifiers/primitives/override-promotion.ts — check if PromotionOverride === 'disabled' to suppress firing
    • T1 primitives/context.ts — PrimitiveEvent type carries promotedFrom + promotedTo

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/on-promotion.test.ts → PASS (≥4 scenarios)
    • event.promotedFrom is always 'pawn'; event.promotedTo is the new type
    • Does NOT fire when PromotionOverride = 'disabled'

    QA Scenarios:

    Scenario: Promotion event carries both types
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-promotion.test.ts -t "event carries types" 2>&1 | tee .sisyphus/evidence/task-7-on-promotion-event.txt
        2. Assert: event.promotedFrom === 'pawn', event.promotedTo === 'queen'
      Expected: Event context populated correctly
      Evidence: .sisyphus/evidence/task-7-on-promotion-event.txt
    
    Scenario: Does not fire when promotion disabled
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-promotion.test.ts -t "disabled" 2>&1 | tee .sisyphus/evidence/task-7-on-promotion-disabled.txt
        2. Assert: Hook dispatched 0 times when override-promotion:disabled is active
      Expected: No false firing
      Evidence: .sisyphus/evidence/task-7-on-promotion-disabled.txt
    

    Evidence: task-7-on-promotion-event.txt, task-7-on-promotion-disabled.txt

    Commit: YES. Message: feat(chess/primitives): add on-promotion primitive with old+new type event. Files: on-promotion.{ts,test.ts}, index.ts, types.ts. Pre-commit: bun run check

  • 8. packages/chess/src/modifiers/primitives/on-check-received.ts + .test.ts: Edge-triggered check-received (TDD)

    What to do:

    • RED first: (a) fires ONCE when royal transitions from not-in-check → in-check, (b) does NOT fire again on subsequent turn if still in check (edge-only), (c) fires again after exiting + re-entering check, (d) only fires on royal pieces (king, or royal knight with knightmate preset)
    • GREEN: paramsSchema { primitives: [...] }. apply() writes OnCheckReceivedHooks. Evaluator (T12) uses T4's PRE_MOVE_CHECK_STATE_SNAPSHOTS to diff pre vs post — fires only on not-in-check→in-check transition.

    Must NOT do: fire on non-royal pieces; fire level-triggered (every turn in check); fire when piece was already in check

    Recommended Agent Profile: deep. Skills: []

    Parallelization: Wave 2. Blocks T12, T21. Blocked by T1, T2, T3, T4.

    References:

    • packages/chess/src/modifiers/primitives/on-capture.ts — structural mirror
    • T4 PRE_MOVE_CHECK_STATE_SNAPSHOTS for edge detection
    • packages/chess/src/presets/active-set.ts — getRoyalPieces hook API; piece must be royal to receive hook

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/on-check-received.test.ts → PASS (≥4 scenarios)
    • Edge-only firing confirmed via multi-turn test

    QA Scenarios:

    Scenario: Edge-only firing over 3 turns
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-check-received.test.ts -t "edge" 2>&1 | tee .sisyphus/evidence/task-8-edge-firing.txt
        2. Assert: Hook fires exactly 1 time across 3 turns where king stays in check
      Expected: 1 firing, not 3
      Evidence: .sisyphus/evidence/task-8-edge-firing.txt
    
    Scenario: Non-royal pieces do not receive hook
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-check-received.test.ts -t "non-royal"
        2. Assert: Rook with on-check-received hook, attacked rook, hook NOT dispatched
      Expected: Hook ignored on non-royal
      Evidence: .sisyphus/evidence/task-8-non-royal.txt
    

    Evidence: task-8-edge-firing.txt, task-8-non-royal.txt

    Commit: YES. Message: feat(chess/primitives): add on-check-received primitive (edge-triggered). Files: on-check-received.{ts,test.ts}, index.ts, types.ts. Pre-commit: bun run check

  • 9. packages/chess/src/modifiers/primitives/on-check-delivered.ts + .test.ts: Attacker-attached check-delivered with discovered check handling (TDD)

    What to do:

    • RED first: (a) fires on direct check — attacking piece moves and attacks royal, (b) fires on discovered check — piece B (not the mover) newly attacks royal after piece A moves, hook on B fires (NOT on A), (c) does NOT fire if piece already attacked royal pre-move (edge-triggered), (d) fires on double check on both attackers
    • GREEN: paramsSchema { primitives: [...] }. apply() writes OnCheckDeliveredHooks. Evaluator iterates all pieces post-move, checks if piece attacks royal in post-state but NOT in T4's pre-move snapshot.

    Must NOT do: attribute discovered check to the mover; fire on pieces that already attacked pre-move; fire on own royal

    Recommended Agent Profile: deep. Skills: []

    Parallelization: Wave 2. Blocks T12, T21. Blocked by T1, T2, T3, T4.

    References:

    • packages/chess/src/modifiers/primitives/on-capture.ts — structural mirror
    • T4 PRE_MOVE_CHECK_STATE_SNAPSHOTS for diff detection
    • packages/chess/src/rules/ — attack-ray detection helpers

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/on-check-delivered.test.ts → PASS (≥4 scenarios)
    • Discovered-check case: hook on B (not A)

    QA Scenarios:

    Scenario: Discovered check attributes correctly
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-check-delivered.test.ts -t "discovered" 2>&1 | tee .sisyphus/evidence/task-9-discovered-check.txt
        2. Setup: Bishop C behind pawn P. P moves, revealing C's check on king. Hook on C fires, hook on P does NOT.
        3. Assert: Dispatch log shows C's hook fired; P's hook not fired
      Expected: Discovered check correctly attributes to revealing attacker
      Evidence: .sisyphus/evidence/task-9-discovered-check.txt
    
    Scenario: Double check fires both attackers
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-check-delivered.test.ts -t "double"
        2. Assert: Both pieces newly attacking royal receive hook
      Expected: 2 hooks fired on double check
      Evidence: .sisyphus/evidence/task-9-double-check.txt
    

    Evidence: task-9-discovered-check.txt, task-9-double-check.txt

    Commit: YES. Message: feat(chess/primitives): add on-check-delivered primitive. Files: on-check-delivered.{ts,test.ts}, index.ts, types.ts. Pre-commit: bun run check

  • 10. packages/chess/src/modifiers/primitives/on-moved-onto-square.ts + .test.ts: Discriminated union predicate (TDD)

    What to do:

    • RED first: (a) squares-list variant fires when piece ends on d4 from squares: ['d4','d5'], (b) predicate variant fires when piece ends on file='d' regardless of rank, (c) predicate variant fires when rank=8 regardless of file, (d) does NOT fire when move does not change piece's square
    • GREEN: paramsSchema — Zod discriminated union: z.discriminatedUnion('kind', [z.object({kind: z.literal('squares'), squares: z.array(SquareSchema)}), z.object({kind: z.literal('predicate'), file: FileSchema.optional(), rank: RankSchema.optional(), primitives: [...]})]) — OR simpler: z.union([ {squares, primitives}, {file?, rank?, primitives} ]) using filter kind inferred from fields present
    • Write OnMovedOntoSquareHooks attr entries = { filter: SquareFilter, primitives: [...] }

    Must NOT do: add a color field to predicate; allow empty predicate (file AND rank both absent) — must have at least one constraint

    Recommended Agent Profile: deep. Skills: []

    Parallelization: Wave 2. Blocks T12, T21. Blocked by T1, T2, T3.

    References:

    • packages/chess/src/modifiers/primitives/on-capture.ts — structural mirror
    • packages/chess/src/schema.ts — Square type, SquareFilter type added in T2

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/on-moved-onto-square.test.ts → PASS (≥4 scenarios)
    • Schema rejects empty predicate {} (must have file, rank, or squares)

    QA Scenarios:

    Scenario: Squares list variant matches explicit squares
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-moved-onto-square.test.ts -t "squares list" 2>&1 | tee .sisyphus/evidence/task-10-squares.txt
        2. Assert: Piece moves to d4 with filter {squares:['d4','d5','e4','e5']} → hook fires; moves to a1 → hook does not fire
      Expected: Square filter precise match
      Evidence: .sisyphus/evidence/task-10-squares.txt
    
    Scenario: Predicate rejects empty constraint
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-moved-onto-square.test.ts -t "reject empty predicate"
        2. Assert: params `{kind:'predicate', primitives:[]}` fails Zod validation
      Expected: Validator error surfaces "at least one of file, rank, or squares required"
      Evidence: .sisyphus/evidence/task-10-empty-predicate.txt
    

    Evidence: task-10-squares.txt, task-10-empty-predicate.txt

    Commit: YES. Message: feat(chess/primitives): add on-moved-onto-square primitive. Files: on-moved-onto-square.{ts,test.ts}, index.ts, types.ts. Pre-commit: bun run check

  • 11. packages/chess/src/modifiers/primitives/on-captured.ts + .test.ts: Death-rattle with target redirection (TDD)

    What to do:

    • RED first: (a) fires when piece takes lethal damage (HP → 0 OR captured in non-HP rules), (b) fires BEFORE retract so nested primitives can still access defender attrs, (c) with target: {relation: 'ally'} nested add-to-attribute Hp +1 buffs all friendly pieces, (d) with target: 'attacker' nested add-to-attribute Hp -1 damages the attacker, (e) does NOT fire on non-lethal damage (HP still > 0)
    • GREEN: paramsSchema { target?: TargetResolver, primitives: [...] } — target is per-hook override, defaults to self. apply() writes OnCapturedHooks with target embedded. Evaluator (T12) fires hook with ctx.target = hook.target, ctx.event = { attackerId, defenderId }.

    Must NOT do: fire AFTER piece retraction (facts already gone); fire on non-lethal damage; allow unrestricted access to opponent royal without target filter

    Recommended Agent Profile: deep. Skills: []

    Parallelization: Wave 2. Blocks T12, T21. Blocked by T1, T2, T3.

    References:

    • packages/chess/src/modifiers/primitives/on-damaged.ts — similar structure; on-damaged fires on any damage, on-captured only on lethal
    • T1 TargetResolver + resolveTargets()
    • packages/chess/src/modifiers/primitives/manifest.ts:getEngineSeedManifest — zombie cleanup happens AFTER hook fires (confirm ordering)

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/primitives/on-captured.test.ts → PASS (≥5 scenarios)
    • target: 'attacker' correctly damages attacker via nested primitive
    • target: {relation: 'ally'} buffs all allies (not self, not enemies)
    • Hook fires BEFORE zombie-fact cleanup (assertable via attr access in nested primitive)

    QA Scenarios:

    Scenario: Death-rattle damages attacker
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-captured.test.ts -t "target attacker" 2>&1 | tee .sisyphus/evidence/task-11-attacker.txt
        2. Setup: Defender D with on-captured {target:'attacker', primitives: [add-to-attribute Hp -1]}. Attacker A captures D lethally.
        3. Assert: A's Hp reduced by 1 after move
      Expected: Death-rattle pattern works
      Evidence: .sisyphus/evidence/task-11-attacker.txt
    
    Scenario: Buffs allies (not self, not enemies)
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/primitives/on-captured.test.ts -t "target ally"
        2. Setup: Defender D with on-captured {target:{relation:'ally'}, primitives:[add-to-attribute Hp +1]}. Multiple allies + enemies on board.
        3. Assert: All ally HP +1; D's HP unchanged (already dead); enemy HP unchanged
      Expected: Ally-buff semantics precise
      Evidence: .sisyphus/evidence/task-11-ally.txt
    

    Evidence: task-11-attacker.txt, task-11-ally.txt

    Commit: YES. Message: feat(chess/primitives): add on-captured primitive with target redirection. Files: on-captured.{ts,test.ts}, index.ts, types.ts. Pre-commit: bun run check

  • 12. packages/chess/src/modifiers/triggers.ts: Add 7 new fire*Hooks evaluator functions

    What to do:

    • Add evaluators mirroring fireOnCaptureHooks / fireOnTurnStartHooks pattern:
      • fireOnMoveHooks(engine, movedPieceIds: readonly EntityId[]) — iterates hooks, runs nested primitives
      • fireOnTurnEndHooks(engine, endedColor) — color-filtered (matches hook's color: 'white'|'black'|'both')
      • fireOnPromotionHooks(engine, promotedPieceId, promotedFrom: PieceType, promotedTo: PieceType) — populates ctx.event
      • fireOnCheckReceivedHooks(engine, royalId) — edge-filtered via T4 snapshot diff
      • fireOnCheckDeliveredHooks(engine, newlyAttackingPieceIds) — edge-filtered
      • fireOnMovedOntoSquareHooks(engine, movedPieceId, destSquare) — filter matches SquareFilter
      • fireOnCapturedHooks(engine, capturedPieceId, attackerId) — fires BEFORE retraction, resolves hook.target

    Must NOT do: modify existing 4 fire*Hooks functions; use any state not sourced from engine + T4 snapshots; mutate session outside of nested primitive apply calls

    Recommended Agent Profile: deep. Skills: []

    Parallelization: Wave 3. Blocks T21. Blocked by T5–T11.

    References:

    • packages/chess/src/modifiers/triggers.ts:fireOnCaptureHooks (lines 149-180) — canonical pattern
    • packages/chess/src/modifiers/primitives/context.ts (T1) — for resolveTargets() call
    • T4 PRE_MOVE_CHECK_STATE_SNAPSHOTS for edge-triggered evaluators

    Acceptance Criteria:

    • grep -c "^export function fire.*Hooks" packages/chess/src/modifiers/triggers.ts → ≥ 11 (4 existing + 7 new)
    • bun test packages/chess/src/modifiers/triggers.test.ts → PASS with ≥7 new scenarios covering each evaluator

    QA Scenarios:

    Scenario: All 11 evaluators exported
      Tool: Bash
      Steps:
        1. Run: grep -c "^export function fire.*Hooks" /home/joey/Projects/rules/packages/chess/src/modifiers/triggers.ts
        2. Assert: ≥ 11
      Evidence: .sisyphus/evidence/task-12-evaluator-count.txt
    
    Scenario: Edge-triggered check evaluator uses snapshot diff
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/triggers.test.ts -t "check-received edge" 2>&1 | tee .sisyphus/evidence/task-12-check-edge.txt
        2. Assert: Scenario with king in check across 2 turns → evaluator dispatched 1x total
      Evidence: .sisyphus/evidence/task-12-check-edge.txt
    

    Evidence: task-12-evaluator-count.txt, task-12-check-edge.txt

    Commit: YES. Message: feat(chess/modifiers): add 7 fire*Hooks evaluators in triggers.ts. Files: triggers.ts, triggers.test.ts. Pre-commit: bun run check

  • 13. packages/server/src/wire/custom-modifier.ts (or equivalent): Mirror 7 new primitive kinds in server wire schema + parity fixture

    What to do:

    • Locate server wire schema for CustomModifierDescriptor validation (likely in packages/server/src/wire/ or packages/server/src/routes/)
    • Mirror new PrimitiveKind additions (OR confirm schema uses z.string() structural match + runs against PRIMITIVE_REGISTRY via shared import — in which case no wire change needed, just docs)
    • Update cross-package parity fixture: add descriptor JSON using each new primitive, verify it round-trips through both client + server schemas

    Must NOT do: diverge client and server validation; change descriptor shape; skip parity test (Q4.2 cross-package test)

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

    Parallelization: Wave 1, parallel with others. Blocks T20. Blocked by: none (can define fixtures independently).

    References:

    • packages/server/PROTOCOL.md — wire format docs
    • packages/server/src/ — locate CustomModifierDescriptor validator (grep for primitives + custom schema)
    • packages/chess/src/modifiers/custom/schema.ts — client schema (source of truth for parity)

    Acceptance Criteria:

    • Cross-package parity test PASS with descriptor using each new kind
    • bun test packages/server → PASS
    • Server rejects descriptors violating MAX_PRIMITIVE_COUNT = 50 (unchanged)

    QA Scenarios:

    Scenario: Server accepts all 7 new primitive kinds in wire schema
      Tool: Bash (bun test)
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/server 2>&1 | tee .sisyphus/evidence/task-13-server-wire.txt
        2. Assert: Descriptor fixtures using each new kind parse successfully via server wire schema
      Expected: All new kinds accepted
      Evidence: .sisyphus/evidence/task-13-server-wire.txt
    
    Scenario: Cross-package parity holds
      Tool: Bash (bun test parity test if exists, else write one)
      Steps:
        1. Run: bun test --grep "parity" 2>&1 | tee .sisyphus/evidence/task-13-parity.txt
        2. Assert: No drift between client + server schemas
      Expected: Parity green
      Evidence: .sisyphus/evidence/task-13-parity.txt
    

    Evidence: task-13-server-wire.txt, task-13-parity.txt

    Commit: YES. Message: feat(chess/server): mirror 7 new primitive kinds in wire schema + parity fixtures. Files: packages/server/**, parity test file. Pre-commit: bun run check

  • 14. packages/chess/src/ui/ParamField.tsx + .test.tsx: Extract PrimitiveInspector param-rendering logic from CustomModifierEditor.tsx (PURE REFACTOR)

    What to do:

    • BEFORE touching code: capture golden snapshot of current inspector output for every kind's paramsSchema. Write PrimitiveInspector.snapshot.test.tsx that renders current component with 14 fixture descriptors, snapshots DOM.
    • Extract lines 625-872 of CustomModifierEditor.tsx into new ParamField.tsx preserving Zod-introspection patterns verbatim
    • ParamField props: { node: EffectPrimitiveNode, primitive: EffectPrimitive | undefined, allPrimitives: readonly EffectPrimitiveNode[], onChange: (params: unknown) => void }
    • Re-import into CustomModifierEditor.tsx, replace inline component — ZERO behavior change
    • Snapshot test MUST match byte-for-byte after extraction

    Must NOT do: change any field rendering behavior; rename any test IDs; change props shape; add features

    Recommended Agent Profile: artistry. Skills: []

    • Reason: Pure refactor with snapshot-preserving discipline; unconventional in that zero new features are acceptable — must resist any "improvement"

    Parallelization: Wave 1. Blocks T16, T22. Blocked by: none.

    References:

    • packages/chess/src/ui/CustomModifierEditor.tsx:625-872 — the PrimitiveInspector function to extract
    • packages/chess/src/ui/AttrCombobox.tsx — internal dep kept by import
    • packages/chess/src/ui/attr-suggestions.ts — type helpers used by inspector
    • packages/chess/e2e/custom-modifiers.spec.ts — e2e tests exercising inspector — must all pass unchanged

    Acceptance Criteria:

    • bun test packages/chess/src/ui/ParamField.snapshot.test.tsx → PASS
    • Playwright: bun x playwright test packages/chess/e2e/custom-modifiers.spec.ts → PASS unchanged (prove no regression)
    • git diff --stat shows only file moves + 1-2 import line changes in CustomModifierEditor.tsx
    • Line count of extracted file matches original (within ±10 lines for import cleanup)

    QA Scenarios:

    Scenario: Golden snapshots match before and after extraction
      Tool: Bash (bun test with snapshot update)
      Steps:
        1. Before extraction: run snapshot test, commit golden files
        2. After extraction: re-run snapshot test without --update
        3. Assert: 0 snapshot diffs
      Expected: Byte-identical DOM output
      Evidence: .sisyphus/evidence/task-14-snapshot-match.txt
    
    Scenario: E2E tests pass after extraction
      Tool: Bash (playwright)
      Steps:
        1. Run: cd /home/joey/Projects/rules/packages/chess && bun x playwright test e2e/custom-modifiers.spec.ts 2>&1 | tee .sisyphus/evidence/task-14-e2e.txt
        2. Assert: 100% pass rate
      Expected: No e2e regressions
      Evidence: .sisyphus/evidence/task-14-e2e.txt
    

    Evidence: task-14-snapshot-match.txt, task-14-e2e.txt

    Commit: YES. Message: refactor(chess/ui): extract ParamField from CustomModifierEditor (no behavior change). Files: ParamField.tsx, ParamField.snapshot.test.tsx, CustomModifierEditor.tsx (imports only). Pre-commit: bun run check

  • 15. packages/chess/src/ui/narrate.ts + .test.ts: Pure descriptor → English narrative module

    What to do:

    • Pure module. Zero session/engine access. Input: CustomModifierDescriptor or readonly EffectPrimitiveNode[]. Output: string (English narrative)
    • narrate(descriptor): string — walks tree, composes sentences: "When this piece captures, gain 1 HP. At turn start (matching 'white'), if HP < 3, heal 1."
    • Per-primitive narrator functions: narrateSeed(params), narrateAddTo(params), etc. — one per kind (21 total after new primitives)
    • Cycle guard: use visited-Set to prevent infinite loop on in-edit malformed trees
    • Length cap: if result > 4000 chars, truncate with "... and N more primitives"
    • Golden test file narrate.test.ts — table-driven, one scenario per primitive kind + 3 nested-combination scenarios

    Must NOT do: import engine/session; call PRIMITIVE_REGISTRY.get (use static kind→narrator map); add i18n; use template literals with escape-heavy HTML (plain English only)

    Recommended Agent Profile: deep. Skills: []

    Parallelization: Wave 1. Blocks T17 (PreviewPane reads narrate output). Blocked by: none.

    References:

    • packages/chess/src/modifiers/primitives/types.ts:PrimitiveKind — kinds to cover
    • packages/chess/src/modifiers/primitives/manifest.ts:collectDynamicSeedsInTree — tree-walk pattern with visited guard

    Acceptance Criteria:

    • bun test packages/chess/src/ui/narrate.test.ts → PASS (≥24 scenarios: 21 kinds + 3 nested)
    • narrate() runs < 1ms on 50-node descriptor (microbenchmark test)
    • Cyclic reference input returns bounded output without hanging

    QA Scenarios:

    Scenario: Golden output per primitive kind
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/ui/narrate.test.ts 2>&1 | tee .sisyphus/evidence/task-15-golden.txt
        2. Assert: All 24+ snapshot cases pass
      Expected: Deterministic narration per kind
      Evidence: .sisyphus/evidence/task-15-golden.txt
    
    Scenario: 50-node perf
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/ui/narrate.test.ts -t "perf"
        2. Assert: 50-node narrate completes under 1ms (averaged over 100 runs)
      Expected: Live-preview friendly perf
      Evidence: .sisyphus/evidence/task-15-perf.txt
    

    Evidence: task-15-golden.txt, task-15-perf.txt

    Commit: YES. Message: feat(chess/ui): add narrate.ts pure module for descriptor → English. Files: narrate.ts, narrate.test.ts. Pre-commit: bun run check

  • 16. packages/chess/src/ui/visual-builder/BlockCard.tsx + .test.tsx: Single primitive block renderer

    What to do:

    • Presentational component rendering ONE primitive node as a colored block card with: kind label, compact param summary (first 60 chars of JSON), remove button, expand/collapse for nested triggers (on-* kinds with child primitives)
    • Color-code by category: State (blue), Mechanic (emerald), Advanced/Trigger (violet)
    • Accessibility: role="article", aria-label="<kind> block", keyboard focusable, Enter to expand
    • Props: { node: EffectPrimitiveNode, index: number, isSelected: boolean, onSelect: () => void, onRemove: () => void, depth: number } — NO dnd-kit yet (pure rendering)
    • Uses ParamField from T14 in the inspector-overlay (only when expanded/selected)
    • Nested triggers render child BlockCards recursively with depth + 1 for indentation

    Must NOT do: handle DnD logic here (T18); mutate descriptor directly (call onChange); exceed depth-3 indentation visually (clamp style)

    Recommended Agent Profile: visual-engineering. Skills: [interface-design, frontend-ui-ux]

    • interface-design: direct domain match for authoring UI affordances
    • frontend-ui-ux: color system + a11y patterns

    Parallelization: Wave 2. Blocks T18. Blocked by T14, T15.

    References:

    • packages/chess/src/ui/CustomModifierEditor.tsx:339-388 — existing tree-node rendering pattern (color-coded rows)
    • packages/chess/src/ui/ParamField.tsx (from T14) — for selected state
    • Tailwind color palette: blue-50/blue-500 (State), emerald-50/emerald-500 (Mechanic), violet-50/violet-500 (Trigger)

    Acceptance Criteria:

    • bun test packages/chess/src/ui/visual-builder/BlockCard.test.tsx → PASS (≥5 scenarios: render, select, remove, expand nested, depth-clamp at 3)
    • Keyboard-focusable via Tab; Enter activates expand
    • No DnD code imported

    QA Scenarios:

    Scenario: Render + select + remove flow
      Tool: Bash (vitest)
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun test packages/chess/src/ui/visual-builder/BlockCard.test.tsx 2>&1 | tee .sisyphus/evidence/task-16-blockcard.txt
        2. Assert: All scenarios pass
      Evidence: .sisyphus/evidence/task-16-blockcard.txt
    
    Scenario: Nested trigger renders child blocks
      Tool: Bash (vitest)
      Steps:
        1. Run: bun test packages/chess/src/ui/visual-builder/BlockCard.test.tsx -t "nested"
        2. Assert: on-capture with child add-to-attribute renders both blocks; child at depth+1
      Evidence: .sisyphus/evidence/task-16-nested.txt
    

    Evidence: task-16-blockcard.txt, task-16-nested.txt

    Commit: YES. Message: feat(chess/ui): add BlockCard component. Files: visual-builder/BlockCard.tsx, .test.tsx. Pre-commit: bun run check

  • 17. packages/chess/src/ui/visual-builder/preview/: PreviewPane + 3 sub-views (NarrativeView, JsonView, BoardDiagramView)

    What to do:

    • PreviewPane.tsx: tab UI (3 tabs: Narrative / JSON / Board). Props { descriptor: CustomModifierDescriptor }.
    • NarrativeView.tsx: calls narrate(descriptor) from T15, renders in a scrollable <pre> with semantic role="region" aria-label="narrative".
    • JsonView.tsx: syntax-highlighted JSON (use minimal custom highlighter or accept plain <pre>). Collapsible by top-level keys. Copy-to-clipboard button.
    • BoardDiagramView.tsx: 8×8 SVG chessboard (200×200). Highlights:
      • Squares from on-moved-onto-square filters (yellow overlay)
      • Aura radius (concentric ring)
      • Poisoned/affected squares (from existing integration)
      • If descriptor has no positional primitives, render "No board effect" placeholder
    • All 3 views memoized on descriptor identity.

    Must NOT do: run engine simulation; show dynamic game state; add editor-level controls here (preview-only); use react-syntax-highlighter (size bloat — roll simple regex highlighter or pre-format)

    Recommended Agent Profile: visual-engineering. Skills: [interface-design, frontend-ui-ux]

    Parallelization: Wave 2. Blocks T19. Blocked by T15.

    References:

    • packages/chess/src/ui/Board.tsx — existing 8×8 SVG board for coordinate math reference
    • packages/chess/src/coord.ts — square-to-coords helpers
    • packages/chess/src/ui/narrate.ts (from T15)

    Acceptance Criteria:

    • bun test packages/chess/src/ui/visual-builder/preview/PreviewPane.test.tsx → PASS (tabs switch, each view mounts)
    • bun test packages/chess/src/ui/visual-builder/preview/BoardDiagramView.test.tsx → PASS (poisoned squares highlighted; aura renders ring; empty case shows placeholder)
    • Bundle size delta for preview subtree < 8kb gz (check via bun run build)

    QA Scenarios:

    Scenario: Tab switching preserves descriptor
      Tool: Bash (vitest)
      Steps:
        1. Run: bun test packages/chess/src/ui/visual-builder/preview 2>&1 | tee .sisyphus/evidence/task-17-preview.txt
        2. Assert: All tab tests PASS; descriptor not mutated across tab switches
      Evidence: .sisyphus/evidence/task-17-preview.txt
    
    Scenario: Board diagram renders aura
      Tool: Bash (vitest)
      Steps:
        1. Run: bun test packages/chess/src/ui/visual-builder/preview/BoardDiagramView.test.tsx -t "aura"
        2. Assert: Descriptor with add-aura{radius:2} renders concentric ring in SVG
      Evidence: .sisyphus/evidence/task-17-aura.txt
    

    Evidence: task-17-preview.txt, task-17-aura.txt

    Commit: YES. Message: feat(chess/ui): add PreviewPane (Narrative/JSON/Board tabs). Files: visual-builder/preview/*.tsx, tests. Pre-commit: bun run check

  • 18. packages/chess/src/ui/visual-builder/BlockList.tsx + .test.tsx: dnd-kit SortableContext tree with nested triggers

    What to do:

    • BlockList.tsx: top-level SortableContext wraps root-level blocks. Each trigger block that has child primitives wraps its children in a nested SortableContext.
    • Sensors: PointerSensor + KeyboardSensor with sortableKeyboardCoordinates.
    • DragOverlay component for drag preview (prevents layout jank during drag).
    • onDragEnd: calls onReorder(activeId, overId, isNested: boolean) — parent component updates descriptor tree structurally.
    • Announcements: custom announcements prop surfacing "Moved block from position X to Y" for screen readers.
    • Nested drop: dropping a block inside a trigger's children SortableContext moves it INTO that trigger's primitives[].

    Must NOT do: allow drag across different descriptor levels that would exceed depth 3 (block with validation fallback); enable multi-select; persist drag state to localStorage

    Recommended Agent Profile: visual-engineering. Skills: [interface-design, context7]

    • context7: dnd-kit has specific v6 API patterns; official docs via Context7 to avoid guesses

    Parallelization: Wave 3. Blocks T19. Blocked by T16.

    References:

    • packages/chess/src/ui/LayoutEditor.tsx — existing HTML5 drag pattern (orthogonal reference for behavior parity)
    • @dnd-kit/sortable docs — SortableContext, arrayMove, rectSortingStrategy
    • packages/chess/src/ui/visual-builder/BlockCard.tsx (T16) — integrates via useSortable

    Acceptance Criteria:

    • bun test packages/chess/src/ui/visual-builder/BlockList.test.tsx → PASS (render, prop-driven reorder via direct callback; SKIP PointerEvent tests in happy-dom, cover via e2e T25)
    • Keyboard reorder test: simulate Space → ArrowDown → Space, assert descriptor reorders (use KeyboardSensor explicit invocation)
    • Depth-4 drop is rejected with error toast/no-op

    QA Scenarios:

    Scenario: Keyboard reorder swaps blocks
      Tool: Bash (vitest)
      Steps:
        1. Run: bun test packages/chess/src/ui/visual-builder/BlockList.test.tsx -t "keyboard" 2>&1 | tee .sisyphus/evidence/task-18-keyboard.txt
        2. Assert: After Space+ArrowDown+Space, descriptor primitives[0] and primitives[1] swapped
      Evidence: .sisyphus/evidence/task-18-keyboard.txt
    
    Scenario: Depth-4 drop rejected
      Tool: Bash (vitest)
      Steps:
        1. Run: bun test packages/chess/src/ui/visual-builder/BlockList.test.tsx -t "depth limit"
        2. Assert: Drop attempt into 4th-level children → no mutation; validation toast triggered (mock)
      Evidence: .sisyphus/evidence/task-18-depth-limit.txt
    

    Evidence: task-18-keyboard.txt, task-18-depth-limit.txt

    Commit: YES. Message: feat(chess/ui): add BlockList with dnd-kit sortable (nested). Files: BlockList.tsx, .test.tsx. Pre-commit: bun run check

  • 19. packages/chess/src/ui/visual-builder/VisualBuilderPane.tsx: Composition shell — BlockList + Palette + PreviewPane

    What to do:

    • Layout: left sidebar (palette from existing editor, reused verbatim), center (BlockList), right (PreviewPane). Replaces CustomModifierEditor's center+right panels when mode=visual.
    • Props: { descriptor, onChange, validationResult }.
    • Handles tree mutations: add (from palette), remove, reorder (via BlockList callback).
    • Shows invalid-descriptor fallback: if validationResult.ok === false AND descriptor has cyclic or malformed structure, render a banner "Fix errors in Form mode to use Visual mode" with button to switch back.

    Must NOT do: reimplement palette (import from CustomModifierEditor or extract earlier); duplicate save/load UI (that stays in CustomModifierEditor header); introduce new state beyond descriptor sync

    Recommended Agent Profile: visual-engineering. Skills: [interface-design]

    Parallelization: Wave 3. Blocks T22. Blocked by T17, T18.

    References:

    • packages/chess/src/ui/CustomModifierEditor.tsx:272-326 — palette component (extract or reuse)
    • packages/chess/src/ui/visual-builder/BlockList.tsx (T18)
    • packages/chess/src/ui/visual-builder/preview/PreviewPane.tsx (T17)

    Acceptance Criteria:

    • bun test packages/chess/src/ui/visual-builder/VisualBuilderPane.test.tsx → PASS (≥4 scenarios: render, add from palette, remove, invalid-fallback)
    • Fallback banner renders when descriptor has Zod-invalid shape

    QA Scenarios:

    Scenario: Palette → BlockList integration
      Tool: Bash (vitest)
      Steps:
        1. Run: bun test packages/chess/src/ui/visual-builder/VisualBuilderPane.test.tsx 2>&1 | tee .sisyphus/evidence/task-19-pane.txt
        2. Assert: Clicking a palette item adds to descriptor.primitives; block appears in list
      Evidence: .sisyphus/evidence/task-19-pane.txt
    
    Scenario: Invalid descriptor fallback
      Tool: Bash (vitest)
      Steps:
        1. Run: bun test packages/chess/src/ui/visual-builder/VisualBuilderPane.test.tsx -t "invalid fallback"
        2. Assert: descriptor with depth-4 structure → fallback banner + "switch to Form" button
      Evidence: .sisyphus/evidence/task-19-fallback.txt
    

    Evidence: task-19-pane.txt, task-19-fallback.txt

    Commit: YES. Message: feat(chess/ui): add VisualBuilderPane composing blocks + preview. Files: VisualBuilderPane.tsx, .test.tsx. Pre-commit: bun run check

  • 20. packages/chess/src/modifiers/custom/__fixtures__/legacy-descriptor.json + round-trip test

    What to do:

    • Create fixture JSON containing a descriptor using ONLY the 15 pre-existing primitive kinds (no new ones from T5–T11)
    • Write test legacy-descriptor.test.ts: (a) parses via parseCustomModifierDescriptor without error, (b) validates via validateCustomDescriptor → {ok: true}, (c) applies via applyCustomDescriptor — resulting session WMEs match an expected snapshot, (d) serializes back to JSON — deep-equal to input
    • Fixture must exercise at least 5 different kinds including 2 nested triggers

    Must NOT do: introduce any new kinds in this fixture (proves backward compat); update fixture when adding new primitives

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

    Parallelization: Wave 3. Blocks T25. Blocked by T13.

    References:

    • packages/chess/src/modifiers/custom/recipes.ts — source of legacy-only descriptor examples
    • packages/chess/src/modifiers/custom/schema.ts:parseCustomModifierDescriptor
    • packages/chess/src/modifiers/custom/validate.ts
    • packages/chess/src/modifiers/custom/apply.ts:applyCustomDescriptor

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/custom/legacy-descriptor.test.ts → PASS (4 scenarios: parse, validate, apply, serialize)
    • JSON deep-equal round-trip

    QA Scenarios:

    Scenario: Legacy descriptor full round-trip
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/custom/legacy-descriptor.test.ts 2>&1 | tee .sisyphus/evidence/task-20-legacy.txt
        2. Assert: All 4 scenarios pass; JSON.stringify(parsed) === original JSON
      Expected: Byte-equal round-trip
      Evidence: .sisyphus/evidence/task-20-legacy.txt
    
    Scenario: Apply produces expected WMEs
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/custom/legacy-descriptor.test.ts -t "apply"
        2. Assert: Session WMEs after apply match snapshot of legacy-baseline WMEs
      Evidence: .sisyphus/evidence/task-20-apply.txt
    

    Evidence: task-20-legacy.txt, task-20-apply.txt

    Commit: YES. Message: test(chess): add legacy descriptor backward-compat fixture + round-trip test. Files: __fixtures__/legacy-descriptor.json, legacy-descriptor.test.ts. Pre-commit: bun run check

  • 21. packages/chess/src/modifiers/apply.ts: Wire 7 new trigger dispatchers into onAfterMove with strict ordering + ordering test

    What to do:

    • In onAfterMove integration preset hook, add calls in Metis-locked order:
      1. computeAuraFacts (existing)
      2. fireOnDamagedHooks (existing)
      3. fireOnCaptureHooks (existing)
      4. fireOnCapturedHooks(engine, capturedIds, attackerId) — BEFORE retraction (T11)
      5. fireOnPromotionHooks(engine, ...) — uses T4 flag
      6. fireOnMoveHooks(engine, movedPieceIds) — via Position diff
      7. fireOnMovedOntoSquareHooks(engine, movedPieceId, destSquare)
      8. fireOnCheckReceivedHooks(engine, royalId) — T4 snapshot diff
      9. fireOnCheckDeliveredHooks(engine, newlyAttackingIds) — T4 snapshot diff
      10. fireConditionalHooks (existing)
      11. fireOnTurnEndHooks(engine, endedColor) — for mover
      12. fireOnTurnStartHooks(engine, newColor) (existing) — for next color
    • Clear T4 snapshots at end of onAfterMove
    • Write ORDERING TEST: apply.test.ts registers a dispatch-order-spy descriptor on a test piece with all 11 trigger types, runs 1 move that triggers all, asserts dispatch order matches locked sequence

    Must NOT do: change existing dispatcher internals; introduce try/catch that swallows errors; skip snapshot cleanup

    Recommended Agent Profile: deep. Skills: []

    Parallelization: Wave 3. Blocks T25. Blocked by T12.

    References:

    • packages/chess/src/modifiers/apply.ts:665-683 — existing onAfterMove dispatch order
    • T4 PRE_MOVE_CHECK_STATE_SNAPSHOTS cleanup point

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/apply.test.ts -t "dispatch order" → PASS with exact order assertion
    • After move resolves, no pre-move snapshots remain (WeakMap.has() false for test engine)
    • All 21 primitive tests + 11 dispatcher tests still PASS

    QA Scenarios:

    Scenario: Dispatch order locked
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/apply.test.ts -t "dispatch order" 2>&1 | tee .sisyphus/evidence/task-21-order.txt
        2. Assert: Event log shows exact 12-step order from Metis spec
      Expected: Byte-locked order
      Evidence: .sisyphus/evidence/task-21-order.txt
    
    Scenario: Full integration test — descriptor with all trigger types runs end-to-end
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/apply.test.ts -t "all triggers"
        2. Assert: Descriptor with on-move + on-capture + on-captured + on-check-received + on-turn-end runs without error across a 3-move sequence
      Evidence: .sisyphus/evidence/task-21-all-triggers.txt
    

    Evidence: task-21-order.txt, task-21-all-triggers.txt

    Commit: YES. Message: feat(chess/modifiers): wire 7 new triggers in apply.ts onAfterMove + dispatch order test. Files: apply.ts, apply.test.ts. Pre-commit: bun run check

  • 22. packages/chess/src/ui/CustomModifierEditor.tsx: Add Form/Visual mode toggle + localStorage persistence

    What to do:

    • Add mode: 'form' | 'visual' state in CustomModifierEditor. Default: read from localStorage key houserules:custom-modifier-editor-mode:v1, fallback 'form'.
    • Add toggle buttons in header area next to Templates/Load/Save buttons: [Form] [Visual] with visual mode indicator (aria-pressed).
    • When mode === 'form': render existing center (tree view) + right (inspector) — unchanged.
    • When mode === 'visual': render <VisualBuilderPane descriptor={descriptor} onChange={setDescriptor} validationResult={validationResult} />, which replaces center+right.
    • Persist mode change to localStorage on toggle.
    • Add data-testid: custom-modifier-editor-mode-toggle, custom-modifier-editor-mode-form, custom-modifier-editor-mode-visual.

    Must NOT do: touch existing form-mode rendering logic beyond adding the conditional branch; change save/load/share handlers; add undo/redo

    Recommended Agent Profile: visual-engineering. Skills: [interface-design]

    Parallelization: Wave 3. Blocks T23, T25. Blocked by T14, T19.

    References:

    • packages/chess/src/ui/CustomModifierEditor.tsx:119-267 — header structure to extend
    • packages/chess/src/ui/visual-builder/VisualBuilderPane.tsx (T19)
    • packages/chess/src/persist/autosave.ts — localStorage key-versioning pattern

    Acceptance Criteria:

    • bun test packages/chess/src/ui/CustomModifierEditor.test.tsx → PASS with new mode-toggle scenarios
    • Playwright custom-modifiers.spec.ts still PASS (no regressions)
    • localStorage houserules:custom-modifier-editor-mode:v1 updates on toggle
    • Default is 'form' on fresh install (no key in storage)

    QA Scenarios:

    Scenario: Mode toggle persists across reload
      Tool: Bash (vitest or playwright)
      Steps:
        1. Run: bun test packages/chess/src/ui/CustomModifierEditor.test.tsx -t "mode persistence" 2>&1 | tee .sisyphus/evidence/task-22-mode-persist.txt
        2. Assert: After toggle to visual, localStorage contains "visual"; next mount reads it back
      Evidence: .sisyphus/evidence/task-22-mode-persist.txt
    
    Scenario: Existing form mode preserved
      Tool: Bash (playwright)
      Steps:
        1. Run: cd packages/chess && bun x playwright test e2e/custom-modifiers.spec.ts
        2. Assert: 100% pass rate (existing form-mode flows unchanged)
      Evidence: .sisyphus/evidence/task-22-e2e-unchanged.txt
    

    Evidence: task-22-mode-persist.txt, task-22-e2e-unchanged.txt

    Commit: YES. Message: feat(chess/ui): add Form/Visual mode toggle to CustomModifierEditor. Files: CustomModifierEditor.tsx. Pre-commit: bun run check

  • 23. packages/chess/src/ui/CustomModifierEditor.mode-roundtrip.test.tsx: Form↔Visual byte-equal round-trip test

    What to do:

    • Write test: render CustomModifierEditor with fixture descriptor (5 primitives including a nested trigger). Simulate toggle form→visual→form. After each toggle, call JSON.stringify on current descriptor and assert it equals the original.
    • Extend coverage: load descriptor from library, toggle modes 5 times, assert no drift.

    Must NOT do: test DnD behavior here (that's T25); assert visual rendering (T22 already covers rendering)

    Recommended Agent Profile: quick. Skills: []

    Parallelization: Wave 4. Blocks F3. Blocked by T22.

    References:

    • packages/chess/src/ui/CustomModifierEditor.tsx (T22 updated)
    • Fixture descriptor from T20 legacy fixture

    Acceptance Criteria:

    • bun test packages/chess/src/ui/CustomModifierEditor.mode-roundtrip.test.tsx → PASS
    • 5-toggle drift test passes

    QA Scenarios:

    Scenario: Form → Visual → Form byte-equal
      Tool: Bash (vitest)
      Steps:
        1. Run: bun test packages/chess/src/ui/CustomModifierEditor.mode-roundtrip.test.tsx 2>&1 | tee .sisyphus/evidence/task-23-roundtrip.txt
        2. Assert: JSON.stringify(descriptor_after) === JSON.stringify(descriptor_before)
      Expected: No drift
      Evidence: .sisyphus/evidence/task-23-roundtrip.txt
    

    Evidence: task-23-roundtrip.txt

    Commit: YES. Message: test(chess/ui): add mode toggle round-trip byte-equal test. Files: CustomModifierEditor.mode-roundtrip.test.tsx. Pre-commit: bun run check

  • 24. packages/chess/src/modifiers/custom/validate.test.ts (extend): Depth-3 composition test + depth-4 rejection

    What to do:

    • Add test scenarios:
      • Depth-3 valid: conditional → on-move → add-to-attribute (exactly 3 levels) → validateCustomDescriptor returns {ok: true}
      • Depth-4 invalid: conditional → on-move → conditional → add-to-attribute → returns {ok: false} with error code descriptor.primitives.depth.exceeded
      • Mixed new + old kinds at depth 3 still valid: on-captured{target:ally} → conditional → add-aura
      • 50-node cap still enforced (just verify existing test still green)

    Must NOT do: raise depth cap; change existing depth test

    Recommended Agent Profile: quick. Skills: []

    Parallelization: Wave 4. Blocks F1. Blocked by T21.

    References:

    • packages/chess/src/modifiers/custom/validate.ts:MAX_RECURSION_DEPTH
    • packages/chess/src/modifiers/custom/validate.test.ts — existing test file to extend

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/custom/validate.test.ts → PASS with new scenarios
    • Depth-3-valid and depth-4-invalid both demonstrated

    QA Scenarios:

    Scenario: Depth-3 trigger composition accepted, depth-4 rejected
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/custom/validate.test.ts -t "depth" 2>&1 | tee .sisyphus/evidence/task-24-depth.txt
        2. Assert: Depth-3 test: {ok: true}. Depth-4 test: {ok: false} with code "descriptor.primitives.depth.exceeded"
      Expected: Cap enforced precisely
      Evidence: .sisyphus/evidence/task-24-depth.txt
    

    Evidence: task-24-depth.txt

    Commit: YES. Message: test(chess/modifiers): add depth-3 trigger composition test + depth-4 rejection test. Files: validate.test.ts. Pre-commit: bun run check

  • 25. packages/chess/e2e/custom-modifier-visual.spec.ts: Playwright e2e visual authoring flow

    What to do:

    • New Playwright spec. Flow: (1) open chess app, start solo, open rules drawer, open custom modifier editor. (2) Toggle to Visual mode — assert palette + block list + preview visible. (3) Drag (or click-to-add) on-move primitive from palette. (4) Inside the on-move block's children zone, add add-to-attribute with Hp +1. (5) Verify preview narrative shows "When this piece moves, add 1 to Hp". (6) Save to library. (7) Reload page. (8) Open library, reload saved descriptor. (9) Verify Visual mode renders the saved structure identically.
    • Include a negative: (10) attempt to drop a 4th-level block — assert toast error or no-op.
    • Screenshots at each major step to .sisyphus/evidence/task-25-*.png.

    Must NOT do: mock the engine; stub dnd-kit (use real events); skip negative scenarios

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

    Parallelization: Wave 4. Blocks F3. Blocked by T20, T21, T22.

    References:

    • packages/chess/e2e/custom-modifiers.spec.ts — pattern for localStorage seeding and flow
    • packages/chess/playwright.config.ts — test runner config

    Acceptance Criteria:

    • cd packages/chess && bun x playwright test e2e/custom-modifier-visual.spec.ts → all scenarios PASS
    • Evidence screenshots saved at each major step
    • At least 10 assertions covering happy path + 1 negative

    QA Scenarios:

    Scenario: Full visual authoring + save + reload round-trip
      Tool: Playwright
      Preconditions: Chess app buildable (`bun run build` passing in packages/chess)
      Steps:
        1. Run: cd /home/joey/Projects/rules/packages/chess && bun x playwright test e2e/custom-modifier-visual.spec.ts --reporter=list 2>&1 | tee ../../.sisyphus/evidence/task-25-e2e.txt
        2. Navigate: localhost dev → Play Solo → Rules Drawer → Custom Modifier Editor → Visual mode toggle
        3. Click palette item 'on-move' → assert block appears (selector: [data-testid="block-on-move"])
        4. Within block, click '+' to add child → select 'add-to-attribute' → set attr='Hp', delta=1
        5. Assert preview narrative (selector: [role="region"][aria-label="narrative"]) contains "When this piece moves, add 1 to Hp"
        6. Click Save to Library
        7. Reload page (page.reload())
        8. Open editor → Load → select saved descriptor
        9. Assert Visual mode still active + block structure rendered identically
      Expected Result: Descriptor round-trips through save/reload with no data loss
      Failure Indicators: Any step fails; any selector not found
      Evidence: task-25-e2e.txt + screenshots at each step
    
    Scenario: Depth-4 drop rejected with user feedback
      Tool: Playwright
      Steps:
        1. Build descriptor with conditional → on-move → conditional structure (depth 3)
        2. Attempt to drop add-to-attribute into the innermost conditional (would be depth 4)
        3. Assert: toast message "Maximum nesting depth exceeded" visible; descriptor unchanged
      Expected Result: Depth-4 drop rejected
      Evidence: .sisyphus/evidence/task-25-depth-reject.png + task-25-depth-reject.txt
    

    Evidence: task-25-e2e.txt, task-25-*.png screenshots, task-25-depth-reject.{txt,png}

    Commit: YES. Message: test(chess/e2e): add Playwright visual-mode authoring flow. Files: e2e/custom-modifier-visual.spec.ts. Pre-commit: bun run check

  • 26. packages/chess/src/modifiers/custom/recipes.ts: Add 2+ new recipes showcasing new triggers

    What to do:

    • Add at least 2 new entries to CUSTOM_MODIFIER_RECIPES:
      • "Kamikaze Knight" — on-captured with target: 'attacker' → add-to-attribute Hp -2. Death-rattle that damages whoever captures this piece.
      • "Berserker Pawn" — on-move → add-to-attribute attack-bonus +1 (stacking rage). At each move, the piece grows stronger.
    • Optional third: "Promotion Feast" — on-promotion → seed-attribute Hp = 5, capitalizing on promotedTo from event
    • Each recipe validates via validateCustomDescriptor → {ok: true} (verified by recipes.test.ts)

    Must NOT do: use kinds not yet implemented; skip recipe tests; introduce recipe IDs that collide with existing

    Recommended Agent Profile: quick. Skills: []

    Parallelization: Wave 4. Blocks F1. Blocked by T21.

    References:

    • packages/chess/src/modifiers/custom/recipes.ts — existing recipe format
    • packages/chess/src/modifiers/custom/recipes.test.ts — existing recipe validation test

    Acceptance Criteria:

    • bun test packages/chess/src/modifiers/custom/recipes.test.ts → PASS with ≥ 7 recipes (5 existing + 2 new minimum)
    • Each new recipe validates via validateCustomDescriptor

    QA Scenarios:

    Scenario: New recipes validate + render
      Tool: Bash (bun test)
      Steps:
        1. Run: bun test packages/chess/src/modifiers/custom/recipes.test.ts 2>&1 | tee .sisyphus/evidence/task-26-recipes.txt
        2. Assert: 7+ recipes total; each validates cleanly
      Evidence: .sisyphus/evidence/task-26-recipes.txt
    

    Evidence: task-26-recipes.txt

    Commit: YES. Message: feat(chess/modifiers): add recipes showcasing on-captured + on-move triggers. Files: recipes.ts, recipes.test.ts. Pre-commit: bun run check

  • 27. packages/chess/RULES.md + packages/chess/docs/PRESET-API.md: Document new trigger semantics + target redirection

    What to do:

    • Add section to RULES.md or a new packages/chess/docs/TRIGGERS.md: documentation for all 11 triggers (4 existing + 7 new) with exact firing semantics locked in the interview:
      • on-move: "fires whenever this piece's Position WME changes"
      • on-turn-end: "fires at end of matching-color turn, BEFORE opponent's on-turn-start"
      • on-promotion: "fires AFTER PieceType flip; event context carries {promotedFrom, promotedTo}"
      • on-check-received: "edge-triggered; fires only on transition into check; royal pieces only"
      • on-check-delivered: "fires on the piece whose attack-line newly threatens opponent royal; discovered check attributes to the revealing piece"
      • on-moved-onto-square: "predicate is discriminated union of {squares: Square[]} or {file?, rank?}"
      • on-captured: "fires on lethal damage BEFORE retraction; use target to redirect nested primitives"
    • Document target field and TargetResolver semantics in PRESET-API.md
    • Document dispatch order (Metis-locked)

    Must NOT do: document internal implementation details users don't need; skip examples

    Recommended Agent Profile: writing. Skills: []

    Parallelization: Wave 4. Blocks F1. Blocked by T21.

    References:

    • packages/chess/RULES.md — existing docs structure to extend
    • packages/chess/docs/PRESET-API.md — hook reference to extend

    Acceptance Criteria:

    • RULES.md (or new TRIGGERS.md) contains sections for all 7 new triggers with firing rules
    • PRESET-API.md updated with TargetResolver + PrimitiveEvent context fields
    • Dispatch order from Metis documented

    QA Scenarios:

    Scenario: Docs contain all 7 new trigger entries
      Tool: Bash
      Steps:
        1. Run: grep -c "^### on-" /home/joey/Projects/rules/packages/chess/RULES.md /home/joey/Projects/rules/packages/chess/docs/*.md 2>&1 | tee .sisyphus/evidence/task-27-docs-count.txt
        2. Assert: Total on-* sections ≥ 11
      Expected: Each trigger has a dedicated section
      Evidence: .sisyphus/evidence/task-27-docs-count.txt
    
    Scenario: Firing-rule language matches user decisions
      Tool: Bash (grep)
      Steps:
        1. Run: grep -i "edge-triggered" /home/joey/Projects/rules/packages/chess/RULES.md /home/joey/Projects/rules/packages/chess/docs/*.md
        2. Assert: on-check-received documented as edge-triggered
      Evidence: .sisyphus/evidence/task-27-docs-semantics.txt
    

    Evidence: task-27-docs-count.txt, task-27-docs-semantics.txt

    Commit: YES. Message: docs(chess): document new trigger semantics in RULES.md + PRESET-API.md. Files: RULES.md, docs/PRESET-API.md. Pre-commit: bun run check

  • 28. packages/chess: Add dnd-kit deps + verify bundle size delta + registry count assertion

    What to do:

    • bun add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities in packages/chess
    • Run bun run build, measure dist/ output bytes (gz via gzip | wc -c or vite's built-in size report)
    • Compare against baseline (capture baseline size via git stash trick or pre-task measurement); assert delta ≤ 25kb gz
    • Assert PRIMITIVE_REGISTRY.list().length === 21 via a one-off script: bun -e "import {PRIMITIVE_REGISTRY} from './packages/chess/src/modifiers/primitives/index.js'; console.log(PRIMITIVE_REGISTRY.list().length)"

    Must NOT do: add other deps; disable Vite tree-shaking; skip delta measurement

    Recommended Agent Profile: quick. Skills: []

    Parallelization: Wave 4. Blocks F2. Blocked by T22.

    References:

    • packages/chess/package.json — add deps
    • packages/chess/vite.config.ts — build config

    Acceptance Criteria:

    • bun run build in packages/chess → exits 0
    • Bundle delta ≤ 25kb gz
    • PRIMITIVE_REGISTRY.list().length === 21
    • @dnd-kit/* listed in dependencies (not devDependencies)

    QA Scenarios:

    Scenario: Bundle builds and stays under budget
      Tool: Bash
      Steps:
        1. Run: cd /home/joey/Projects/rules/packages/chess && bun run build 2>&1 | tee ../../.sisyphus/evidence/task-28-build.txt
        2. Run: ls -la dist/ | awk '{sum+=$5} END {print sum}'  (capture total bundle size)
        3. Assert: Delta from pre-task baseline ≤ 25600 bytes (gz); if exact baseline unrecorded, total bundle size is within repository's documented ceiling
      Evidence: .sisyphus/evidence/task-28-build.txt, task-28-size.txt
    
    Scenario: Registry count locked at 21
      Tool: Bash
      Steps:
        1. Run: cd /home/joey/Projects/rules && bun -e "import('./packages/chess/src/modifiers/primitives/index.js').then(m => console.log(require('./packages/chess/src/modifiers/primitives/registry.js').PRIMITIVE_REGISTRY.list().length))" 2>&1 | tee .sisyphus/evidence/task-28-registry.txt
        2. Assert: Output === "21"
      Evidence: .sisyphus/evidence/task-28-registry.txt
    

    Evidence: task-28-build.txt, task-28-size.txt, task-28-registry.txt

    Commit: YES. Message: chore(chess): add dnd-kit deps + verify bundle size delta. Files: package.json, bun.lock. Pre-commit: bun run check


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. Never mark F1-F4 as checked before getting user's okay. Rejection or user feedback → fix → re-run → present again → wait for okay.

  • F1. Plan Compliance Audit — oracle

    Read this plan end-to-end. For each "Must Have": verify implementation exists by inspecting the cited file:line and running the cited command. For each "Must NOT Have": grep the codebase for forbidden patterns and reject with file:line if found (e.g., grep -r "MAX_RECURSION_DEPTH" packages/chess/src/modifiers/ must show only = 3). Check every evidence file exists in .sisyphus/evidence/. Compare delivered files to "Concrete Deliverables" list — every path must exist.

    Output: Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | Evidence files [N/N present] | VERDICT: APPROVE/REJECT

  • F2. Code Quality Review — unspecified-high

    Run cd /home/joey/Projects/rules && bun run check — must be green. Review all changed files via git diff main...HEAD for: as any, @ts-ignore, empty catches, console.log, commented-out code, unused imports, generic names (data/result/item/temp). Check AI slop: excessive comments, over-abstraction, extracted helpers with one consumer. Check bundle size delta stays within 25kb gz.

    Output: Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | Bundle delta [Xkb] | VERDICT

  • F3. Real Manual QA — unspecified-high + playwright skill

    Start from clean state (clear localStorage). Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence screenshots + HAR. Test cross-task integration: (a) author a modifier in Form mode with 3 old kinds, toggle to Visual, verify renders; (b) author using new on-move + nested add-to-attribute, save, reload page, verify persists; (c) save via "Share with Room" broadcast, verify server accepts (mock/real); (d) depth-3 nested trigger-in-conditional-in-trigger validates. Edge cases: empty descriptor in Visual mode, 50-node descriptor narrates <1ms, mode toggle with invalid JSON in form shows fallback. Save all evidence 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 T1–T28: read "What to do", read actual diff via git log --oneline main..HEAD + git diff main...HEAD -- <paths>. Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance per task. Detect cross-task contamination: Task N touching Task M's files inappropriately (e.g., T5 on-move primitive accidentally editing CustomModifierEditor.tsx). Flag unaccounted changes (files modified that aren't in any task deliverable list).

    Output: Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT


Commit Strategy

Atomic commits. Each commit passes bun run check on its own.

  • T1: feat(chess/modifiers): extend PrimitiveApplyContext with target resolver + event field
    • Files: packages/chess/src/modifiers/primitives/{types.ts,context.ts}, tests
  • T2: feat(chess/schema): add 7 new hook attrs to ChessAttrMap
    • Files: packages/chess/src/schema.ts
  • T3: feat(chess/modifiers): register 7 new attr consumers
    • Files: packages/chess/src/modifiers/apply.ts
  • T4: feat(chess/modifiers): add pre-move check/promotion state snapshots
    • Files: packages/chess/src/modifiers/apply.ts
  • T5–T11: feat(chess/primitives): add <kind> primitive (one per commit, TDD)
  • T12: feat(chess/modifiers): add 7 fire*Hooks evaluators in triggers.ts
  • T13: feat(chess/server): mirror 7 new primitive kinds in wire schema + parity fixtures
  • T14: refactor(chess/ui): extract ParamField from CustomModifierEditor (no behavior change)
  • T15: feat(chess/ui): add narrate.ts pure module for descriptor → English
  • T16: feat(chess/ui): add BlockCard component
  • T17: feat(chess/ui): add PreviewPane (Narrative/JSON/Board tabs)
  • T18: feat(chess/ui): add BlockList with dnd-kit sortable (nested)
  • T19: feat(chess/ui): add VisualBuilderPane composing blocks + preview
  • T20: test(chess): add legacy descriptor backward-compat fixture + round-trip test
  • T21: feat(chess/modifiers): wire 7 new triggers in apply.ts onAfterMove + dispatch order test
  • T22: feat(chess/ui): add Form/Visual mode toggle to CustomModifierEditor
  • T23: test(chess/ui): add mode toggle round-trip byte-equal test
  • T24: test(chess/modifiers): add depth-3 trigger composition test + depth-4 rejection test
  • T25: test(chess/e2e): add Playwright visual-mode authoring flow
  • T26: feat(chess/modifiers): add recipes showcasing on-captured + on-move triggers
  • T27: docs(chess): document new trigger semantics in RULES.md + PRESET-API.md
  • T28: chore(chess): add dnd-kit deps + verify bundle size delta

Pre-commit hook: bun run check (lint + typecheck + tests). Never --no-verify.


Success Criteria

Verification Commands

# From repo root
bun run check                                                          # PASS (full repo lint + typecheck + test)
bun test packages/chess                                                # PASS (all chess unit tests)
bun test packages/chess/src/modifiers/primitives/registry.test.ts      # 22 primitives registered
bun test packages/chess/src/modifiers/custom/validate.test.ts          # depth/count/cycle validators pass
bun test packages/chess/src/modifiers/apply.test.ts                    # dispatch ordering locked in
bun test packages/chess/src/ui/narrate.test.ts                         # golden outputs match
bun test packages/chess/src/ui/visual-builder                          # block editor unit tests pass
cd packages/chess && bun x playwright test e2e/custom-modifier-visual.spec.ts  # e2e pass
bun run build                                                          # bundle delta ≤ 25kb gz

Final Checklist

  • All "Must Have" items present (verified via F1)
  • All "Must NOT Have" items absent (verified via F2 + F4)
  • All 28 implementation tasks (T1–T28) complete and commits atomic
  • F1 + F2 + F3 + F4 all APPROVE
  • User explicitly okayed the verification results
  • Draft file .sisyphus/drafts/visual-modifier-builder.md deleted after plan delivery