87 KiB
Piece Modifier Profiles
TL;DR
Quick Summary: Users can author Modifier Profiles — reusable sets of rules that attach to pieces by type ("all knights +1 HP") OR by layout slot ("the b1 knight has +2 range"). Profiles are a first-class entity saved to their own library, selectable at game start, hot-swappable mid-game at turn boundaries. Six T1 modifier categories ship end-to-end: HP bonus, movement range, movement directions, capture behavior, promotion override, damage resistance.
Deliverables:
- 2 new preset hooks (
transformMoveGenerator,modifyMoveAttrs) wrapping existing generators (not replacing)- 6 new
ChessAttrMapentries + self-registering modifier catalogModifierProfiletype, Zod schema, library persistence (houserules:modifier-profiles:v1)- WS message
modifier-profile.updatefor hot-swap with server-side legality validator + rollback- Dedicated "Modifier Profiles" editor screen (reuses layout-editor primitives) accessible from rules drawer
- Hover tooltip + click-to-pin side panel for in-play inspection
- Full Playwright e2e vertical slice
- ADR + user docs
Estimated Effort: Large Parallel Execution: YES — 7 waves Critical Path: T1 (ADR) → T3 (hooks) → T5 (registry) → T6 (descriptors) → T11 (apply) → T12 (hot-swap) → T20 (e2e)
Context
Original Request
"plan out the feature of being able to add presets that apply to certain pieces? So like, a particular piece can have more HP, or higher movement, or move backwards, etc"
Interview Summary
Key Decisions (user-confirmed):
- Scope: BOTH layered — per-type (all knights) AND per-instance (the b1 knight). Per-instance overrides per-type.
- T1 modifier catalog (all 6): HP bonus, movement range, movement directions, capture behavior, promotion override, invulnerability/damage resistance.
- UX: Dedicated new editor screen, accessible from rules drawer. Visual (layout-editor style). In-play inspection: hover tooltip + click-to-pin panel.
- Timing: Hot-swap mid-game (applied at turn boundary for determinism).
- Storage: Separate "modifier profiles" as first-class reusable entities — profile + layout combine at game start.
- Depth: Full vertical slice (engine + data + serialization + UI + e2e) for T1. T2/T3 planned as future tiers.
Research Findings (bg_6129dc58)
- Preset hook system is rich (13 hooks) but missing a move-generator override — we add one.
- Pieces have per-instance
EntityIds already. All per-piece state stored via declaredpieceAttributes→ChessAttrMapunion → seeded inonActivate/onPieceSpawn, auto-retracted viaeffectivePieceAttrson death. piece-hp.tsis the reference implementation — follow its pattern.- Facts serialize automatically via
GameStatePayload.facts— no wire changes needed for modifier VALUES, only for modifier DEFINITIONS (new WS message + room-create field). - Move generation keyed by piece TYPE but passes
pieceId— per-instance state readable during generation.
Metis Review (ses_25c313fd2ffeljiMUzgKRTyMM0)
- Identified 3 architecture decisions needing resolution before task-level planning.
- All 3 have been resolved in this plan (see "Architecture Decisions (ADR)" below) with sensible defaults aligned to user's interview answers. User may override.
- Recommended registry pattern for modifier catalog (adopted — T5).
- Recommended wrap semantics over replace for move-generator hook (adopted — T3).
- Recommended layout-bound keying for per-instance (adopted — Option B).
Architecture Decisions (ADR)
These are baked in as defaults. Each has a sensible rationale aligned with user's interview answers. User may override any before execution begins.
ADR-1: Move Generator Hook Contract — WRAP (not REPLACE)
- Decision: New hook signature:
transformMoveGenerator?(engine, pieceId, prevGenerator) => MoveGenerator - Rationale: Composition — multiple presets + profile can stack. Each receives the previous generator and returns a new one. Enables "profile adds backward move" + "preset forbids captures" to layer cleanly.
- Contract: Returned generator emits pseudo-legal moves. Engine's legality layer (self-check filter, turn validation, repetition) ALWAYS runs downstream. Profile-aware generators MUST NOT bypass self-check.
ADR-2: Per-Instance Identity Keying — Layout-Slot Bound (Option B)
- Decision: Per-instance modifiers stored keyed by layout-slot identifier (
squarefrom the layout, e.g."b1"). Profile specifies a targetlayoutIdit's bound to. At game start, whenapplyLayout()assignsEntityIds to pieces, engine mapssquare → EntityIdand seeds facts accordingly. - Rationale: Aligned with user's "profile + layout combine at game start". Portable within a single layout. Cross-layout reuse allowed for per-type; per-instance portion dropped with warning if applied to a non-matching layout.
- Orphan handling: If profile's per-instance entry targets a
squarewith no piece in the current layout, show warning in editor; skip entry at game start (non-fatal).
ADR-3: Hot-Swap Reconciliation (per modifier kind, at turn boundary only)
| Modifier | On swap-apply | On swap-remove |
|---|---|---|
| HpBonus | maxHp recomputed; currentHp = min(currentHp, newMax) (never grows) |
Same clamp rule |
| RangeBonus | Next legal-move query reflects new range | Next legal-move query reflects baseline |
| DirectionAdditions | Next legal-move query includes new directions | Next legal-move query excludes removed directions |
| CaptureFlags | Applies to next capture event | Baseline capture rules from next event |
| PromotionOverride | Applies to future promotions only; past promotions untouched | Future promotions use default |
| DamageResistance | Applies to next damage event | Baseline damage from next event |
- Timing: Hot-swap applies at turn boundary only (after
turnEnd, before next player'sturnStart). - Mid-turn edit attempts: Queued server-side; applied at next turn boundary.
- Rollback: Server runs legality validator BEFORE broadcast. If invalid → NACK to sender with error code, no broadcast. No client optimistic state to rewind.
ADR-4: Stacking Rules per Modifier Kind
| Modifier | Stacking rule |
|---|---|
| HpBonus | Additive: base + perType + perInstance + sumOf(activePreset.hpBonus) |
| RangeBonus | Additive: clamped to [0, 7] |
| DirectionAdditions | Union of direction sets |
| CaptureFlags | Union (bitflags OR'd) |
| PromotionOverride | Precedence wins: perInstance > perType > presetDefault |
| DamageResistance | Multiplicative: finalDamage = amount * ∏(1 - resistance_i), clamped to ≥ 0 |
ADR-5: Precedence (Source Priority)
per-instance (profile) > per-type (profile) > preset > engine base
- For additive/union stacking: all sources contribute.
- For override-style (PromotionOverride): higher-priority source wins.
ADR-6: Legality Validator Checklist
On each profile apply / swap, validate:
- Every side has ≥1 king on board AFTER modifiers applied.
- King pieces MUST NOT carry
Invulnerabilitymodifier (hard rejection). - No per-instance entries reference non-existent squares (warn, skip).
- Each side has ≥1 legal move on next turn (run fast legal-move query).
- Total facts per piece ≤ 16 attrs (prevent DoS via attr bloat).
- Error codes:
E_PROFILE_NO_KING,E_PROFILE_INVULN_KING,E_PROFILE_ORPHAN_INSTANCE,E_PROFILE_DEADLOCK,E_PROFILE_ATTR_LIMIT.
ADR-7: Single Active Profile Per Game (T1)
- One profile active at a time. Changing profile = full swap via
modifier-profile.update. - Profile composition/stacking → T2.
ADR-8: Registry Pattern for Modifier Catalog
- Each T1 modifier lives in ONE file under
packages/chess/src/modifiers/descriptors/{kind}.ts. - Registers into
MODIFIER_REGISTRYat module load (mirrorPRESET_REGISTRY,LAYOUT_REGISTRY). - Descriptor shape:
{ id, attrName, valueSchema, stackingRule, apply(session, pieceId, value), describe(value), uiForm }. - T3 custom modifiers will plug into this same registry.
Work Objectives
Core Objective
Ship a first-class "Modifier Profile" system that lets users attach rule modifiers to pieces by type OR by layout slot, save/share profiles in a library, select them at game start, hot-swap them mid-game, and inspect active modifiers during play.
Concrete Deliverables
Engine package (packages/chess/src/):
modifiers/types.ts—ModifierProfile,TypeModifier,InstanceModifier,ModifierKindId,ModifierDescriptormodifiers/registry.ts—MODIFIER_REGISTRYsingleton, registration APImodifiers/descriptors/{hp-bonus,range-bonus,direction-additions,capture-flags,promotion-override,damage-resistance}.ts— 6 descriptor implsmodifiers/apply.ts—applyProfileToSession(session, profile, layout)— seed facts at game startmodifiers/reconcile.ts—reconcileProfileSwap(session, oldProfile, newProfile)— hot-swap diff + clampmodifiers/validate.ts— legality checklistmodifiers/schema.ts— Zod schema for profile serializationmodifiers/library.ts— localStorage persistence (houserules:modifier-profiles:v1)modifiers/index.ts— barrel + side-effect registration- Extend
presets/registry.ts: addtransformMoveGenerator?,modifyMoveAttrs?hooks - Extend
schema.ts: 6 new attrs inChessAttrMap - Extend
engine.ts: calltransformMoveGeneratorchain before type's default generator; integrate profile application in lifecycle
UI package (packages/chess/src/ui/):
ModifierProfileEditor.tsx— dedicated editor modal/screen (reuses layout board primitives)ModifierPalettePanel.tsx— modifier catalog (schema-driven forms)PerTypePanel.tsx— per-type modifier listPerInstancePanel.tsx— board view + click-to-select + per-piece modifier listModifierTooltip.tsx— hover tooltip showing active modifiers with source attributionModifierPinnedPanel.tsx— click-pinned side panelRulesDrawer.tsx— add "Modifier Profiles" entry- Extend
Lobby.tsx— profile picker alongside layout picker - Extend
GameView.tsx— wire tooltip + pinned panel
Server package (packages/server/src/):
- Extend
protocol.ts:ModifierProfileSchema,ProfileRequestSchema,modifier-profile.updatemessage,ModifierProfileUpdatedPayload, new error codes - Extend
rooms.ts:Room.profile?: ModifierProfilefield - Extend
game-session.ts: accept optional profile - Extend
broadcast.ts: resolve + validate profile on room-create; handlemodifier-profile.updatemessage (validate + broadcast or NACK) - New
profile-validator.ts— runs legality checklist
Net package (packages/chess/src/net/):
- Extend
types.ts:ModifierProfileWire,TypeModifierWire,InstanceModifierWire
Docs:
docs/adr/modifier-profiles.md— ADR (this plan's architecture section formalized)docs/user/modifier-profiles.md— user-facing guide
E2E (packages/chess/e2e/):
modifier-profiles.spec.ts— ~18 scenarios
Definition of Done
bun run checkgreen (typecheck + lint + vitest)- All 6 modifier descriptors pass unit tests including stacking + precedence
modifier-profile.updateWS round-trip tested server-side (happy + illegal + rollback)- Hot-swap reconciliation idempotent (same inputs → same state)
- Editor accessible from rules drawer, reachable in ≤2 clicks
- Hover tooltip shows modifiers within 200ms; pinned panel updates within 500ms on hot-swap
- Full e2e vertical slice passes in Playwright
- ADR + user docs published
- Profile JSON size ≤ 8KB (fits URL share)
Must Have
- All 6 T1 modifier categories working end-to-end.
- Per-type AND per-instance (Option B layout-slot keying) both functional.
- Hot-swap at turn boundary with server-authoritative validation.
- In-play inspection with source attribution (per-instance/per-type/preset/base).
- Reuse of layout-editor board primitives — NO duplicate board component.
- Shared engine code path for client move preview + server validation (no drift).
Must NOT Have (Guardrails — from Metis)
- ❌ Custom DSL / scripting for user-authored modifiers (T3)
- ❌ Cross-piece aura effects (T3)
- ❌ Multiple profiles stacking (post-T3)
- ❌ Copy/paste modifiers between pieces in editor (T2)
- ❌ Visual diff of "modified" vs base pieces (T2)
- ❌ Conflict resolution UI (T2 — show inline validator error only in T1)
- ❌ Undo/redo inside editor beyond native text inputs (T2)
- ❌ Mid-turn hot-swap (T1: turn-boundary-only)
- ❌ New piece TYPES via modifiers (modifiers modify, they don't invent)
- ❌ Breaking changes to existing WS messages (all additions are NEW messages)
- ❌ Invulnerability on kings (hard-rejected by validator)
- ❌ Client computing effective modifiers differently from server (shared code path mandatory)
- ❌ Duplicating the board editor component (reuse layout-editor primitives)
- ❌ Any acceptance criterion requiring human visual confirmation
- ❌ Monster commits combining engine + UI + tests
Must NOT Have (AI Slop Patterns)
- ❌
as any,@ts-ignore, or bypassing strict TS - ❌ Generic names like
data,result,item,temp - ❌ Empty catch blocks
- ❌
console.login production code - ❌ Over-commenting (don't narrate obvious code)
- ❌ Premature abstraction (keep descriptors concrete until 3+ share shape)
- ❌ Duplicate validation logic across client/server (server is authority; client mirrors)
Verification Strategy (MANDATORY)
ZERO HUMAN INTERVENTION — ALL verification is agent-executed.
Test Decision
- Infrastructure exists: YES (vitest + Playwright, established patterns)
- Automated tests: TDD — RED failing test first, GREEN minimal impl, REFACTOR
- Framework:
bun run test(vitest) for unit/integration,bunx playwright testfor e2e - Pattern: Mirror
piece-hp.test.ts,layout-library.test.ts,layouts.spec.ts
QA Policy
Every task MUST include agent-executed QA scenarios. Evidence saved to .sisyphus/evidence/piece-modifiers/task-{N}-{slug}.{ext}.
- Engine/data:
bun run test {path}— assertion-based - Server:
bun run test {server-path}+ WebSocket harness - Frontend/UI: Playwright from repo root (
bunx playwright test e2e/modifier-profiles.spec.ts) - Integration: E2E full-flow scenarios
Execution Strategy
Parallel Execution Waves
Wave 1 (Foundation - start immediately):
└── T1: Architecture ADR document (single task — blocks everything)
Wave 2 (Schema scaffolding - PARALLEL after T1):
├── T2: Add 6 ChessAttrMap entries
├── T3: Add transformMoveGenerator + modifyMoveAttrs preset hooks
├── T4: Modifier types (ModifierProfile, descriptor types)
└── T5: Modifier registry + barrel
Wave 3 (Descriptors + schema - PARALLEL after Wave 2):
├── T6: HP-bonus descriptor + tests
├── T7: Range-bonus descriptor + tests
├── T8: Direction-additions descriptor + tests
├── T9: Capture-flags descriptor + tests
├── T10: Promotion-override descriptor + tests
├── T11: Damage-resistance descriptor + tests
├── T12: Zod schema + roundtrip tests
└── T13: Library persistence
Wave 4 (Integration - PARALLEL after Wave 3):
├── T14: applyProfileToSession (game start)
├── T15: reconcileProfileSwap (hot-swap)
├── T16: Legality validator
├── T17: Server protocol extensions (schemas + error codes)
└── T18: UI editor shell + rules drawer entry
Wave 5 (Server + UI core - PARALLEL after Wave 4):
├── T19: Server room-create accepts profile
├── T20: Server modifier-profile.update WS handler
├── T21: UI per-type panel
├── T22: UI per-instance panel (board picker)
└── T23: UI save/load library + URL share
Wave 6 (In-play integration - PARALLEL after Wave 5):
├── T24: Hover tooltip inspection
├── T25: Pinned side panel
└── T26: Lobby profile picker integration
Wave 7 (End-to-end verification - PARALLEL):
├── T27: Full e2e Playwright suite
└── T28: ADR + user docs
Wave FINAL (after ALL tasks — 4 parallel reviews, then user okay):
├── F1: Plan compliance audit (oracle)
├── F2: Code quality review (unspecified-high)
├── F3: Real manual QA (unspecified-high)
└── F4: Scope fidelity check (deep)
Dependency Matrix
| Task | Depends On | Blocks |
|---|---|---|
| 1 | — | 2-28 |
| 2 | 1 | 6-11, 14 |
| 3 | 1 | 6-11, 14 |
| 4 | 1 | 5, 6-11, 12 |
| 5 | 4 | 6-11, 14 |
| 6 | 2, 3, 5 | 14, 21, 22, 24 |
| 7 | 2, 3, 5 | 14, 21, 22, 24 |
| 8 | 2, 3, 5 | 14, 21, 22, 24 |
| 9 | 2, 3, 5 | 14, 21, 22, 24 |
| 10 | 2, 3, 5 | 14, 21, 22, 24 |
| 11 | 2, 3, 5 | 14, 21, 22, 24 |
| 12 | 4 | 13, 17, 19 |
| 13 | 12 | 23 |
| 14 | 6-11, 16 | 15, 19 |
| 15 | 14 | 20 |
| 16 | 12 | 14, 19, 20 |
| 17 | 12, 16 | 19, 20 |
| 18 | 5 | 21, 22, 23 |
| 19 | 14, 17 | 26, 27 |
| 20 | 15, 17 | 24, 25, 27 |
| 21 | 6-11, 18 | 26, 27 |
| 22 | 6-11, 18 | 26, 27 |
| 23 | 13, 18 | 26, 27 |
| 24 | 6-11, 20 | 25, 27 |
| 25 | 24 | 27 |
| 26 | 19, 21, 22, 23 | 27 |
| 27 | 19-26 | 28 |
| 28 | 1, 27 | F1 |
Agent Dispatch Summary
| Wave | Tasks | Categories |
|---|---|---|
| 1 | T1 (ADR) | ultrabrain |
| 2 | T2-T5 | T2: quick, T3: deep, T4: unspecified-low, T5: deep |
| 3 | T6-T13 | T6-T11: unspecified-high, T12: unspecified-low, T13: unspecified-low |
| 4 | T14-T18 | T14-T16: deep, T17: unspecified-low, T18: visual-engineering |
| 5 | T19-T23 | T19-T20: deep, T21-T22: visual-engineering, T23: unspecified-low |
| 6 | T24-T26 | T24-T25: visual-engineering, T26: unspecified-low |
| 7 | T27-T28 | T27: unspecified-high (+ playwright skill), T28: writing |
| FINAL | F1-F4 | F1: oracle, F2: unspecified-high, F3: unspecified-high, F4: deep |
TODOs
-
1. Architecture ADR document
What to do:
- Create
docs/adr/modifier-profiles.mdformalizing ADR-1 through ADR-8 from the plan's Architecture Decisions section. - Include: hook contract (WRAP, pseudo-legal), per-instance keying (Option B layout-slot bound), hot-swap semantics (turn-boundary), stacking rules table, precedence chain, legality validator checklist, single-profile constraint, registry pattern choice.
- Add a "Rejected Alternatives" section (REPLACE semantics, Option A/C keying, mid-turn swap) with rationale.
Must NOT do: Don't write code in this task. Don't design UI. ADR is a pure decision log.
Recommended Agent Profile:
- Category:
ultrabrain— foundational architectural decisions - Skills: [
repo-analysis] — read existing ADR patterns if any
Parallelization: Wave 1 (solo). Blocks: ALL other tasks. Blocked By: None.
References:
.sisyphus/plans/piece-modifiers.md§ "Architecture Decisions (ADR)" — source of truth.sisyphus/plans/starting-layouts.md— prior plan format referencepackages/chess/src/presets/piece-hp.ts— reference preset pattern
Acceptance Criteria:
- File
docs/adr/modifier-profiles.mdexists ls docs/adr/modifier-profiles.mdreturns file (verify via Bash)- Contains all 8 ADR decisions, each with: Decision statement, Rationale, Rejected Alternatives
- Stacking rules table covers all 6 T1 modifier kinds
QA Scenarios:
Scenario: ADR file readable and contains all sections Tool: Bash Steps: 1. Run: grep -c "^## ADR-" docs/adr/modifier-profiles.md 2. Assert output: 8 3. Run: grep -c "^| HpBonus\|^| RangeBonus\|^| DirectionAdditions\|^| CaptureFlags\|^| PromotionOverride\|^| DamageResistance" docs/adr/modifier-profiles.md 4. Assert output: ≥6 (stacking table rows) Evidence: .sisyphus/evidence/piece-modifiers/task-1-adr-structure.txtCommit: YES (solo) —
docs(adr): modifier-profiles architecture decisions- Files:
docs/adr/modifier-profiles.md - Pre-commit:
bun run check
- Create
-
2. Add 6 modifier attrs to ChessAttrMap
What to do:
- Edit
packages/chess/src/schema.ts - Add 6 new optional attribute types to
ChessAttrMap:HpBonus: numberRangeBonus: numberDirectionAdditions: readonly Direction[](use existingDirectiontype)CaptureFlags: number(bitflags:CAN_CAPTURE_OWN=1,CANNOT_BE_CAPTURED=2,EN_PASSANT=4)PromotionOverride: PieceType | "disabled"DamageResistance: number(0-1 range, multiplicative)
- Export capture-flag constants from
schema.ts.
Must NOT do: Don't modify core attrs (PieceType, Color, Position, HasMoved). Don't touch engine.ts — types only.
Recommended Agent Profile:
- Category:
quick— mechanical type additions - Skills: []
Parallelization: Wave 2. Blocks: 6-11, 14. Blocked By: 1.
References:
packages/chess/src/schema.ts— current ChessAttrMap shapepackages/chess/src/presets/piece-hp.ts:13-15— how Hp attr was added (same pattern)docs/adr/modifier-profiles.md§ ADR-4 — stacking rules dictate value shapes
Acceptance Criteria:
bun run checkpasses (typecheck green)grep "HpBonus\|RangeBonus\|DirectionAdditions\|CaptureFlags\|PromotionOverride\|DamageResistance" packages/chess/src/schema.ts→ 6 matches
QA Scenarios:
Scenario: Types added without breaking existing build Tool: Bash Steps: 1. Run: bun run check 2. Assert: exit code 0, output contains "Tests" and no TS errors Evidence: .sisyphus/evidence/piece-modifiers/task-2-typecheck.txtCommit: YES —
feat(engine): add 6 modifier attrs to ChessAttrMap- Files:
packages/chess/src/schema.ts - Pre-commit:
bun run check
- Edit
-
3. Add transformMoveGenerator + modifyMoveAttrs preset hooks
What to do:
- Extend
PresetDefinpackages/chess/src/presets/registry.ts:transformMoveGenerator?: ( engine: ChessEngine, pieceId: EntityId, prev: MoveGenerator ) => MoveGenerator; modifyMoveAttrs?: ( engine: ChessEngine, pieceId: EntityId ) => { rangeBonus?: number; directionAdditions?: readonly Direction[] }; - Update
ChessEngine.getAllLegalMoves()inpackages/chess/src/engine.ts:- After resolving piece type's base generator, fold through all active presets'
transformMoveGenerator(reduce:acc, preset => preset.transformMoveGenerator?.(engine, pieceId, acc) ?? acc). - Existing
getExtraMoves/filterMovescontinue to run downstream. - Self-check filter MUST still run after all transformations.
- After resolving piece type's base generator, fold through all active presets'
- Add unit tests in
packages/chess/src/presets/transform-hook.test.ts:- Noop preset: legal moves unchanged.
- Wrapping preset: adds backward move to pawn; base forward moves still present.
- Two wrapping presets compose: second receives output of first.
- Self-check filter still runs: king-in-check blocks non-escaping moves even if transform added them.
Must NOT do: Don't add REPLACE semantics. Don't bypass self-check filter. Don't change existing hook signatures.
Recommended Agent Profile:
- Category:
deep— contract design, subtle composition semantics - Skills: [
code-search]
Parallelization: Wave 2. Blocks: 6-11, 14. Blocked By: 1.
References:
packages/chess/src/presets/registry.ts— current PresetDef shapepackages/chess/src/engine.ts:getAllLegalMoves— where to integratedocs/adr/modifier-profiles.md§ ADR-1 — contract spec
Acceptance Criteria:
bun run test packages/chess/src/presets/transform-hook.test.ts→ all tests passbun run checkgreen- Existing preset tests unchanged (no regressions)
QA Scenarios:
Scenario: Noop transform preserves legal moves Tool: Bash Steps: 1. Run: bun run test packages/chess/src/presets/transform-hook.test.ts 2. Assert: exit 0, "noop preset preserves legal moves" test passes Evidence: .sisyphus/evidence/piece-modifiers/task-3-noop.txt Scenario: Self-check filter still applies after transform Tool: Bash Steps: 1. Same test file — scenario "transform cannot bypass self-check" 2. Assert: king-in-check with transformed pawn → pawn move blocked Evidence: .sisyphus/evidence/piece-modifiers/task-3-selfcheck.txtCommit: YES —
feat(engine): add transformMoveGenerator + modifyMoveAttrs preset hooks- Files:
packages/chess/src/presets/registry.ts,packages/chess/src/engine.ts,packages/chess/src/presets/transform-hook.test.ts - Pre-commit:
bun run check
- Extend
-
4. Modifier profile types
What to do:
- Create
packages/chess/src/modifiers/types.ts:export type ModifierKindId = "hp-bonus" | "range-bonus" | "direction-additions" | "capture-flags" | "promotion-override" | "damage-resistance"; export interface TypeModifier { readonly kind: ModifierKindId; readonly pieceType: PieceType; readonly color: PieceColor; readonly value: unknown; } export interface InstanceModifier { readonly kind: ModifierKindId; readonly square: Square; readonly value: unknown; } export interface ModifierProfile { readonly id: string; readonly name: string; readonly description: string; readonly layoutId?: string; // Optional - only needed if per-instance used readonly perType: readonly TypeModifier[]; readonly perInstance: readonly InstanceModifier[]; readonly version: 1; readonly source: "premade" | "custom"; } export interface ModifierDescriptor<V = unknown> { readonly id: ModifierKindId; readonly attrName: keyof ChessAttrMap; readonly label: string; readonly valueSchema: ZodSchema<V>; readonly stackingRule: "additive" | "union" | "multiplicative" | "priority-wins"; readonly apply: (session: Session, pieceId: EntityId, value: V, sumOfOthers: V | null) => void; readonly describe: (value: V) => string; readonly uiForm: "number" | "direction-set" | "capture-flags" | "promotion-target" | "percentage"; }
Must NOT do: Don't implement descriptors yet (T6-T11). Don't write schema here (T12). Types only.
Recommended Agent Profile:
- Category:
unspecified-low— type definitions - Skills: []
Parallelization: Wave 2. Blocks: 5, 6-11, 12. Blocked By: 1.
References:
packages/chess/src/layouts/types.ts— parallel structure for referencepackages/chess/src/schema.ts— ChessAttrMap import targetdocs/adr/modifier-profiles.md§ ADR-2, ADR-8
Acceptance Criteria:
- File
packages/chess/src/modifiers/types.tsexists bun run checkgreen- Types export:
ModifierKindId,TypeModifier,InstanceModifier,ModifierProfile,ModifierDescriptor
QA Scenarios:
Scenario: Types compile and export correctly Tool: Bash Steps: 1. Run: bun run check 2. Assert: exit 0 3. Run: grep "^export " packages/chess/src/modifiers/types.ts | wc -l 4. Assert: ≥5 exports Evidence: .sisyphus/evidence/piece-modifiers/task-4-types.txtCommit: YES —
feat(engine): modifier profile types- Files:
packages/chess/src/modifiers/types.ts - Pre-commit:
bun run check
- Create
-
5. Modifier registry + barrel
What to do:
- Create
packages/chess/src/modifiers/registry.ts:class ModifierRegistry { private byId = new Map<ModifierKindId, ModifierDescriptor>(); register(d: ModifierDescriptor): void { /* throw if dup */ } get(id: ModifierKindId): ModifierDescriptor | undefined { ... } list(): readonly ModifierDescriptor[] { ... } has(id: ModifierKindId): boolean { ... } } export const MODIFIER_REGISTRY = new ModifierRegistry(); - Create
packages/chess/src/modifiers/index.ts— barrel + side-effect imports for all 6 descriptors (T6-T11 will add imports as they're written). - Unit tests
packages/chess/src/modifiers/registry.test.ts: register/get/list/duplicate-throws.
Must NOT do: Don't register any descriptors here. Don't implement descriptor logic.
Recommended Agent Profile:
- Category:
deep— mirror existing registry patterns - Skills: [
code-search]
Parallelization: Wave 2. Blocks: 6-11, 14. Blocked By: 4.
References:
packages/chess/src/presets/registry.ts— PresetRegistry patternpackages/chess/src/layouts/registry.ts— LayoutRegistry patternpackages/chess/src/presets/piece-type-registry.ts— another mirror
Acceptance Criteria:
bun run test packages/chess/src/modifiers/registry.test.tspassesMODIFIER_REGISTRY.list().length === 0initially (before T6-T11 register)
QA Scenarios:
Scenario: Registry accepts registrations and prevents duplicates Tool: Bash Steps: 1. Run: bun run test packages/chess/src/modifiers/registry.test.ts 2. Assert: exit 0, all tests pass Evidence: .sisyphus/evidence/piece-modifiers/task-5-registry.txtCommit: YES —
feat(engine): modifier registry pattern- Files:
packages/chess/src/modifiers/registry.ts,packages/chess/src/modifiers/index.ts,packages/chess/src/modifiers/registry.test.ts - Pre-commit:
bun run check
- Create
-
6. HP-bonus descriptor + tests
What to do:
- Create
packages/chess/src/modifiers/descriptors/hp-bonus.ts:id: "hp-bonus",attrName: "HpBonus",valueSchema: z.number().int().min(-10).max(10),stackingRule: "additive",uiForm: "number".apply(session, pieceId, value, sumOfOthers)→session.insert(pieceId, "HpBonus", (sumOfOthers ?? 0) + value).describe(v)→"HP ${v >= 0 ? '+' : ''}${v}".
- Side-effect register in
modifiers/index.ts. - Tests in
packages/chess/src/modifiers/descriptors/hp-bonus.test.ts:- Additive stacking (2 per-type + 1 per-instance → sum all 3).
- Precedence: no override behavior (additive means all contribute).
- Integration with piece-hp preset: total maxHp = base + HpBonus.
Must NOT do: Don't modify piece-hp preset itself. Don't handle damage here — that's orthogonal (DamageResistance).
Recommended Agent Profile:
- Category:
unspecified-high— descriptor + tests - Skills: [
code-search]
Parallelization: Wave 3 (PARALLEL with 7-11). Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
References:
packages/chess/src/presets/piece-hp.ts— HP system integrationpackages/chess/src/modifiers/types.ts— descriptor shapedocs/adr/modifier-profiles.md§ ADR-4
Acceptance Criteria:
bun run test packages/chess/src/modifiers/descriptors/hp-bonus.test.ts→ all passMODIFIER_REGISTRY.get("hp-bonus")returns descriptor- Stacking test: 3 entries of +2 → piece gets HpBonus=6
QA Scenarios:
Scenario: HP bonus stacks additively across sources Tool: Bash Steps: 1. Run: bun run test packages/chess/src/modifiers/descriptors/hp-bonus.test.ts -t "stacks additively" 2. Assert: test passes, final HpBonus = sum of all sources Evidence: .sisyphus/evidence/piece-modifiers/task-6-hp-stacking.txt Scenario: HP bonus integrates with existing HP preset Tool: Bash Steps: 1. Same test file — "integrates with piece-hp preset" 2. Assert: maxHp === baseHp + HpBonus Evidence: .sisyphus/evidence/piece-modifiers/task-6-hp-integration.txtCommit: YES —
feat(engine): hp-bonus modifier descriptor- Files:
packages/chess/src/modifiers/descriptors/hp-bonus.ts,packages/chess/src/modifiers/descriptors/hp-bonus.test.ts,packages/chess/src/modifiers/index.ts - Pre-commit:
bun run check
- Create
-
7. Range-bonus descriptor + tests
What to do:
- Create
packages/chess/src/modifiers/descriptors/range-bonus.ts:id: "range-bonus",attrName: "RangeBonus",valueSchema: z.number().int().min(-7).max(7),stackingRule: "additive"(clamped to[0, 7]),uiForm: "number".applyseedsRangeBonusfact as clamped sum.- Integration: modify
modifyMoveAttrsreturn so sliding pieces (rook/bishop/queen) see extended range. - Unit behavior: update existing sliding-move generators to consume
session.get(pieceId, "RangeBonus") ?? 0if present.
Must NOT do: Don't affect knight/king/pawn (non-sliding). Those get separate modifier (DirectionAdditions) if needed.
Recommended Agent Profile:
- Category:
unspecified-high - Skills: [
code-search]
Parallelization: Wave 3 (PARALLEL with 6, 8-11). Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
References:
packages/chess/src/presets/piece-type-registry.ts— sliding-move generators (rook, bishop, queen)packages/chess/src/modifiers/types.ts
Acceptance Criteria:
bun run test packages/chess/src/modifiers/descriptors/range-bonus.test.ts→ all pass- Rook with +1 range: legal-move list includes one extra square per ray direction (when not blocked)
- Clamping: +10 + +5 on same piece → effective 7 (max)
QA Scenarios:
Scenario: Range bonus extends sliding pieces Tool: Bash Steps: 1. Set up empty board + rook on a1 with RangeBonus=1 (test fixture, board size effectively lets us observe clamping) 2. Run test: verify rook legal moves extend by 1 square per direction Evidence: .sisyphus/evidence/piece-modifiers/task-7-range-extension.txt Scenario: Range bonus clamped to [0, 7] Tool: Bash Steps: 1. Apply +10 stacked bonus 2. Assert: effective value === 7 Evidence: .sisyphus/evidence/piece-modifiers/task-7-clamp.txtCommit: YES —
feat(engine): range-bonus modifier descriptor- Files:
packages/chess/src/modifiers/descriptors/range-bonus.ts,.test.ts,modifiers/index.ts, sliding-move generator updates - Pre-commit:
bun run check
- Create
-
8. Direction-additions descriptor + tests
What to do:
- Create
packages/chess/src/modifiers/descriptors/direction-additions.ts:id: "direction-additions",attrName: "DirectionAdditions",valueSchema: z.array(directionEnum),stackingRule: "union",uiForm: "direction-set".apply: set fact to union of arrays.- Integration: use
transformMoveGeneratorto wrap piece's base generator. Added directions generate 1-square moves (for step pieces) OR extend sliding pieces (for rook/bishop/queen) per the added direction. - Test: pawn with
["backward"]gets a backward-1 move; rook with["diagonal-ne"]gets a diagonal ray added.
Must NOT do: Don't auto-add capture semantics for new directions — that stays governed by CaptureFlags. Don't change existing generators except via the transform hook.
Recommended Agent Profile:
- Category:
unspecified-high— non-trivial integration through transform hook - Skills: [
code-search]
Parallelization: Wave 3. Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
References:
packages/chess/src/presets/pawns-move-backward.ts(if exists — search for backward pawn preset; else model from piece-type-registry)packages/chess/src/modifiers/descriptors/range-bonus.ts(T7 — similar transform hook pattern)
Acceptance Criteria:
bun run test packages/chess/src/modifiers/descriptors/direction-additions.test.tspasses- Pawn + backward direction: 1-square backward move appears in legal moves
- Union stacking: two entries each adding different directions → both present
QA Scenarios:
Scenario: Backward-pawn modifier grants backward move Tool: Bash Steps: 1. Fixture: pawn on e4 white, no blockers 2. Apply DirectionAdditions=["backward"] 3. Query legal moves → assert e3 is in the set Evidence: .sisyphus/evidence/piece-modifiers/task-8-backward-pawn.txt Scenario: Direction union stacks without duplication Tool: Bash Steps: 1. Two sources: ["backward"] and ["sideways-left"] and ["backward"] 2. Assert final set = {backward, sideways-left} (no dup backward) Evidence: .sisyphus/evidence/piece-modifiers/task-8-union.txtCommit: YES —
feat(engine): direction-additions modifier descriptor- Files:
packages/chess/src/modifiers/descriptors/direction-additions.ts,.test.ts,modifiers/index.ts - Pre-commit:
bun run check
- Create
-
9. Capture-flags descriptor + tests
What to do:
- Create
packages/chess/src/modifiers/descriptors/capture-flags.ts:id: "capture-flags",attrName: "CaptureFlags",valueSchema: z.number().int().min(0).max(7),stackingRule: "union"(bitwise OR),uiForm: "capture-flags".- Flags:
CAN_CAPTURE_OWN = 1,CANNOT_BE_CAPTURED = 2,EN_PASSANT = 4. - Integration via
filterMoves(if piece hasCANNOT_BE_CAPTURED, filter out moves that would capture it from ALL enemy pieces) — this requires engine-wide scan. - Integration via
filterMovesforCAN_CAPTURE_OWN: include moves to own-color squares.
- Tests: 3 flag scenarios × happy path, flag union stacking, toggling behavior on live board.
Must NOT do: Don't conflate with
CaptureHook(which governs damage events). CaptureFlags govern move-generation legality only.Recommended Agent Profile:
- Category:
unspecified-high— touches multiple hook points - Skills: [
code-search]
Parallelization: Wave 3. Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
References:
packages/chess/src/rules/capture.ts— existing capture logicpackages/chess/src/engine.ts:filterMoves invocation
Acceptance Criteria:
- Piece with
CANNOT_BE_CAPTURED: no enemy legal move targets it - Piece with
CAN_CAPTURE_OWN: legal moves include own-color squares - Union stacking: flags OR'd correctly
QA Scenarios:
Scenario: CANNOT_BE_CAPTURED shields piece from enemies Tool: Bash Steps: 1. Fixture: white knight b1 with flag=2; black queen in check range 2. Query black's legal moves → assert queen cannot target b1 Evidence: .sisyphus/evidence/piece-modifiers/task-9-shielded.txt Scenario: CAN_CAPTURE_OWN allows friendly fire Tool: Bash Steps: 1. White knight with flag=1, friendly pawn in move range 2. Assert knight's legal moves include the friendly square Evidence: .sisyphus/evidence/piece-modifiers/task-9-friendly-fire.txtCommit: YES —
feat(engine): capture-flags modifier descriptor- Files:
packages/chess/src/modifiers/descriptors/capture-flags.ts,.test.ts,modifiers/index.ts - Pre-commit:
bun run check
- Create
-
10. Promotion-override descriptor + tests
What to do:
- Create
packages/chess/src/modifiers/descriptors/promotion-override.ts:id: "promotion-override",attrName: "PromotionOverride",valueSchema: z.union([pieceTypeSchema, z.literal("disabled")]),stackingRule: "priority-wins"(per-instance > per-type),uiForm: "promotion-target".- Integration: hook into existing promotion logic in move-resolver. If piece has
PromotionOverride="disabled", remove the promotion step from the move. If set to a piece type, force that type.
- Tests: pawn with override="queen" always promotes to queen; override="disabled" → pawn stays pawn; per-instance beats per-type.
Must NOT do: Don't allow promotion to "king" (validator rejects — kings are singular-ish). Don't change default promotion prompt for pieces without override.
Recommended Agent Profile:
- Category:
unspecified-high - Skills: [
code-search]
Parallelization: Wave 3. Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
References:
packages/chess/src/rules/— search for "promotion" to find resolverpackages/chess/src/modifiers/types.ts
Acceptance Criteria:
- Pawn reaches rank 8 with override="bishop" → becomes bishop (no prompt)
- Pawn with override="disabled" → stays pawn on rank 8
- Per-instance override beats per-type override
QA Scenarios:
Scenario: Promotion forced to specific type Tool: Bash Steps: 1. Fixture: pawn on e7 with PromotionOverride="bishop" 2. Execute move e7→e8 3. Assert piece on e8 is bishop Evidence: .sisyphus/evidence/piece-modifiers/task-10-forced.txt Scenario: Promotion disabled keeps piece as pawn Tool: Bash Steps: 1. Fixture: pawn on e7 with PromotionOverride="disabled" 2. Move to e8 — assert piece remains pawn Evidence: .sisyphus/evidence/piece-modifiers/task-10-disabled.txtCommit: YES —
feat(engine): promotion-override modifier descriptor- Files:
packages/chess/src/modifiers/descriptors/promotion-override.ts,.test.ts,modifiers/index.ts - Pre-commit:
bun run check
- Create
-
11. Damage-resistance descriptor + tests
What to do:
- Create
packages/chess/src/modifiers/descriptors/damage-resistance.ts:id: "damage-resistance",attrName: "DamageResistance",valueSchema: z.number().min(0).max(1),stackingRule: "multiplicative"(as1 - ∏(1 - r_i)),uiForm: "percentage".- Hook into
onDamage: reduce incoming damage byamount * (1 - effectiveResistance). Clamp final damage to≥ 0.
- Tests:
- 0.5 resistance → half damage.
- Stacking: 0.5 + 0.5 →
1 - (0.5 * 0.5) = 0.75→ 25% damage taken. - Integration with HP preset: piece with HP=2 takes 1 damage (instead of 2) with 0.5 resistance.
Must NOT do: Don't reduce damage below 0 (no healing via damage). Don't make resistance value negative.
Recommended Agent Profile:
- Category:
unspecified-high— integrates with existing damage pipeline - Skills: [
code-search]
Parallelization: Wave 3. Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
References:
packages/chess/src/presets/piece-hp.ts— existingonDamagehookpackages/chess/src/engine.ts:dealDamage
Acceptance Criteria:
bun run test packages/chess/src/modifiers/descriptors/damage-resistance.test.tspasses- Multiplicative stacking formula verified
- Damage clamped to ≥ 0
QA Scenarios:
Scenario: Resistance reduces damage multiplicatively Tool: Bash Steps: 1. Piece HP=10 with resistance=0.5 2. Deal 4 damage 3. Assert HP after = 8 (damage halved) Evidence: .sisyphus/evidence/piece-modifiers/task-11-reduction.txt Scenario: Stacked resistances compound Tool: Bash Steps: 1. Two resistances 0.5 + 0.5 → effective 0.75 2. Deal 4 damage → assert HP reduced by 1 (4 * 0.25) Evidence: .sisyphus/evidence/piece-modifiers/task-11-stack.txtCommit: YES —
feat(engine): damage-resistance modifier descriptor- Files:
packages/chess/src/modifiers/descriptors/damage-resistance.ts,.test.ts,modifiers/index.ts - Pre-commit:
bun run check
- Create
-
12. Zod schema + roundtrip tests
What to do:
- Create
packages/chess/src/modifiers/schema.tswith Zod schemas for:TypeModifierSchema— validateskind∈ registry,pieceType,color,valuedispatched viaMODIFIER_REGISTRY.get(kind).valueSchema.InstanceModifierSchema— same but withsquareinstead ofpieceType.ModifierProfileSchema— full profile withversion: z.literal(1).
parse(profile)→ModifierProfile;stringify(profile)→ canonical JSON.- Tests: roundtrip (parse-stringify-parse deep-equal); 10+ invalid fixtures rejected with specific errors; unknown
kindrejected.
Must NOT do: Don't hardcode value schemas — delegate to registry. Don't version-skip (v1 only in T1; v0 migration stub only if needed for future).
Recommended Agent Profile:
- Category:
unspecified-low— Zod schema work - Skills: [
context7] — latest Zod API
Parallelization: Wave 3. Blocks: 13, 17, 19. Blocked By: 4.
References:
packages/chess/src/layouts/schema.tsor equivalent — mirror layouts patternpackages/server/src/protocol.ts— Zod usage patterns in project- Zod v3 docs:
z.discriminatedUnion,z.record
Acceptance Criteria:
bun run test packages/chess/src/modifiers/schema.test.tspasses- 10+ invalid fixtures rejected with specific error messages
- Roundtrip test:
parse(JSON.parse(JSON.stringify(profile)))deep-equal to input
QA Scenarios:
Scenario: Valid profile roundtrips losslessly Tool: Bash Steps: 1. Run: bun run test packages/chess/src/modifiers/schema.test.ts -t "roundtrip" 2. Assert: exit 0, no output diff Evidence: .sisyphus/evidence/piece-modifiers/task-12-roundtrip.txt Scenario: Invalid kind rejected Tool: Bash Steps: 1. Test fixture with kind="nonexistent" 2. Run schema test — assert rejection with `ZodError` path Evidence: .sisyphus/evidence/piece-modifiers/task-12-invalid.txtCommit: YES —
feat(engine): ModifierProfile Zod schema + roundtrip tests- Files:
packages/chess/src/modifiers/schema.ts,packages/chess/src/modifiers/schema.test.ts - Pre-commit:
bun run check
- Create
-
13. Library persistence
What to do:
- Create
packages/chess/src/modifiers/library.ts:- Key:
houserules:modifier-profiles:v1 SavedModifierProfileshape:{ profile: ModifierProfile, starred: boolean, createdAt, updatedAt }- API:
saveToLibrary,loadLibrary,deleteFromLibrary,setStarred,duplicateEntry,makeId. - Max 20 entries; oldest-non-starred eviction (mirror layout-library).
- localStorage shim for tests (mirror
layout-library.test.ts).
- Key:
- Tests:
packages/chess/src/modifiers/library.test.ts— save/load/delete/star/evict/max-starred-full (14+ tests mirroring layout-library).
Must NOT do: Don't add cloud sync. Don't support multiple library namespaces.
Recommended Agent Profile:
- Category:
unspecified-low— direct mirror of layout-library - Skills: []
Parallelization: Wave 3. Blocks: 23. Blocked By: 12.
References:
packages/chess/src/persist/layout-library.ts— direct mirrorpackages/chess/src/persist/layout-library.test.ts— mirror tests
Acceptance Criteria:
bun run test packages/chess/src/modifiers/library.test.tspasses (14+ tests)- Save → load roundtrip returns identical entries
- Eviction works (21st save removes oldest-non-starred)
QA Scenarios:
Scenario: Save and reload library entry Tool: Bash Steps: 1. Run: bun run test packages/chess/src/modifiers/library.test.ts -t "save and load" 2. Assert: entry saved, loaded identically Evidence: .sisyphus/evidence/piece-modifiers/task-13-save.txt Scenario: Eviction preserves starred entries Tool: Bash Steps: 1. Test "evict oldest non-starred" 2. Assert: starred entries survive, oldest unstarred evicted Evidence: .sisyphus/evidence/piece-modifiers/task-13-evict.txtCommit: YES —
feat(engine): modifier-profile library persistence (v1)- Files:
packages/chess/src/modifiers/library.ts,packages/chess/src/modifiers/library.test.ts - Pre-commit:
bun run check
- Create
-
14. applyProfileToSession (game start)
What to do:
- Create
packages/chess/src/modifiers/apply.ts:applyProfileToSession(session, profile, layout):- For each
perTypeentry: iterate pieces matching(pieceType, color), seed modifier facts viadescriptor.apply(session, pieceId, value, sumOfOthers). - For each
perInstanceentry: look up pieceId atsquarein layout's piece mapping; if found, seed fact; if not found, log warning (orphan handling per ADR-2). - Stacking: collect all values for each
(pieceId, kind)pair BEFORE applying, compute effective per ADR-4 stacking rules, then apply once.
- For each
- Integrate with
ChessEngineconstructor: ifoptions.profilesupplied, call after layout applied but before presets activate. - Tests
apply.test.ts: seed facts correctly for per-type + per-instance; stacking verified; orphan instances logged + skipped.
Must NOT do: Don't apply profile mid-game (that's T15). Don't modify engine's preset activation order beyond the single insertion point.
Recommended Agent Profile:
- Category:
deep— critical lifecycle integration - Skills: []
Parallelization: Wave 4. Blocks: 15, 19. Blocked By: 6-11, 16.
References:
packages/chess/src/starting-position.ts:applyLayout— similar function, same invocation sitepackages/chess/src/engine.ts— constructor + lifecycledocs/adr/modifier-profiles.md§ ADR-4, ADR-5
Acceptance Criteria:
bun run test packages/chess/src/modifiers/apply.test.tspasseseffectivePieceAttrs(pieceId)on a modified piece matches expected table- Orphan
perInstanceentry logged as warning, skipped
QA Scenarios:
Scenario: Per-type modifiers seed all matching pieces Tool: Bash Steps: 1. Profile: perType=[{kind:"hp-bonus", pieceType:"knight", color:"white", value:3}] 2. Apply to session with classic layout 3. Assert: both white knights have HpBonus=3 Evidence: .sisyphus/evidence/piece-modifiers/task-14-per-type.txt Scenario: Per-instance modifier targets specific square Tool: Bash Steps: 1. Profile: perInstance=[{kind:"range-bonus", square:"b1", value:2}] 2. Apply 3. Assert: b1 knight has RangeBonus=2; g1 knight does not Evidence: .sisyphus/evidence/piece-modifiers/task-14-per-instance.txt Scenario: Orphan instance entry handled gracefully Tool: Bash Steps: 1. Profile with perInstance square="z9" (invalid) 2. Apply — assert no throw, warning logged, other entries still applied Evidence: .sisyphus/evidence/piece-modifiers/task-14-orphan.txtCommit: YES —
feat(engine): apply profile at game start- Files:
packages/chess/src/modifiers/apply.ts,.test.ts,packages/chess/src/engine.ts(integration point) - Pre-commit:
bun run check
- Create
-
15. reconcileProfileSwap (hot-swap)
What to do:
- Create
packages/chess/src/modifiers/reconcile.ts:reconcileProfileSwap(session, oldProfile, newProfile, layout):- Compute old effective modifiers per piece.
- Compute new effective modifiers per piece.
- Diff → for each (pieceId, attr) pair, apply per-kind reconciliation rule (ADR-3):
- HpBonus change: recompute maxHp, clamp currentHp to
min(currentHp, newMax). - RangeBonus / DirectionAdditions change: overwrite fact (next move query will reflect).
- CaptureFlags change: overwrite (applies to next move).
- PromotionOverride change: overwrite (applies to future promotions).
- DamageResistance change: overwrite (applies to next damage event).
- HpBonus change: recompute maxHp, clamp currentHp to
- Idempotent:
reconcile(a, b, layout); reconcile(a, b, layout)→ same state as single call.
- Tests: 5+ scenarios including HP clamp on maxReduction, idempotency, no-op when profiles equal, full swap (all modifiers different), partial swap.
Must NOT do: Don't apply mid-turn. Don't retroactively resolve past events (in-flight attacks resolve against pre-swap state).
Recommended Agent Profile:
- Category:
deep— determinism-critical - Skills: []
Parallelization: Wave 4. Blocks: 20. Blocked By: 14.
References:
packages/chess/src/modifiers/apply.ts(T14) — source of effective computationdocs/adr/modifier-profiles.md§ ADR-3
Acceptance Criteria:
bun run test packages/chess/src/modifiers/reconcile.test.tspasses (5+ scenarios)- HP clamp rule: piece at 7/10 HP, modifier removes +3 → maxHp=7, currentHp=7 (clamped)
- Idempotency test passes
QA Scenarios:
Scenario: HP clamp on maxHp reduction Tool: Bash Steps: 1. Piece at currentHp=7, maxHp=10 (HpBonus=+3) 2. Reconcile to profile with HpBonus=0 → maxHp=7, currentHp=min(7,7)=7 Evidence: .sisyphus/evidence/piece-modifiers/task-15-hp-clamp.txt Scenario: Reconcile is idempotent Tool: Bash Steps: 1. Reconcile(old, new, layout) twice 2. Assert session state identical after 1 call and after 2 calls Evidence: .sisyphus/evidence/piece-modifiers/task-15-idempotent.txtCommit: YES —
feat(engine): hot-swap reconciliation- Files:
packages/chess/src/modifiers/reconcile.ts,.test.ts - Pre-commit:
bun run check
- Create
-
16. Legality validator
What to do:
- Create
packages/chess/src/modifiers/validate.ts:validateProfile(profile, layout, session?): ValidationResult { errors: [...], warnings: [...] }- Checklist (ADR-6):
- Both sides have ≥1 king (after applying all kingless-impact modifiers — e.g., CANNOT_BE_CAPTURED on king is fine; we only check presence).
- No king has Invulnerability / CANNOT_BE_CAPTURED (HARD — treated as error).
- No per-instance orphan refs (WARN, not error).
- Each side has ≥1 legal move on starting position (fast query — skip if session not provided).
- Attrs per piece ≤ 16 (DoS prevention).
- Error codes:
E_PROFILE_NO_KING,E_PROFILE_INVULN_KING,E_PROFILE_ORPHAN_INSTANCE,E_PROFILE_DEADLOCK,E_PROFILE_ATTR_LIMIT.
- Tests: each error code has a positive + negative fixture; warning-level orphan doesn't block save.
Must NOT do: Don't run full legal-game simulation (too slow for hot-swap). Don't over-validate — T1 catches the critical-safety subset only.
Recommended Agent Profile:
- Category:
deep— checklist correctness is critical - Skills: []
Parallelization: Wave 4. Blocks: 14, 19, 20. Blocked By: 12.
References:
packages/chess/src/layouts/validate.ts— sibling validator patterndocs/adr/modifier-profiles.md§ ADR-6
Acceptance Criteria:
bun run test packages/chess/src/modifiers/validate.test.tspasses- 5 error codes each tested positive + negative (10+ tests)
- Invuln on king → error; invuln on queen → allowed
QA Scenarios:
Scenario: Invuln on king rejected Tool: Bash Steps: 1. Profile: perInstance=[{kind:"capture-flags", square:"e1", value: CANNOT_BE_CAPTURED}] 2. Validate against classic layout 3. Assert: errors contains E_PROFILE_INVULN_KING Evidence: .sisyphus/evidence/piece-modifiers/task-16-invuln-king.txt Scenario: Orphan instance triggers warning not error Tool: Bash Steps: 1. Profile with perInstance square="z9" 2. Validate 3. Assert: warnings has entry; errors empty Evidence: .sisyphus/evidence/piece-modifiers/task-16-orphan.txtCommit: YES —
feat(engine): profile legality validator- Files:
packages/chess/src/modifiers/validate.ts,.test.ts - Pre-commit:
bun run check
- Create
-
17. Server protocol extensions
What to do:
- Extend
packages/server/src/protocol.ts:- Import
ModifierProfileSchemafrom chess package (re-export). - Add to
RoomCreatePayloadSchema:profile?: z.union([ProfileByIdSchema, ModifierProfileSchema]).optional()(by ID reference to library OR inline profile). - New message:
ModifierProfileUpdatePayloadSchema—{ roomCode, newProfile: ModifierProfile, version: number }. - New broadcast:
ModifierProfileUpdatedPayload—{ profile: ModifierProfile, version: number, appliedAt: "turn-boundary" }. - Error codes:
MODIFIER_PROFILE_INVALID,MODIFIER_PROFILE_NO_KING,MODIFIER_PROFILE_INVULN_KING,MODIFIER_PROFILE_DEADLOCK. - Wire types in
packages/chess/src/net/types.ts: mirror server.
- Import
- Update
packages/server/PROTOCOL.mdwith full docs + examples. - Tests in
protocol.test.ts: all new schemas validate valid payloads, reject invalid.
Must NOT do: Don't modify existing messages. Don't remove or rename existing fields.
Recommended Agent Profile:
- Category:
unspecified-low— additive schema work - Skills: []
Parallelization: Wave 4. Blocks: 19, 20. Blocked By: 12, 16.
References:
packages/server/src/protocol.ts— current shape (look forRoomCreatePayloadSchema,LAYOUT_INVALIDprecedent)packages/server/PROTOCOL.md— docs patternpackages/chess/src/net/types.ts— client mirror
Acceptance Criteria:
bun run test packages/server/src/protocol.test.tspasses (existing + new)bun run checkgreen- PROTOCOL.md has sections for all new messages with example JSON
QA Scenarios:
Scenario: room-create accepts inline profile Tool: Bash Steps: 1. Fixture: valid RoomCreatePayload with profile inline 2. Run schema.parse → assert no throw Evidence: .sisyphus/evidence/piece-modifiers/task-17-inline.txt Scenario: modifier-profile.update schema validates Tool: Bash Steps: 1. Fixture: valid update payload with version + profile 2. Assert parse succeeds 3. Fixture: missing version → assert rejection Evidence: .sisyphus/evidence/piece-modifiers/task-17-update.txtCommit: YES —
feat(server): modifier profile protocol schemas + error codes- Files:
packages/server/src/protocol.ts,packages/server/src/protocol.test.ts,packages/server/PROTOCOL.md,packages/chess/src/net/types.ts - Pre-commit:
bun run check
- Extend
-
18. UI editor shell + rules drawer entry
What to do:
- Create
packages/chess/src/ui/ModifierProfileEditor.tsx:- Modal-style editor (mirror
LayoutEditor.tsxlayout). - Layout: left=palette (modifier catalog), center=board preview (reuse
LayoutEditor's board primitives), right=panel switcher (per-type / per-instance / library). - State: working profile draft + isDirty flag.
- Header: name input, description input, save/cancel buttons, library drawer toggle.
- Accessible from rules drawer: add "Modifier Profiles" entry in
RulesDrawer.tsx(or equivalent) that opens the editor.
- Modal-style editor (mirror
- Add e2e fixture:
data-testid="open-modifier-editor",data-testid="modifier-editor-modal".
Must NOT do: Don't implement per-type or per-instance panels yet (T21, T22). Don't write save/load (T23). Shell only.
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
interface-design,frontend-ui-ux]
Parallelization: Wave 4. Blocks: 21, 22, 23. Blocked By: 5.
References:
packages/chess/src/ui/LayoutEditor.tsx— shell pattern to mirror- Find rules drawer:
grep -r "rules" packages/chess/src/ui --include="*.tsx"(probably in GameView or settings drawer)
Acceptance Criteria:
bunx playwright test e2e/modifier-profiles.spec.ts -g "editor opens"passes- Editor visible within 1s of click
- Esc closes editor (reuse layout-editor pattern)
QA Scenarios:
Scenario: Editor opens from rules drawer Tool: Playwright Preconditions: Game running solo mode, rules drawer available Steps: 1. Click [data-testid=open-rules-drawer] 2. Click [data-testid=open-modifier-editor] 3. Wait for [data-testid=modifier-editor-modal] visible (timeout 1s) 4. Screenshot evidence Expected Result: modal visible within 1s Evidence: .sisyphus/evidence/piece-modifiers/task-18-open.png Scenario: Esc closes editor Tool: Playwright Steps: 1. Open editor 2. Press Escape 3. Assert modal hidden within 300ms Evidence: .sisyphus/evidence/piece-modifiers/task-18-esc.pngCommit: YES —
feat(ui): modifier profile editor shell + rules drawer entry- Files:
packages/chess/src/ui/ModifierProfileEditor.tsx, rules-drawer update,e2e/modifier-profiles.spec.ts(first tests) - Pre-commit:
bun run check
- Create
-
19. Server: room-create accepts profile
What to do:
- Update
packages/server/src/rooms.ts: addRoom.profile?: ModifierProfilefield. - Update
packages/server/src/game-session.ts: accept optional profile, pass to ChessEngine options bag. - Update
packages/server/src/broadcast.ts: inhandleRoomCreate:- If payload has
profile, resolve it (inline OR by-id lookup stub — for T1 inline only). - Validate via chess
validateProfile(profile, layout). - If invalid → respond with error code; don't create room.
- If valid → create room with profile; echo in
room.createdpayload.
- If payload has
- Tests
packages/server/src/room.create-profile.test.ts: valid profile creates room; invalid king-invuln rejected withMODIFIER_PROFILE_INVULN_KING.
Must NOT do: Don't support profile-by-id lookup (T1: inline only). Don't break existing no-profile room-create.
Recommended Agent Profile:
- Category:
deep— integration + validation - Skills: []
Parallelization: Wave 5. Blocks: 26, 27. Blocked By: 14, 17.
References:
packages/server/src/broadcast.ts— existing layout resolution pattern (inhandleRoomCreate)packages/server/src/rooms.ts— Room shape (see layout field added in prior plan)
Acceptance Criteria:
bun run test packages/server/src/room.create-profile.test.tspasses- Existing room-create tests without profile still pass
- Invalid profile rejected with specific error code
QA Scenarios:
Scenario: Room created with valid profile Tool: Bash Steps: 1. Test creates room with profile in payload 2. Assert: room.profile === submitted profile; broadcast echoes Evidence: .sisyphus/evidence/piece-modifiers/task-19-create.txt Scenario: Invalid profile rejects room creation Tool: Bash Steps: 1. Test with profile granting CANNOT_BE_CAPTURED to king 2. Assert: response error code MODIFIER_PROFILE_INVULN_KING 3. Assert: no room created Evidence: .sisyphus/evidence/piece-modifiers/task-19-invuln.txtCommit: YES —
feat(server): room-create accepts profile- Files:
packages/server/src/rooms.ts,packages/server/src/game-session.ts,packages/server/src/broadcast.ts,packages/server/src/room.create-profile.test.ts - Pre-commit:
bun run check
- Update
-
20. Server: modifier-profile.update WS handler
What to do:
- Add handler in
packages/server/src/broadcast.tsformodifier-profile.updatemessage:- Parse payload via
ModifierProfileUpdatePayloadSchema. - Authenticate: only room host OR both-player consent (T1: host-only for simplicity).
- Queue until turn-boundary (ADR-3): if mid-turn, set
room.pendingProfile; apply on nextturnEnd. - On apply: validate via
validateProfile(profile, layout, session). If invalid → NACK to sender with error code; don't broadcast. - If valid: call
reconcileProfileSwap(session, oldProfile, newProfile, layout)(T15); updateroom.profile; increment version; broadcastmodifier-profile.updatedto all clients with new profile + facts delta.
- Parse payload via
- Tests
packages/server/src/ws.modifier-profile-update.test.ts: happy path, illegal-rejected-with-rollback, turn-boundary queueing, simultaneous-swap (last-write-wins).
Must NOT do: Don't broadcast invalid swaps. Don't allow clients to bypass validator. Don't support mid-turn application (queue instead).
Recommended Agent Profile:
- Category:
deep— concurrency + state consistency - Skills: [
code-search]
Parallelization: Wave 5. Blocks: 24, 25, 27. Blocked By: 15, 17.
References:
packages/server/src/broadcast.ts— existing WS message dispatcherdocs/adr/modifier-profiles.md§ ADR-3, ADR-6
Acceptance Criteria:
bun run test packages/server/src/ws.modifier-profile-update.test.tspasses (4+ scenarios)- Happy path: broadcast to all clients with new version number
- Illegal: NACK to sender only; no broadcast
- Turn-boundary queueing: mid-turn update doesn't apply immediately
QA Scenarios:
Scenario: Happy-path hot-swap broadcasts Tool: Bash Steps: 1. Test sends valid modifier-profile.update 2. Trigger turnEnd 3. Assert: all connected clients receive modifier-profile.updated with new version Evidence: .sisyphus/evidence/piece-modifiers/task-20-happy.txt Scenario: Illegal swap rollback Tool: Bash Steps: 1. Send update granting invuln to king 2. Trigger turnEnd 3. Assert: sender receives NACK with error code; no broadcast; room.profile unchanged Evidence: .sisyphus/evidence/piece-modifiers/task-20-illegal.txtCommit: YES —
feat(server): modifier-profile.update WS handler- Files:
packages/server/src/broadcast.ts,packages/server/src/ws.modifier-profile-update.test.ts - Pre-commit:
bun run check
- Add handler in
-
21. UI per-type panel
What to do:
- Create
packages/chess/src/ui/PerTypePanel.tsx:- Layout: list of
TypeModifierrows; "Add Type Modifier" button opens sub-form. - Sub-form: piece type dropdown (from PIECE_TYPE_REGISTRY), color toggle (white/black/both), modifier kind dropdown (from MODIFIER_REGISTRY), value input (schema-driven from
descriptor.uiForm). - Validation: Zod schemas per modifier kind validate value input live.
- On save: append to working profile's
perTypearray. - On delete: remove entry.
- Layout: list of
- Unit tests via Playwright: add per-type modifier → panel shows row with descriptor.describe output.
Must NOT do: Don't implement per-instance here (T22). Don't bake hardcoded modifier kinds — iterate
MODIFIER_REGISTRY.list().Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
interface-design]
Parallelization: Wave 5. Blocks: 26, 27. Blocked By: 6-11, 18.
References:
packages/chess/src/ui/ModifierProfileEditor.tsx(T18) — parent shellpackages/chess/src/ui/LayoutEditor.tsx— form patternspackages/chess/src/modifiers/registry.ts— iteration source
Acceptance Criteria:
- Adding a per-type modifier shows in row list
- All 6 modifier kinds selectable in dropdown
- Invalid value input disables Save button
QA Scenarios:
Scenario: Add per-type HP modifier Tool: Playwright Steps: 1. Open modifier editor → Per-Type tab 2. Click [data-testid=add-type-modifier] 3. Select pieceType=knight, color=white, kind=hp-bonus, value=2 4. Click [data-testid=save-type-modifier] 5. Assert: row shows "Knight (white): HP +2" 6. Screenshot evidence Evidence: .sisyphus/evidence/piece-modifiers/task-21-add.png Scenario: Invalid value disables save Tool: Playwright Steps: 1. Open add-modifier form, set range-bonus value=100 (out of range) 2. Assert [data-testid=save-type-modifier] is disabled Evidence: .sisyphus/evidence/piece-modifiers/task-21-invalid.pngCommit: YES —
feat(ui): per-type modifier panel- Files:
packages/chess/src/ui/PerTypePanel.tsx, editor wiring, e2e additions - Pre-commit:
bun run check
- Create
-
22. UI per-instance panel (board picker)
What to do:
- Create
packages/chess/src/ui/PerInstancePanel.tsx:- Layout: board preview (reuse LayoutEditor's board primitive) + click-to-select piece.
- Selected piece panel: list current per-instance modifiers for that square; "Add Modifier" form (modifier kind + value input — schema-driven).
- Per ADR-2: profile must be bound to a layout. If no layout selected yet → prompt user to pick layout first.
- On selection, show modifier list; on add, append to working profile's
perInstancearray keyed bysquare.
Must NOT do: Don't allow per-instance without a layout. Don't permit editing to produce orphans silently (show warning).
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
interface-design]
Parallelization: Wave 5. Blocks: 26, 27. Blocked By: 6-11, 18.
References:
packages/chess/src/ui/LayoutEditor.tsx:BoardPanel— board primitivepackages/chess/src/ui/PerTypePanel.tsx(T21) — parallel form patterndocs/adr/modifier-profiles.md§ ADR-2
Acceptance Criteria:
- Click piece on board → selection highlight + modifier list appears
- Adding modifier keys it by
squareinperInstancearray - Changing bound layout re-maps selection appropriately
QA Scenarios:
Scenario: Per-instance modifier attached to specific piece Tool: Playwright Steps: 1. Open modifier editor → Per-Instance tab → select layout "classic" 2. Click b1 knight on preview board 3. Add modifier: kind=range-bonus, value=1 4. Assert: profile's perInstance contains {square:"b1", kind:"range-bonus", value:1} Evidence: .sisyphus/evidence/piece-modifiers/task-22-attach.png Scenario: No-layout state prompts selection Tool: Playwright Steps: 1. Open editor, no layout bound 2. Assert Per-Instance tab shows "Select a layout first" Evidence: .sisyphus/evidence/piece-modifiers/task-22-no-layout.pngCommit: YES —
feat(ui): per-instance modifier panel- Files:
packages/chess/src/ui/PerInstancePanel.tsx, e2e additions - Pre-commit:
bun run check
- Create
-
23. UI save/load library + URL share
What to do:
- Add to
ModifierProfileEditor.tsx:- Save button: writes to library via
saveToLibrary(). - Library drawer: list saved profiles, load/rename/delete/star actions.
- Share button: encode profile as URL param
?modifierProfileId=<id>OR?modifierProfile=<base64-json>for portability. - URL reader:
Lobby.tsxreadsmodifierProfile*params and seeds editor/creates room. - Size check: enforce ≤ 8KB for URL-encoded profile; fall back to library-id share if too large.
- Save button: writes to library via
Must NOT do: Don't auto-save (explicit Save click only). Don't share as plain URL if > 8KB.
Recommended Agent Profile:
- Category:
unspecified-low— mirrors layout save/share - Skills: []
Parallelization: Wave 5. Blocks: 26, 27. Blocked By: 13, 18.
References:
packages/chess/src/ui/Lobby.tsx— layout URL-param pattern (same approach)packages/chess/src/ui/LayoutEditor.tsx— library drawer patternpackages/chess/src/modifiers/library.ts(T13)
Acceptance Criteria:
- Save → refresh page → reload from library → profile identical
- URL share encode/decode roundtrips
- Profile > 8KB falls back to library-id path
QA Scenarios:
Scenario: Save and reload profile from library Tool: Playwright Steps: 1. Create profile "My HP" with a modifier, click Save 2. Close editor, reopen, open library drawer 3. Click "My HP" entry → assert editor loads with matching state Evidence: .sisyphus/evidence/piece-modifiers/task-23-save-reload.png Scenario: URL share roundtrips Tool: Playwright Steps: 1. Save profile, click Share → copy URL 2. Open URL in new tab 3. Assert profile pre-loaded in editor Evidence: .sisyphus/evidence/piece-modifiers/task-23-share.pngCommit: YES —
feat(ui): profile library save/load + URL share- Files:
ModifierProfileEditor.tsxupdates, Lobby.tsx URL reader, e2e additions - Pre-commit:
bun run check
- Add to
-
24. Hover tooltip inspection
What to do:
- Create
packages/chess/src/ui/ModifierTooltip.tsx:- Listens to hover events on piece squares in
GameView.tsx. - Reads effective modifiers from session facts via
engine.session.get(pieceId, attr)for each registered attr. - Cross-references with active profile to determine SOURCE of each modifier (per-type vs per-instance vs preset vs base).
- Tooltip layout: piece name + color, list of active modifiers with source badge each (e.g. "HP +2 (per-type)", "Range +1 (per-instance)").
- Appear within 200ms of hover.
- Reuse existing floating-ui lib if in project (grep first).
- Listens to hover events on piece squares in
- Wire into GameView.
Must NOT do: Don't make tooltip modal / blocking. Don't require click (hover only for this task). Don't show pre-T1 modifiers (unregistered attrs).
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
interface-design,frontend-ui-ux]
Parallelization: Wave 6. Blocks: 25, 27. Blocked By: 6-11, 20.
References:
packages/chess/src/ui/GameView.tsx— hover hook targetpackages/chess/src/modifiers/registry.ts— iterate to enumerate visible attrs- Existing tooltip libs in project: grep for
floating-ui|Tooltip|@radix-ui
Acceptance Criteria:
- Hover any modified piece → tooltip appears within 200ms
- Tooltip shows all active modifiers with correct source badges
- Unmodified piece → tooltip shows no modifier rows
QA Scenarios:
Scenario: Hover modified piece shows tooltip Tool: Playwright Preconditions: Solo game started with profile granting HP +2 per-type on knights Steps: 1. Hover white knight on b1 2. Wait for tooltip within 200ms timeout 3. Assert tooltip text contains "HP +2" and "(per-type)" source 4. Screenshot Evidence: .sisyphus/evidence/piece-modifiers/task-24-hover.png Scenario: Unmodified piece has empty modifier section Tool: Playwright Steps: 1. Hover pawn (no modifiers in profile) 2. Assert tooltip shows piece name but no modifier rows Evidence: .sisyphus/evidence/piece-modifiers/task-24-empty.pngCommit: YES —
feat(ui): hover modifier tooltip- Files:
packages/chess/src/ui/ModifierTooltip.tsx,GameView.tsxintegration - Pre-commit:
bun run check
- Create
-
25. Pinned side panel
What to do:
- Create
packages/chess/src/ui/ModifierPinnedPanel.tsx:- Click a piece (not hover) → panel pins on right side.
- Content: same as tooltip but more detailed (full descriptions, source chain for stacked modifiers).
- Reactive: subscribes to session fact changes — updates on hot-swap within 500ms.
- Pinned state persists across turns until user clicks X or clicks another piece (re-pins to new piece).
- Wire into GameView.
Must NOT do: Don't include change history log (T2). Don't support multiple pinned panels simultaneously.
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
interface-design]
Parallelization: Wave 6. Blocks: 27. Blocked By: 24.
References:
packages/chess/src/ui/ModifierTooltip.tsx(T24) — content sourcepackages/chess/src/ui/GameView.tsx— panel mounting point- Existing side panels in GameView for layout reference
Acceptance Criteria:
- Click piece → panel pins within 200ms
- Hot-swap updates pinned panel within 500ms
- Clicking X closes panel
QA Scenarios:
Scenario: Click pins panel with matching content Tool: Playwright Steps: 1. Click b1 knight 2. Wait for [data-testid=modifier-pinned-panel] visible 3. Assert content matches what tooltip would show 4. Screenshot Evidence: .sisyphus/evidence/piece-modifiers/task-25-pin.png Scenario: Hot-swap updates pinned panel reactively Tool: Playwright Steps: 1. Pin panel on a modified piece (current: HP +2) 2. Trigger hot-swap via modifier-profile.update (new: HP +5) 3. Wait for panel text to update — assert within 500ms Evidence: .sisyphus/evidence/piece-modifiers/task-25-hotswap.pngCommit: YES —
feat(ui): pinned modifier inspection panel- Files:
packages/chess/src/ui/ModifierPinnedPanel.tsx,GameView.tsxintegration - Pre-commit:
bun run check
- Create
-
26. Lobby profile picker integration
What to do:
- Update
packages/chess/src/ui/Lobby.tsx:- Add Profile Picker next to Layout Picker.
- Dropdown of saved profiles from library + "None" + "Custom…" (opens editor).
- On Create Room: if profile selected, include in RoomCreatePayload.
- Reads
?modifierProfile*URL param and pre-selects.
- Show active profile on GameView header similar to LayoutBadge (new
ModifierProfileBadge).
Must NOT do: Don't auto-load "last used" profile unless user explicitly enables (T2 feature). Don't duplicate editor entry point — reuse Custom… flow.
Recommended Agent Profile:
- Category:
unspecified-low— mirror layout picker - Skills: []
Parallelization: Wave 6. Blocks: 27. Blocked By: 19, 21, 22, 23.
References:
packages/chess/src/ui/Lobby.tsx— LayoutPicker wiring patternpackages/chess/src/ui/GameView.tsx:LayoutBadge— badge pattern
Acceptance Criteria:
- Profile picker visible in lobby
- Creating room with profile: room state includes profile; GameView shows badge
- URL param pre-selects profile
QA Scenarios:
Scenario: Create room with profile Tool: Playwright Steps: 1. In lobby, select layout=classic, profile="My HP Profile" 2. Click Create Room 3. Assert GameView shows ModifierProfileBadge with profile name Evidence: .sisyphus/evidence/piece-modifiers/task-26-create.png Scenario: URL pre-select Tool: Playwright Steps: 1. Visit /lobby?modifierProfileId=<id> 2. Assert picker shows selected profile Evidence: .sisyphus/evidence/piece-modifiers/task-26-url.pngCommit: YES —
feat(ui): lobby profile picker integration- Files:
packages/chess/src/ui/Lobby.tsx,GameView.tsx(badge addition) - Pre-commit:
bun run check
- Update
-
27. Full e2e Playwright suite
What to do:
- Create
packages/chess/e2e/modifier-profiles.spec.tswith 18 scenarios:- Editor opens from rules drawer within 1s
- Esc closes editor
- Create per-type HP bonus → save → reload from library
- Create per-instance range bonus on b1 → save → reload
- URL share roundtrip (with
?modifierProfile*param) - Invalid profile (invuln on king) save blocked in editor with inline error
- Solo game creates with profile; modifier visible in hover tooltip
- Multiplayer: host creates room with profile, joiner sees same profile on join
- Hot-swap mid-game: host updates profile at turn boundary, both clients see new values
- Illegal hot-swap rejected by server with NACK; profile unchanged
- Pinned panel click + content verification
- Pinned panel updates reactively on hot-swap
- Pawn with DirectionAdditions=["backward"] can move backward in game
- Rook with RangeBonus=+1 can move 1 square further
- Knight with CaptureFlags=CANNOT_BE_CAPTURED — enemy cannot target it
- Pawn with PromotionOverride="bishop" auto-promotes to bishop (no prompt)
- Piece with DamageResistance=0.5 takes half damage (test via HP preset + attack flow)
- Profile library max-20 eviction test
- Run from repo root:
bunx playwright test e2e/modifier-profiles.spec.ts.
Must NOT do: Don't add tests for T2/T3 features. Don't modify existing layout/multiplayer specs.
Recommended Agent Profile:
- Category:
unspecified-high— thorough e2e authoring - Skills: [
playwright]
Parallelization: Wave 7. Blocks: 28. Blocked By: 19-26.
References:
packages/chess/e2e/layouts.spec.ts— 24-scenario exemplarpackages/chess/e2e/multiplayer.spec.ts— two-client test patternplaywright.config.ts(repo root)
Acceptance Criteria:
- All 18 scenarios pass:
bunx playwright test e2e/modifier-profiles.spec.ts→ 18/18 green - Runs in < 90s total
- Evidence files for each scenario saved to
.sisyphus/evidence/piece-modifiers/e2e/
QA Scenarios:
Scenario: Full e2e suite passes Tool: Bash (invoking Playwright) Steps: 1. Run: bunx playwright test e2e/modifier-profiles.spec.ts --reporter=list 2. Assert: exit 0 3. Assert: 18 passed, 0 failed Evidence: .sisyphus/evidence/piece-modifiers/task-27-e2e-results.txt Scenario: Cross-browser spot check (chromium baseline) Tool: Bash Steps: 1. Run: bunx playwright test e2e/modifier-profiles.spec.ts --project=chromium 2. Assert exit 0 Evidence: .sisyphus/evidence/piece-modifiers/task-27-chromium.txtCommit: YES —
test(e2e): modifier profiles full vertical slice- Files:
packages/chess/e2e/modifier-profiles.spec.ts - Pre-commit:
bun run check+bunx playwright test e2e/modifier-profiles.spec.ts
- Create
-
28. ADR finalization + user docs
What to do:
- Finalize
docs/adr/modifier-profiles.md(T1): add "Implementation Retrospective" section noting any deviations or discoveries. - Create
docs/user/modifier-profiles.md:- What are modifier profiles?
- How to open the editor
- Per-type vs per-instance (when to use each)
- Catalog of 6 T1 modifiers (one subsection each, with 1 example)
- Hot-swap semantics (turn-boundary, host-only in T1)
- Library save/load/share workflow
- In-play inspection (hover + pin)
- Known limitations (T1 scope; roadmap to T2/T3)
- Include screenshots from e2e evidence.
Must NOT do: Don't describe T2/T3 as if shipped. Don't promise features not in plan.
Recommended Agent Profile:
- Category:
writing - Skills: []
Parallelization: Wave 7. Blocks: F1. Blocked By: 1, 27.
References:
.sisyphus/evidence/piece-modifiers/— screenshotsdocs/adr/modifier-profiles.md(T1)docs/user/— existing user docs pattern if present
Acceptance Criteria:
- Both files exist
- User doc references all 6 modifiers with at least 1 example each
- Screenshots embedded (verify files referenced exist)
QA Scenarios:
Scenario: Docs exist and reference all modifiers Tool: Bash Steps: 1. Assert: ls docs/adr/modifier-profiles.md docs/user/modifier-profiles.md (both exist) 2. Run: grep -c "hp-bonus\|range-bonus\|direction-additions\|capture-flags\|promotion-override\|damage-resistance" docs/user/modifier-profiles.md 3. Assert output ≥ 6 Evidence: .sisyphus/evidence/piece-modifiers/task-28-docs.txt Scenario: Referenced screenshots exist Tool: Bash Steps: 1. Extract image refs from docs/user/modifier-profiles.md 2. For each, assert the file exists Evidence: .sisyphus/evidence/piece-modifiers/task-28-images.txtCommit: YES —
docs(user): modifier profiles user guide- Files:
docs/adr/modifier-profiles.md(finalization),docs/user/modifier-profiles.md - Pre-commit: none (docs only)
- Finalize
Final Verification Wave (MANDATORY — after ALL implementation tasks)
-
F1. Plan Compliance Audit —
oracleRead this plan end-to-end. For each "Must Have": verify implementation exists (read file, run tests). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check every ADR decision is reflected in code (WRAP semantics in transformMoveGenerator, layout-slot keying in per-instance, turn-boundary-only hot-swap, registry pattern, etc). Verify evidence files exist in.sisyphus/evidence/piece-modifiers/. Compare deliverables against plan. Output:Must Have [N/N] | Must NOT Have [N/N] | ADR Decisions [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT -
F2. Code Quality Review —
unspecified-highRunbun run check(typecheck + lint + vitest). Review all changed files for:as any/@ts-ignore, empty catches,console.login prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names (data/result/item/temp). Verify descriptors follow registry pattern (no hardcoded switch statements across 6+ files). Verify shared engine code between client/server (no drift). Output:Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | Pattern compliance [PASS/FAIL] | VERDICT -
F3. Real Manual QA —
unspecified-high(+playwrightskill) Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration: create profile with all 6 modifier kinds → save to library → URL-share → load in new tab → start room with profile + layout → play 3 turns using modified moves → hot-swap to different profile → verify reconciliation → hover/pin inspection throughout → game ends correctly. Test edge cases: empty profile, profile with orphan instance entries, illegal profile (king invuln), simultaneous swap, reconnect during swap. Output:Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT -
F4. Scope Fidelity Check —
deepFor each task: read "What to do", read actual diff (git log/diff). 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. Flag unaccounted changes. Verify T2/T3 features NOT smuggled into T1: no copy-paste, no diff UI, no DSL, no auras, no multi-profile stacking, no mid-turn swap. Output:Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | Scope creep [CLEAN/N issues] | VERDICT
Commit Strategy
Atomic commits per task. Each commit compiles + tests green. Use conventional commits.
docs(adr): modifier-profiles architecture decisionsfeat(engine): add 6 modifier attrs to ChessAttrMapfeat(engine): add transformMoveGenerator + modifyMoveAttrs preset hooksfeat(engine): modifier profile typesfeat(engine): modifier registry pattern6-11.feat(engine): {kind}-modifier descriptor with unit tests(×6)feat(engine): ModifierProfile Zod schema + roundtrip testsfeat(engine): modifier-profile library persistence (v1)feat(engine): apply profile at game startfeat(engine): hot-swap reconciliationfeat(engine): profile legality validatorfeat(server): modifier profile protocol schemas + error codesfeat(ui): modifier profile editor shell + rules drawer entryfeat(server): room-create accepts profilefeat(server): modifier-profile.update WS handlerfeat(ui): per-type modifier panelfeat(ui): per-instance modifier panelfeat(ui): profile library save/load + URL sharefeat(ui): hover modifier tooltipfeat(ui): pinned modifier inspection panelfeat(ui): lobby profile picker integrationtest(e2e): modifier profiles full vertical slicedocs(user): modifier profiles user guide
Success Criteria
Verification Commands
bun run check # Expected: all green
bun run test packages/chess/src/modifiers/ # Expected: N pass, 0 fail
bun run test packages/server/src/profile-validator.test.ts # Expected: all pass
bunx playwright test e2e/modifier-profiles.spec.ts # Expected: 18/18 pass
Final Checklist
- All 6 modifier descriptors registered and functional
- Per-type + per-instance both work end-to-end
- Hot-swap at turn boundary, validated server-side
- Editor reachable in ≤2 clicks from game
- Hover tooltip < 200ms, pinned panel < 500ms on update
- All QA scenario evidence files present
- ADR + user docs published
bun run checkgreen- Playwright e2e 18/18 green
- No
as any, no@ts-ignore, no pattern violations - F1-F4 final-wave all APPROVE
- User explicit "okay"