houserules/.sisyphus/plans/modifier-profiles-t3.md
Joey Yakimowich-Payne 6cddb1dcd0
chore(sisyphus): T3 Final Verification Wave — all reviewers APPROVE
F1 Plan Compliance Audit — APPROVE
  Primitives [15/15] | Tasks [17/17 top-level] | ADRs [7/7]

F2 Code Quality Review — APPROVE
  Build [PASS] | Lint [PASS] | Tests [1386 pass] | No 'as any' / '@ts-ignore'
  in non-test source | Registry-dispatch pattern throughout (no
  hardcoded kind switches)

F3 Manual QA — APPROVE
  e2e [79/79] including 18/18 custom-modifiers.spec.ts scenarios
  (plan called for 15, shipped 18). All former fixmes passing.

F4 Scope Fidelity — APPROVE
  Recursion cap [3, enforced by MAX_RECURSION_DEPTH in validate.ts]
  Primitive count cap [50, enforced by MAX_PRIMITIVE_COUNT in
  validate.ts + server Zod .max(50)]
  Per-room cap [10, enforced by CUSTOM_MODIFIER_ROOM_CAP in
  broadcast.ts]
  Per-engine custom registry [CustomModifierRegistry owned by
  ChessEngine, never global — cross-room leakage structurally
  impossible]
  T4 smuggling [CLEAN — scripted type is rejected in both
  validate.test.ts and schema.test.ts; no runtime scripted
  descriptor shipped]

T3 boulder complete.
2026-04-19 22:04:36 -06:00

27 KiB
Raw Permalink Blame History

Modifier Profiles — T3 User-Authored Custom Modifiers (DSL)

TL;DR

Quick Summary: Users can author their own modifier categories at runtime via a constrained config DSL. A custom modifier is a named, versioned, data-only descriptor composed from atomic effect primitives ("add N to attribute X", "add direction Y", "reduce damage by N%"). No code execution — purely structured data. Custom modifiers register into a per-game registry (alongside built-ins), persist in a separate library, and travel through the network via ModifierProfileSchema with full validation. Forward-designed for a T4 scripted-modifier extension.

Deliverables:

  • Effect primitives catalog + registry for user-composable atoms
  • CustomModifierDescriptor type — data-only, validated, sandboxed
  • CustomModifierEditor.tsx — visual composer (no code typing)
  • Per-room custom modifier registration via WS protocol
  • Server-side validation of custom descriptors (anti-DoS, legality)
  • Modifier Profile editor: custom modifiers appear in kind dropdown alongside built-ins
  • Per-user library for custom descriptors + sharing
  • Cross-piece aura effects (built on the same primitive model)
  • Multi-profile stacking (ordered composition)
  • Full e2e vertical slice
  • T4 design note: how scripted modifiers would plug in

Estimated Effort: Large (25 tasks, ~3-4 execution waves like T1) Parallel Execution: YES — 5 waves Critical Path: T1 (primitives ADR) → T3 (primitive registry) → T5 (custom descriptor type) → T13 (engine apply) → T19 (e2e)


Context

Original Request

"Start DSL, design for script upgrade" (T3 approach) "T2 first, then T3 (Recommended)" (execution order)

Pre-requisites

  • T1 (shipped): built-in modifier descriptors + registry + editor + server integration
  • T2 (must ship first): turn-boundary queue, two-player consent, undo/redo, copy/paste, conflict resolution

T3 Rationale

Extend the T1/T2 foundation so users can define new modifier CATEGORIES (not just configure existing ones).

Example user-authored modifier: "Shield — piece has 3 shield charges. Each incoming damage spends one charge instead of reducing HP. No charges → damage falls through." Users compose this from primitives:

  • consume-attribute-on-damage primitive with attr="ShieldCharges", consume=1, absorb=true
  • seed-attribute primitive with attr="ShieldCharges", value=3

Architecture (new ADRs)

T3-ADR-1: DSL is structured data, not code Custom modifiers are composed from a fixed catalog of ~15 effect primitives. Each primitive is a TypeScript function (shipped in the engine) that takes parameters. Users compose primitives in the UI; the resulting JSON object IS the modifier. No eval, no sandbox, no script parsing — structured validation only.

Rejected alternatives:

  • Sandboxed script runtime (QuickJS, etc.) — defers to T4. Security surface too large for T3.
  • AST-based mini-language — same complexity as sandboxed script.
  • String template interpolation — too limited.

T3-ADR-2: Effect Primitive catalog (T3 v1)

Primitives are atomic, composable, pure. Full catalog:

Primitive Parameters Effect
seed-attribute attr, value Seeds a fact on the piece at profile apply time
add-to-attribute attr, delta Adds delta to existing attr value (additive stacking)
multiply-attribute attr, factor Multiplies existing attr value
add-direction directions[] Adds to DirectionAdditions
set-capture-flag flag ORs into CaptureFlags
absorb-damage-with-attribute attr, rate Each damage point consumes rate of attr instead of HP
reflect-damage percentage Damage sends back to attacker at percentage
block-move-type moveType (capture / step / slide) Filter out moves matching criteria
add-aura radius, targetAttr, delta For each piece within radius, add delta to attr
on-turn-start primitive[] Runs contained primitives at turn start
on-capture primitive[] Runs contained primitives when this piece captures
on-damaged primitive[] Runs contained primitives when this piece takes damage
conditional condition, then-primitive[], else-primitive[] Condition-branch (e.g., "if HP < 2, do X")
modify-movement-range delta RangeBonus integration
override-promotion target PromotionOverride integration

Users compose these in a visual editor. Each primitive has a known shape, validation, UI form, and engine integration. Adding a new primitive = one file (same pattern as descriptors in T1).

T3-ADR-3: Custom modifier authoring scope

  • Per-room: custom modifiers are registered on a room-by-room basis. Registration = send full descriptor via WS at room.create or via custom-modifier.register message.
  • Per-user library: users save their custom descriptors locally (like profiles). Separate library key houserules:custom-modifiers:v1.
  • Sharing: custom descriptors travel with profiles that reference them. A profile using a custom modifier "shield-v1" embeds the full shield-v1 descriptor.
  • Versioning: custom descriptors have a version: 1 field. v2+ is a new modifier id.
  • Validation: server-side validator checks primitive catalog membership, parameter bounds, recursion depth ≤ 3 (for on-turn-start / conditional nesting), total primitive count ≤ 50 per descriptor.

T3-ADR-4: Registry extension — per-engine not per-process Built-in descriptors use module-level MODIFIER_REGISTRY. Custom descriptors use a per-engine customModifiers: Map<string, CustomModifierDescriptor>. MODIFIER_REGISTRY.get(id) transparently consults per-engine custom registry when global misses. This prevents user modifiers from leaking across rooms.

T3-ADR-5: T4 forward-design (scripted modifiers) Future scripted modifiers would plug in as a new ModifierDescriptor type:

interface ScriptedModifierDescriptor {
  type: "scripted";
  id: string;
  script: string;  // QuickJS or similar
  permissions: Permission[];
  // ... other fields
}

The engine's integration point (same apply signature) doesn't know the difference. T3's validator gets a validateCustomDescriptor branch-point that's currently type: "data" only; T4 adds type: "scripted" with separate validation + sandboxed execution.

T3-ADR-6: Multi-profile stacking (bundled with T3 since primitives enable it) Profiles can be stacked. Engine maintains activeProfiles: readonly ModifierProfile[] instead of a single profile. Stacking rules per modifier kind (from ADR-4 of T1) apply across profiles. Conflict resolution: explicit priority order set by user. Default: profiles applied in registration order.

T3-ADR-7: Aura effects (primitive add-aura) Auras are effect primitives that apply to OTHER pieces within a radius. At profile-apply time, auras create derived facts on affected pieces. Derived facts are re-computed on every move (affected pieces may change). Implementation: effectivePieceAttrs engine integration for aura-derived attrs.


Work Objectives

Core Objective

Ship user-authored custom modifier descriptors composed from an effect primitive catalog. Design the whole system so T4's scripted modifiers can plug in without redesigning.

Concrete Deliverables

Primitive catalog (packages/chess/src/modifiers/primitives/):

  • types.ts — EffectPrimitive, PrimitiveKind, primitive-specific parameter types
  • registry.ts — PRIMITIVE_REGISTRY
  • Individual primitive files (15 files):
    • seed-attribute.ts, add-to-attribute.ts, multiply-attribute.ts
    • add-direction.ts, set-capture-flag.ts
    • absorb-damage-with-attribute.ts, reflect-damage.ts
    • block-move-type.ts, modify-movement-range.ts, override-promotion.ts
    • add-aura.ts
    • on-turn-start.ts, on-capture.ts, on-damaged.ts, conditional.ts
  • index.ts — barrel + side-effect registration
  • validate.ts — descriptor-level validator (recursion depth, primitive count, parameter validation per primitive)

Custom modifier descriptor (packages/chess/src/modifiers/custom/):

  • types.ts — CustomModifierDescriptor, CustomModifierId
  • apply.ts — executes primitive list at profile apply time (replaces built-in apply for custom modifiers)
  • library.ts — persistence at houserules:custom-modifiers:v1
  • schema.ts — Zod schema for full descriptor serialization

Registry extension (packages/chess/src/modifiers/registry.ts):

  • Extend ModifierRegistryClass with customDescriptors: Map<string, CustomModifierDescriptor>
  • .registerCustom(engine, descriptor) and .getCustom(engine, id) methods
  • get(id) falls back to customDescriptors if not in built-ins

Aura engine support (packages/chess/src/modifiers/auras.ts):

  • computeAuraFacts(session) — runs all active auras, computes derived attrs, updates session
  • Hooks: invoked after every move via pseudo-preset

Multi-profile stacking (packages/chess/src/modifiers/apply.ts + reconcile.ts):

  • applyProfilesToSession(session, profiles: readonly ModifierProfile[], layout) — stack in order
  • reconcileProfilesSwap(session, oldProfiles, newProfiles, layout)

Server protocol (packages/server/src/protocol.ts):

  • New message: custom-modifier.register — attach custom descriptor to room
  • Extend ModifierProfile to include customModifiers: readonly CustomModifierDescriptor[] (embedded, not by-ref)
  • Validator: validateCustomDescriptor(descriptor) runs before allowing registration

UI (packages/chess/src/ui/):

  • CustomModifierEditor.tsx — visual primitive composer
  • PrimitivePalettePanel.tsx — catalog of 15 primitives, drag/drop or click-to-add
  • PrimitiveInspectorPanel.tsx — parameter editor for selected primitive
  • CustomModifierLibrary.tsx — library drawer for saved custom modifiers
  • Extend PerTypePanel.tsx + PerInstancePanel.tsx — "kind" dropdown shows custom modifiers alongside built-ins

Profile stacking UI (packages/chess/src/ui/Lobby.tsx):

  • Allow selecting multiple profiles (stacked, ordered)
  • Reorder via drag/drop
  • Show stacked effect preview

Docs:

  • Update docs/adr/modifier-profiles.md — add T3-ADR-1 through T3-ADR-7 sections
  • New docs/user/custom-modifiers.md — user guide for authoring custom modifiers
  • New docs/adr/T4-scripted-modifiers-design.md — forward-looking design doc

E2E (packages/chess/e2e/):

  • New custom-modifiers.spec.ts — ~15 scenarios

Definition of Done

  • bun run check green
  • All 15 primitives registered and individually tested
  • A user can create a "Shield" custom modifier in the UI using seed-attribute + absorb-damage-with-attribute primitives
  • Custom modifier saves to library, reloads after page refresh, applies to a game
  • Multi-profile stacking: 2 profiles stacked → HP bonuses add, direction additions union
  • Aura: a piece with add-aura radius=2 targetAttr=HpBonus delta=+1 gives +1 HP to all pieces within 2 squares
  • Server rejects malformed custom descriptors (recursion too deep, unknown primitive, parameter out of range)
  • Playwright e2e: 15 new scenarios pass in custom-modifiers.spec.ts
  • Existing 26 T1+T2 e2e tests still pass (no regression)
  • ADR + user docs + T4 design note published

Must Have

  • Full DSL with 15 T3 primitives covering the 6 built-in modifier behaviors + new capabilities (auras, conditionals, event hooks)
  • Custom modifiers compose in the editor alongside built-ins
  • Per-engine custom registry (no cross-room leakage)
  • Multi-profile stacking with explicit priority
  • Aura effects via add-aura primitive
  • Server-side validation of custom descriptors (anti-DoS)
  • T4 forward-design document

Must NOT Have (Guardrails)

  • ❌ Scripted modifiers (T4 — only the forward-design note lives here)
  • ❌ Sandboxed script runtime in T3
  • ❌ User-defined primitives (primitives are engine-shipped)
  • ❌ Cross-room custom modifier sharing (explicit re-registration per room)
  • ❌ Breaking changes to T1/T2 built-in modifiers
  • ❌ Breaking WS protocol changes (custom-modifier messages are additive)
  • ❌ Recursion depth > 3 in primitive nesting (DoS guard)
  • ❌ More than 50 primitives per custom descriptor
  • ❌ More than 10 custom modifiers per room (DoS guard)

Must NOT Have (AI Slop Patterns)

  • ❌ as any, @ts-ignore, or bypassing strict TS
  • ❌ Generic names (data, result, item, temp)
  • ❌ Empty catch blocks
  • ❌ console.log in production code
  • ❌ Switch statements on primitive kinds (use registry dispatch)

Verification Strategy

Test Decision

  • Infrastructure: vitest + Playwright, same as T1/T2
  • Approach: TDD per primitive, Playwright for UX

QA Policy

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


Execution Strategy

Parallel Execution Waves

Wave 1 (Foundation — PARALLEL):
├── T1: T3-ADR documentation
├── T2: Primitive types + registry class
└── T3: Custom modifier descriptor types

Wave 2 (Primitive implementations — HIGHLY PARALLEL, 15 in parallel if caps allow):
├── T4-T8:  5 state primitives (seed-attribute, add-to-attribute, multiply-attribute, add-direction, set-capture-flag)
├── T9-T11: 3 damage primitives (absorb-damage-with-attribute, reflect-damage, modify-movement-range)
├── T12-T14: 3 control primitives (block-move-type, override-promotion, add-aura)
└── T15-T18: 4 event primitives (on-turn-start, on-capture, on-damaged, conditional)

Wave 3 (Integration — PARALLEL):
├── T19: Custom descriptor validator (depth cap, parameter check)
├── T20: Custom descriptor Zod schema
├── T21: Custom descriptor library persistence
├── T22: Engine integration — applyCustomDescriptor
└── T23: Multi-profile stacking in apply/reconcile

Wave 4 (Server + UI — PARALLEL):
├── T24: Server custom-modifier.register handler + validation
├── T25: CustomModifierEditor UI (primitive composer)
├── T26: Extend PerTypePanel/PerInstancePanel for custom kinds
├── T27: Multi-profile picker in Lobby
└── T28: Aura engine integration (computeAuraFacts hook)

Wave 5 (E2E + Docs — PARALLEL):
├── T29: Playwright e2e suite (15 scenarios)
├── T30: ADR updates
├── T31: User docs (docs/user/custom-modifiers.md)
└── T32: T4 forward-design document

Wave FINAL (4 parallel reviewers):
├── F1: Plan compliance audit
├── F2: Code quality review
├── F3: Manual QA
└── F4: Scope fidelity check

Dependency Matrix (Abbreviated)

Group Depends On Blocks
T1 (ADR) — ALL
T2 (primitives registry) T1 T4-T18
T3 (custom desc types) T1 T19-T28
T4-T18 (primitives) T2 T19, T22, T25
T19 (validator) T2, T4-T18 T24
T20 (schema) T3 T24
T21 (library) T3, T20 T27
T22 (engine apply) T4-T18 T23, T28
T23 (stacking) T22 T27, T29
T24 (server) T19, T20 T29
T25 (editor) T4-T18 T26, T29
T26 (panel ext) T25 T29
T27 (lobby stack) T21, T23 T29
T28 (aura hook) T22 T29
T29 (e2e) T23-T28 F1-F4
T30-T32 (docs) T1, T29 F1

TODOs

(Abbreviated — each task follows the same 7-section format as T1/T2. Full details TBD when executing.)

  • 1. T3-ADR documentation

    What to do: Append T3-ADR-1 through T3-ADR-7 to docs/adr/modifier-profiles.md.

    Recommended Agent Profile: writing

    Parallelization: Wave 1 (solo). Blocks: ALL. Blocked By: None.

    Commit: docs(adr): T3 custom modifier DSL architecture decisions

  • 2. Primitive types + registry class

    What to do: Create packages/chess/src/modifiers/primitives/types.ts (EffectPrimitive, PrimitiveKind, parameter type families). Create primitives/registry.ts with PRIMITIVE_REGISTRY singleton.

    Recommended Agent Profile: deep

    Parallelization: Wave 1. Blocks: T4-T18. Blocked By: T1.

    Commit: feat(engine): primitive types and registry

  • 3. Custom modifier descriptor types

    What to do: Create packages/chess/src/modifiers/custom/types.ts — CustomModifierDescriptor shape with { id, name, description, version, primitives[], targetAttrs[] }.

    Recommended Agent Profile: unspecified-low

    Parallelization: Wave 1. Blocks: T19-T28. Blocked By: T1.

    Commit: feat(engine): custom modifier descriptor types

  • 4-18. 15 primitive implementations (one per primitive)

    Pattern (one task per primitive):

    • Create packages/chess/src/modifiers/primitives/{kind}.ts:
      • Export descriptor implementing EffectPrimitive<Params>
      • Zod schema for params
      • Apply function (session mutation or engine hook registration)
      • Side-effect register in primitives/index.ts
    • Create .test.ts with 3+ scenarios

    Primitives to implement:

    • T4: seed-attribute
    • T5: add-to-attribute
    • T6: multiply-attribute
    • T7: add-direction
    • T8: set-capture-flag
    • T9: absorb-damage-with-attribute
    • T10: reflect-damage
    • T11: modify-movement-range
    • T12: block-move-type
    • T13: override-promotion
    • T14: add-aura
    • T15: on-turn-start
    • T16: on-capture
    • T17: on-damaged
    • T18: conditional

    Recommended Agent Profile: unspecified-high (all)

    Parallelization: Wave 2 (PARALLEL). Blocks: T19, T22, T25. Blocked By: T2.

    Commits: feat(engine): {kind} effect primitive (×15)

  • 19. Custom descriptor validator

    What to do: packages/chess/src/modifiers/custom/validate.ts — validates a CustomModifierDescriptor:

    • Every primitive kind is in PRIMITIVE_REGISTRY
    • Primitive params satisfy primitive's Zod schema
    • Recursion depth ≤ 3 (count nesting of on-turn-start / on-capture / on-damaged / conditional)
    • Total primitive count ≤ 50
    • No circular references (descriptor referencing itself is banned)

    Recommended Agent Profile: deep

    Parallelization: Wave 3. Blocks: T24. Blocked By: T2, T4-T18.

    Commit: feat(engine): custom modifier descriptor validator

  • 20. Zod schema for custom descriptor serialization

    Recommended Agent Profile: unspecified-low

    Parallelization: Wave 3. Blocks: T24. Blocked By: T3.

    Commit: feat(engine): custom modifier Zod schema

  • 21. Custom modifier library persistence

    What to do: packages/chess/src/modifiers/custom/library.ts — localStorage at houserules:custom-modifiers:v1. Mirror T1 library API.

    Recommended Agent Profile: unspecified-low

    Parallelization: Wave 3. Blocks: T27. Blocked By: T3, T20.

    Commit: feat(engine): custom modifier library persistence

  • 22. Engine integration — applyCustomDescriptor

    What to do: packages/chess/src/modifiers/custom/apply.ts — when a profile's perType/perInstance entry references a custom kind, resolve the descriptor from per-engine registry, execute its primitive list on the target piece.

    Recommended Agent Profile: deep

    Parallelization: Wave 3. Blocks: T23, T28. Blocked By: T4-T18.

    Commit: feat(engine): apply custom modifier descriptors

  • 23. Multi-profile stacking in apply/reconcile

    What to do: Extend applyProfileToSession → applyProfilesToSession(session, profiles, layout). Extend reconcileProfileSwap → reconcileProfilesSwap(session, oldProfiles, newProfiles, layout). Stacking per ADR-4 rules apply across multiple profiles.

    Recommended Agent Profile: deep

    Parallelization: Wave 3. Blocks: T27, T29. Blocked By: T22.

    Commit: feat(engine): multi-profile stacking

  • 24. Server custom-modifier.register handler

    What to do:

    • New WS message custom-modifier.register with full descriptor payload
    • Server validates via T19's validator
    • Registers per-room (per-engine registry)
    • Rejects if > 10 custom modifiers per room
    • Broadcasts custom-modifier.registered to opponent

    Recommended Agent Profile: deep

    Parallelization: Wave 4. Blocks: T29. Blocked By: T19, T20.

    Commit: feat(server): custom-modifier.register WS handler

  • 25. CustomModifierEditor.tsx — primitive composer UI

    What to do: Visual editor with:

    • PrimitivePalettePanel — 15 primitives grouped by category
    • Primitive tree view — shows nesting for on-turn-start / conditional
    • PrimitiveInspectorPanel — parameter form per primitive, driven by primitive's Zod schema
    • Add/delete/reorder primitives
    • Save/load from library
    • Inline validation (run T19 validator live)

    Recommended Agent Profile: visual-engineering, skills: [interface-design]

    Parallelization: Wave 4. Blocks: T26, T29. Blocked By: T4-T18.

    Commit: feat(ui): custom modifier editor

  • 26. Extend PerTypePanel/PerInstancePanel for custom kinds

    What to do: Kind dropdown iterates MODIFIER_REGISTRY.list() + customRegistry.list(). Custom kinds use the primitive composer for value input (or show a simplified parameter form).

    Recommended Agent Profile: visual-engineering, skills: [interface-design]

    Parallelization: Wave 4. Blocks: T29. Blocked By: T25.

    Commit: feat(ui): custom modifiers in modifier profile panels

  • 27. Multi-profile picker in Lobby

    What to do: Lobby's profile picker becomes multi-select with ordering. Drag to reorder. Selected profiles stack in UI-visible order.

    Recommended Agent Profile: visual-engineering, skills: [interface-design]

    Parallelization: Wave 4. Blocks: T29. Blocked By: T21, T23.

    Commit: feat(ui): multi-profile stacking in lobby

  • 28. Aura engine integration

    What to do: packages/chess/src/modifiers/auras.ts — computeAuraFacts(session) runs after every move via pseudo-preset. Walks all aura-primitive-declared modifiers, finds affected pieces within radius, updates derived facts.

    Recommended Agent Profile: deep

    Parallelization: Wave 4. Blocks: T29. Blocked By: T22.

    Commit: feat(engine): aura effect computation

  • 29. Playwright e2e suite (15 scenarios)

    What to do: Create packages/chess/e2e/custom-modifiers.spec.ts:

    1. Open custom modifier editor from modifier profile editor
    2. Create "Shield" custom modifier via primitive composer
    3. Save custom modifier to library
    4. Reload page → custom modifier still in library
    5. Use custom modifier in a profile (per-type entry)
    6. Start solo game with profile using custom modifier — verify behavior
    7. Multi-profile stacking (2 profiles) — HP bonuses add
    8. Aura effect — piece within radius gets derived attr
    9. Aura updates after move (target moves out of radius → fact retracted)
    10. Server rejects custom modifier with > 50 primitives
    11. Server rejects custom modifier with > 3 recursion depth
    12. Custom modifier sharing via multiplayer — both clients see same behavior
    13. Conditional primitive works (if HP < 2, do X)
    14. on-turn-start primitive fires
    15. absorb-damage-with-attribute works (shield absorbs before HP)

    Recommended Agent Profile: unspecified-high, skills: [playwright]

    Parallelization: Wave 5. Blocks: F1-F4. Blocked By: T22-T28.

    Commit: test(e2e): custom modifier DSL vertical slice

  • 30. ADR updates — T3 Implementation Retrospective

    Recommended Agent Profile: unspecified-low

    Parallelization: Wave 5. Blocks: F1. Blocked By: T1, T22-T28.

    Commit: docs(adr): T3 implementation retrospective

  • 31. User docs: docs/user/custom-modifiers.md

    What to do: New user guide:

    • What are custom modifiers?
    • Opening the editor
    • The 15 effect primitives (one subsection each with 1 example)
    • Composing primitives (simple to complex)
    • Saving & sharing
    • Multi-profile stacking
    • Aura effects (radius, affected pieces)
    • Limitations (50 primitive cap, depth cap, 10 per room)

    Recommended Agent Profile: unspecified-low

    Commit: docs(user): custom modifier DSL user guide

  • 32. T4 forward-design document

    What to do: Create docs/adr/T4-scripted-modifiers-design.md:

    • Why T4 is deferred (security complexity)
    • Sandbox candidates evaluated (QuickJS, Duktape, custom mini-interpreter)
    • Descriptor shape extension (type: "data" | "scripted")
    • Permission system sketch
    • Validation strategy (static analysis of script before execution)
    • How T3 primitives can be migrated (scripted modifier that wraps a primitive sequence)
    • Open questions

    Recommended Agent Profile: writing

    Commit: docs(adr): T4 scripted modifiers forward-design


Final Verification Wave

  • F1. Plan Compliance Audit — oracle Verify all 15 primitives, all 32 tasks' deliverables, T3-ADR decisions reflected. No T4 scripted modifiers shipped (only design doc). Output: Primitives [15/15] | Tasks [N/N] | ADRs [7/7] | VERDICT

  • F2. Code Quality Review — unspecified-high bun run check. Scan for slop. Verify registry-dispatch pattern (no hardcoded kind switches). Output: Build [PASS] | Lint [PASS] | Tests [N pass] | VERDICT

  • F3. Manual QA — unspecified-high (+ playwright) Execute all 15 new e2e scenarios. Author a Shield custom modifier via UI, play a game with it, verify full behavior end-to-end. Output: Scenarios [N/N] | Integration [pass] | VERDICT

  • F4. Scope Fidelity — deep No scripted modifiers (only forward-design doc). No cross-room leakage. Recursion cap enforced. Primitive count cap enforced. Output: Tasks [N/N compliant] | T4 smuggling [CLEAN] | VERDICT


Commit Strategy

  1. docs(adr): T3 custom modifier DSL architecture decisions
  2. feat(engine): primitive types and registry
  3. feat(engine): custom modifier descriptor types 4-18. feat(engine): {kind} effect primitive (×15)
  4. feat(engine): custom modifier descriptor validator
  5. feat(engine): custom modifier Zod schema
  6. feat(engine): custom modifier library persistence
  7. feat(engine): apply custom modifier descriptors
  8. feat(engine): multi-profile stacking
  9. feat(server): custom-modifier.register WS handler
  10. feat(ui): custom modifier editor
  11. feat(ui): custom modifiers in modifier profile panels
  12. feat(ui): multi-profile stacking in lobby
  13. feat(engine): aura effect computation
  14. test(e2e): custom modifier DSL vertical slice
  15. docs(adr): T3 implementation retrospective
  16. docs(user): custom modifier DSL user guide
  17. docs(adr): T4 scripted modifiers forward-design

Success Criteria

bun run check                                                    # all green
bun run test packages/chess/src/modifiers/primitives/            # all 15 primitive tests pass
bun run test packages/chess/src/modifiers/custom/                # apply + validate + library pass
bunx playwright test e2e/custom-modifiers.spec.ts                # 15/15 pass
bunx playwright test e2e/modifier-profiles.spec.ts               # 26/26 pass (no regression)

Final Checklist

  • All 15 primitives registered and individually tested
  • Custom modifier editor UI functional
  • Server validates + registers custom descriptors per-room
  • Multi-profile stacking works
  • Auras work (derived facts update on move)
  • 15 Playwright e2e + 26 existing pass (41 total)
  • ADR + user docs + T4 design note published
  • bun run check green
  • F1-F4 all APPROVE
  • User explicit "okay"