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.
104 KiB
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)
PrimitiveApplyContextextension:targetredirection 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
ParamFieldfrom existing inspector (pure refactor)- New
narrate.tspure module- 2+ new recipes showcasing new triggers
- Updated docs:
RULES.md,PRESET-API.md, user-facing trigger semanticsEstimated 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 extensionon-captured: requires target redirection (self/ally/enemy/square) → all primitive contexts get atargetresolver- 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-squarepredicate:{ squares: Square[] }OR{ file?, rank? };colordropped (ambiguous)on-movefires on any Position change (captures too — bothon-captureandon-movefire, dispatched in order)- DSL
version: 1kept — new kinds are additive; existing descriptors load unchanged - T1
ModifierProfileEditorout of scope - Multiplayer
onShareWithRoompreserved (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 = 3in validator; runtime walker caps at 8. Stays at 3.MAX_PRIMITIVE_COUNT = 50in 3 places (schema, validator, server wire) — cross-package parity test Q4.2 protects drift- Dispatch pattern: integration preset in
modifiers/apply.ts,onAfterMovecallsfire*Hooks()fromtriggers.ts - Attr consumer pattern:
registerAttrConsumer(attrKey)inapply.ts; boot-time integrity check fails if any primitive-seeded attr lacks a consumer - Pre-move snapshots:
PRE_MOVE_HP_SNAPSHOTS,PRE_MOVE_CAPTURE_ATTACKERSvia per-engine WeakMaps set inonBeforeMove - 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:v1localStorage (20 entries FIFO, starred exempt) packages/rete= pure rules engine, NOT a visual node-graph editor
Metis Review
Identified Gaps (addressed):
on-promotiontiming → user chose "both types in context" → drives context extension (T1)on-check-receivededge vs level → edge-triggered (prevents infinite sustain)on-movefiring rule → "any Position fact change" (clean single rule)on-moved-onto-squarepredicatecolorambiguity → droppedon-capturedself-only limitation → user opted for target redirection → drives context extension (T1)- Dispatch ordering → Metis-proposed order locked by explicit test
on-move+on-captureoverlap → 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.tsis 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— newTargetResolvertype +resolveTargets()helper- 7 new primitive files in
packages/chess/src/modifiers/primitives/:on-move.ts+.test.tson-turn-end.ts+.test.tson-promotion.ts+.test.tson-check-received.ts+.test.tson-check-delivered.ts+.test.tson-moved-onto-square.ts+.test.tson-captured.ts+.test.ts
- Updated
primitives/types.ts—PrimitiveKindunion extended;PrimitiveApplyContextgainstargetfield +eventfield - Updated
primitives/index.ts— side-effect imports for new primitives - Updated
schema.ts— 7 newChessAttrMapentries (OnMoveHooks,OnTurnEndHooks,OnPromotionHooks,OnCheckReceivedHooks,OnCheckDeliveredHooks,OnMovedOntoSquareHooks,OnCapturedHooks) - Updated
modifiers/triggers.ts— 7 newfire*Hooks()evaluator functions - Updated
modifiers/apply.ts— 7 newregisterAttrConsumer()calls, dispatch ordering inonAfterMove, 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.tsor 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 modulepackages/chess/src/ui/visual-builder/BlockCard.tsx+.test.tsx— single primitive block rendererpackages/chess/src/ui/visual-builder/BlockList.tsx+.test.tsx— dnd-kit SortableContext tree with nested trigger childrenpackages/chess/src/ui/visual-builder/VisualBuilderPane.tsx— composes BlockList + PreviewPane, replaces center+right when mode=visualpackages/chess/src/ui/visual-builder/preview/PreviewPane.tsx— tabs: Narrative / JSON / Boardpackages/chess/src/ui/visual-builder/preview/NarrativeView.tsxpackages/chess/src/ui/visual-builder/preview/JsonView.tsxpackages/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 keyhouserules: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 + testsbun test packages/chess— all 22 primitives registered (15 pre-existing + 7 new), all trigger tests passbun test packages/chess/src/modifiers/apply.test.ts— dispatch ordering test proves 12-stage orderbun test packages/chess/src/ui/narrate.test.ts— golden-output tests for all 21 kindsbun test packages/chess/src/modifiers/custom/validate.test.ts— depth-3 accepted, depth-4 rejected, cycles rejectedcd 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.tsvalidate viavalidateCustomDescriptor→{ok: true}
Must Have
- 7 new trigger primitives, fully wired through the DSL pipeline
PrimitiveApplyContextextension:targetresolver + per-eventeventfield- 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 forParamField - E2E Playwright coverage of visual-mode authoring flow
Must NOT Have (Guardrails)
Scope creep locks (explicit):
- MUST NOT change
CustomModifierDescriptor.versionfrom1 - MUST NOT change
uiForm: 'primitive-composer'orsource: 'custom'literals - MUST NOT raise
MAX_RECURSION_DEPTHfrom 3 orMAX_PRIMITIVE_COUNTfrom 50 - MUST NOT touch
packages/rete/**(pure engine, orthogonal) - MUST NOT touch
ModifierProfileEditorbeyond incidental (T1 modifiers out of scope) - MUST NOT change
onShareWithRoomwire 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 anyor@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.logleft 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 buildthen 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+playwrightskill, T26 →quick, T27 →writing, T28 →quick - FINAL: 4 tasks — F1 →
oracle, F2 →unspecified-high, F3 →unspecified-high+playwrightskill, F4 →deep
TODOs
-
1.
packages/chess/src/modifiers/primitives/types.ts+context.ts: ExtendPrimitiveApplyContextwithtargetresolver andeventfieldWhat to do:
- Add new
TargetResolvertype:'self' | 'attacker' | 'defender' | { squares: Square[] } | { relation: 'ally' | 'enemy', filter?: { pieceType?: PieceType } } - Extend
PrimitiveApplyContext: addreadonly target: TargetResolver(default'self') andreadonly event: PrimitiveEvent | undefined(carries trigger-specific metadata like{ promotedFrom: PieceType, promotedTo: PieceType }for on-promotion) - Create new file
packages/chess/src/modifiers/primitives/context.tsexportingresolveTargets(ctx, target): readonly EntityId[]— walks session to find matching pieces - Preserve
ctx.pieceIdas the CURRENT-apply target;resolveTargets()returns[ctx.pieceId]whentarget === 'self' - Update the 15 existing primitives that use
ctx.pieceIdto route throughresolveTargets()whenctx.target !== 'self'— but default-case path is unchanged so existing behavior is byte-identical - Add unit tests:
context.test.tscovers each TargetResolver variant against a fixture session
Must NOT do:
- Do NOT change the
ctx.pieceIdfield — 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
CustomModifierDescriptorschema (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 involvedcode-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— currentPrimitiveApplyContextinterface (must preserve existing fields)packages/chess/src/modifiers/primitives/on-capture.ts— canonical trigger primitive that usesctx.pieceIdpackages/chess/src/modifiers/custom/apply.ts— orchestrator that constructs the context; update here to populate new fieldspackages/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 typepackages/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 behaviorpackages/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 markerson-capture.ts: Template for how trigger primitives consume the context — extension must not break this shapecustom/apply.ts: The construction site forPrimitiveApplyContext— this is where defaults fortargetandeventare injected
Acceptance Criteria:
packages/chess/src/modifiers/primitives/context.tsexists and exportsTargetResolver,PrimitiveEvent,resolveTargetsbun 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'sapply()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.txtEvidence to Capture:
task-1-existing-primitives-pass.txt— full bun test outputtask-1-ally-resolver.txt— specific test outputtask-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 tocustom/apply.ts - Pre-commit:
bun run check
- Add new
-
2.
packages/chess/src/schema.ts: Add 7 new hook attrs toChessAttrMapWhat to do:
- Add to
ChessAttrMapinterface: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
SquareFiltertype:{ kind: 'squares', squares: readonly Square[] } | { kind: 'predicate', file?: 'a'|...|'h', rank?: 1|...|8 } - Update
ChessAttrKeyunion (auto-derived from ChessAttrMap) — no manual work if union useskeyof
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
readonlyarrays (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— existingChessAttrMap— add new entries following the pattern ofOnTurnStartHooks,OnCaptureHooks,OnDamagedHooks,ConditionalHookspackages/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/validateEffectPrimitiveNode: each hook attr's array-of-arrays element type is this structure
Acceptance Criteria:
bun tsc --noEmit→ 0 errorsgrep -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.txtEvidence to Capture:
task-2-tsc.txttask-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
- Add to
-
3.
packages/chess/src/modifiers/apply.ts: Register 7 new attr consumersWhat 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— existingregisterAttrConsumercalls — add new ones in the same stylepackages/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 idiommanifest.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→ PASSbun test packages/chess/src/modifiers/primitives/consumer-integration.test.ts→ PASSgrep "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.txtEvidence to Capture:
task-3-consumer-integrity.txttask-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
- Add 7
-
4.
packages/chess/src/modifiers/apply.ts: Pre-move state snapshots for check + promotion diffWhat 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 movePRE_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
onAfterMoveend (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 forPRE_MOVE_HP_SNAPSHOTSandPRE_MOVE_CAPTURE_ATTACKERS— mirror the WeakMap + lifecycle patternpackages/chess/src/engine.ts:1350-1376— promotion resolution inapplyMove;opponentInCheckcomputation line 1371packages/chess/src/rules/— check-detection helpers
WHY Each Reference Matters:
PRE_MOVE_HP_SNAPSHOTSpattern is the canonical lifecycle for pre-move state; copy structure exactlyengine.ts:1350-1376shows 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"→ PASSbun 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.txtEvidence to Capture:
task-4-check-snapshot.txttask-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
- Add per-engine WeakMaps (like existing
-
5.
packages/chess/src/modifiers/primitives/on-move.ts+.test.ts: Addon-moveprimitive (TDD)What to do:
- RED first: author
on-move.test.tswith 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.tsmirroringon-capture.tsstructure — paramsSchema{ primitives: z.array(EffectPrimitiveNodeSchema).max(10) },apply()writesOnMoveHooksarray 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
PrimitiveKindunion intypes.ts
Must NOT do: change existing primitives; add new schema fields beyond
primitives[]; skip docs metadataRecommended 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 exactlypackages/chess/src/modifiers/primitives/on-capture.test.ts— test shape + fixture setuppackages/chess/src/modifiers/primitives/registry.ts— where the new primitive self-registerspackages/chess/src/modifiers/primitives/index.ts— barrel import (addimport './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.tsshows 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.txtEvidence: 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 - RED first: author
-
6.
packages/chess/src/modifiers/primitives/on-turn-end.ts+.test.ts: Addon-turn-endprimitive (TDD)What to do:
- RED first:
on-turn-end.test.tswith 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: [...] }. WriteOnTurnEndHooksattr. 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.txtEvidence: 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 - RED first:
-
7.
packages/chess/src/modifiers/primitives/on-promotion.ts+.test.ts: Addon-promotionprimitive 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 primitiveadd-to-attribute Hp +1runs with new PieceType visible via ctx.event, (c) does NOT fire when promotion is disabled byoverride-promotionwith disabled target, (d) fires AFTER PieceType fact flip - GREEN: paramsSchema
{ primitives: [...] }. apply() writesOnPromotionHooks. UsesPrimitiveEventfrom 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 mirrorpackages/chess/src/engine.ts:1350-1376— promotion resolution in applyMove (event capture point)packages/chess/src/modifiers/primitives/override-promotion.ts— check ifPromotionOverride === '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.txtEvidence: 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 - RED first: test that (a) fires when pawn promotes to queen — event carries
-
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() writesOnCheckReceivedHooks. 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—getRoyalPieceshook 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.txtEvidence: 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() writesOnCheckDeliveredHooks. 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.txtEvidence: 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
OnMovedOntoSquareHooksattr entries ={ filter: SquareFilter, primitives: [...] }
Must NOT do: add a
colorfield to predicate; allow empty predicate (file AND rank both absent) — must have at least one constraintRecommended 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 mirrorpackages/chess/src/schema.ts— Square type,SquareFiltertype 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.txtEvidence: 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'}nestedadd-to-attribute Hp +1buffs all friendly pieces, (d) withtarget: 'attacker'nestedadd-to-attribute Hp -1damages 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() writesOnCapturedHookswith target embedded. Evaluator (T12) fires hook withctx.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 primitivetarget: {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.txtEvidence: 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 - 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
-
12.
packages/chess/src/modifiers/triggers.ts: Add 7 newfire*Hooksevaluator functionsWhat to do:
- Add evaluators mirroring
fireOnCaptureHooks/fireOnTurnStartHookspattern:fireOnMoveHooks(engine, movedPieceIds: readonly EntityId[])— iterates hooks, runs nested primitivesfireOnTurnEndHooks(engine, endedColor)— color-filtered (matches hook'scolor: 'white'|'black'|'both')fireOnPromotionHooks(engine, promotedPieceId, promotedFrom: PieceType, promotedTo: PieceType)— populates ctx.eventfireOnCheckReceivedHooks(engine, royalId)— edge-filtered via T4 snapshot difffireOnCheckDeliveredHooks(engine, newlyAttackingPieceIds)— edge-filteredfireOnMovedOntoSquareHooks(engine, movedPieceId, destSquare)— filter matches SquareFilterfireOnCapturedHooks(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 patternpackages/chess/src/modifiers/primitives/context.ts(T1) — forresolveTargets()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.txtEvidence: 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 - Add evaluators mirroring
-
13.
packages/server/src/wire/custom-modifier.ts(or equivalent): Mirror 7 new primitive kinds in server wire schema + parity fixtureWhat to do:
- Locate server wire schema for CustomModifierDescriptor validation (likely in
packages/server/src/wire/orpackages/server/src/routes/) - Mirror new
PrimitiveKindadditions (OR confirm schema usesz.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 docspackages/server/src/— locate CustomModifierDescriptor validator (grep forprimitives+customschema)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.txtEvidence: 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 - Locate server wire schema for CustomModifierDescriptor validation (likely in
-
14.
packages/chess/src/ui/ParamField.tsx+.test.tsx: ExtractPrimitiveInspectorparam-rendering logic fromCustomModifierEditor.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.tsxthat renders current component with 14 fixture descriptors, snapshots DOM. - Extract lines 625-872 of
CustomModifierEditor.tsxinto newParamField.tsxpreserving Zod-introspection patterns verbatim ParamFieldprops:{ 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— thePrimitiveInspectorfunction to extractpackages/chess/src/ui/AttrCombobox.tsx— internal dep kept by importpackages/chess/src/ui/attr-suggestions.ts— type helpers used by inspectorpackages/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 --statshows 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.txtEvidence: 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 - BEFORE touching code: capture golden snapshot of current inspector output for every kind's paramsSchema. Write
-
15.
packages/chess/src/ui/narrate.ts+.test.ts: Pure descriptor → English narrative moduleWhat to do:
- Pure module. Zero session/engine access. Input:
CustomModifierDescriptororreadonly 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 coverpackages/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.txtEvidence: 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 - Pure module. Zero session/engine access. Input:
-
16.
packages/chess/src/ui/visual-builder/BlockCard.tsx+.test.tsx: Single primitive block rendererWhat 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
ParamFieldfrom T14 in the inspector-overlay (only when expanded/selected) - Nested triggers render child BlockCards recursively with
depth + 1for 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 affordancesfrontend-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.txtEvidence: 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: callsnarrate(descriptor)from T15, renders in a scrollable<pre>with semanticrole="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-squarefilters (yellow overlay) - Aura radius (concentric ring)
- Poisoned/affected squares (from existing integration)
- If descriptor has no positional primitives, render "No board effect" placeholder
- Squares from
- 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 referencepackages/chess/src/coord.ts— square-to-coords helperspackages/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.txtEvidence: 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 triggersWhat 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+KeyboardSensorwithsortableKeyboardCoordinates. DragOverlaycomponent for drag preview (prevents layout jank during drag).onDragEnd: callsonReorder(activeId, overId, isNested: boolean)— parent component updates descriptor tree structurally.- Announcements: custom
announcementsprop 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/sortabledocs — SortableContext,arrayMove,rectSortingStrategypackages/chess/src/ui/visual-builder/BlockCard.tsx(T16) — integrates viauseSortable
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.txtEvidence: 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 + PreviewPaneWhat 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 === falseAND 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.txtEvidence: 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 testWhat 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 viaparseCustomModifierDescriptorwithout error, (b) validates viavalidateCustomDescriptor→{ok: true}, (c) applies viaapplyCustomDescriptor— 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 examplespackages/chess/src/modifiers/custom/schema.ts:parseCustomModifierDescriptorpackages/chess/src/modifiers/custom/validate.tspackages/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.txtEvidence: 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 testWhat to do:
- In
onAfterMoveintegration preset hook, add calls in Metis-locked order:computeAuraFacts(existing)fireOnDamagedHooks(existing)fireOnCaptureHooks(existing)fireOnCapturedHooks(engine, capturedIds, attackerId)— BEFORE retraction (T11)fireOnPromotionHooks(engine, ...)— uses T4 flagfireOnMoveHooks(engine, movedPieceIds)— via Position difffireOnMovedOntoSquareHooks(engine, movedPieceId, destSquare)fireOnCheckReceivedHooks(engine, royalId)— T4 snapshot difffireOnCheckDeliveredHooks(engine, newlyAttackingIds)— T4 snapshot difffireConditionalHooks(existing)fireOnTurnEndHooks(engine, endedColor)— for moverfireOnTurnStartHooks(engine, newColor)(existing) — for next color
- Clear T4 snapshots at end of onAfterMove
- Write ORDERING TEST:
apply.test.tsregisters 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.txtEvidence: 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 - In
-
22.
packages/chess/src/ui/CustomModifierEditor.tsx: Add Form/Visual mode toggle + localStorage persistenceWhat to do:
- Add
mode: 'form' | 'visual'state in CustomModifierEditor. Default: read from localStorage keyhouserules: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 extendpackages/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.tsstill PASS (no regressions) - localStorage
houserules:custom-modifier-editor-mode:v1updates 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.txtEvidence: 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 - Add
-
23.
packages/chess/src/ui/CustomModifierEditor.mode-roundtrip.test.tsx: Form↔Visual byte-equal round-trip testWhat 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.stringifyon 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.txtEvidence: 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 - Write test: render CustomModifierEditor with fixture descriptor (5 primitives including a nested trigger). Simulate toggle form→visual→form. After each toggle, call
-
24.
packages/chess/src/modifiers/custom/validate.test.ts(extend): Depth-3 composition test + depth-4 rejectionWhat to do:
- Add test scenarios:
- Depth-3 valid: conditional → on-move → add-to-attribute (exactly 3 levels) →
validateCustomDescriptorreturns{ok: true} - Depth-4 invalid: conditional → on-move → conditional → add-to-attribute → returns
{ok: false}with error codedescriptor.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)
- Depth-3 valid: conditional → on-move → add-to-attribute (exactly 3 levels) →
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_DEPTHpackages/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.txtEvidence: 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 - Add test scenarios:
-
25.
packages/chess/e2e/custom-modifier-visual.spec.ts: Playwright e2e visual authoring flowWhat 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-moveprimitive from palette. (4) Inside the on-move block's children zone, addadd-to-attributewith 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 flowpackages/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.txtEvidence: 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 - 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)
-
26.
packages/chess/src/modifiers/custom/recipes.ts: Add 2+ new recipes showcasing new triggersWhat 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.
- "Kamikaze Knight" — on-captured with
- Optional third: "Promotion Feast" — on-promotion → seed-attribute Hp = 5, capitalizing on promotedTo from event
- Each recipe validates via
validateCustomDescriptor→{ok: true}(verified byrecipes.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 formatpackages/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.txtEvidence: 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 - Add at least 2 new entries to
-
27.
packages/chess/RULES.md+packages/chess/docs/PRESET-API.md: Document new trigger semantics + target redirectionWhat to do:
- Add section to
RULES.mdor a newpackages/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; usetargetto redirect nested primitives"
- Document
targetfield and TargetResolver semantics inPRESET-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 extendpackages/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 rulesPRESET-API.mdupdated withTargetResolver+PrimitiveEventcontext 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.txtEvidence: 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 - Add section to
-
28.
packages/chess: Add dnd-kit deps + verify bundle size delta + registry count assertionWhat to do:
bun add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilitiesin packages/chess- Run
bun run build, measuredist/output bytes (gz viagzip | wc -cor vite's built-in size report) - Compare against baseline (capture baseline size via
git stashtrick or pre-task measurement); assert delta ≤ 25kb gz - Assert
PRIMITIVE_REGISTRY.list().length === 21via 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 depspackages/chess/vite.config.ts— build config
Acceptance Criteria:
bun run buildin packages/chess → exits 0- Bundle delta ≤ 25kb gz
PRIMITIVE_REGISTRY.list().length === 21@dnd-kit/*listed independencies(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.txtEvidence: 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 —
oracleRead 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-highRun
cd /home/joey/Projects/rules && bun run check— must be green. Review all changed files viagit diff main...HEADfor: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+playwrightskillStart 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+ nestedadd-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 —
deepFor 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 editingCustomModifierEditor.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
- Files:
- T2:
feat(chess/schema): add 7 new hook attrs to ChessAttrMap- Files:
packages/chess/src/schema.ts
- Files:
- T3:
feat(chess/modifiers): register 7 new attr consumers- Files:
packages/chess/src/modifiers/apply.ts
- Files:
- T4:
feat(chess/modifiers): add pre-move check/promotion state snapshots- Files:
packages/chess/src/modifiers/apply.ts
- Files:
- 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.mddeleted after plan delivery