24 KiB
Modifier Profiles — T2 Polish
TL;DR
Quick Summary: UX and robustness polish on top of shipped T1 modifier profiles. Adds copy/paste between pieces, a visual "this piece is modified" indicator on the live board, inline conflict-resolution in the editor, undo/redo in the editor, proper turn-boundary queuing server-side (replaces T1's immediate-apply simplification), two-player consent model for mid-game swaps (replaces host-only), and richer source-chain attribution in the pinned inspection panel.
Deliverables:
- Editor: copy/paste modifiers, undo/redo stack, inline conflict resolution UI
- Board: subtle modifier-indicator badge on modified pieces (no hover required)
- Server: turn-boundary queue (ADR-3 done properly), both-player consent for hot-swap
- Inspection: enhanced source-chain breakdown in pinned panel
- Full e2e vertical slice
- Updated ADR + user docs
Estimated Effort: Medium (15 tasks) Parallel Execution: YES — 4 waves Critical Path: T1 (turn-queue server) → T3 (consent) → T13 (e2e)
Context
Original Request
"can we do t2 and t3? T2 first, then T3 (Recommended)"
T1 Recap (shipped)
T1 (completed) delivered the foundation: 6 built-in modifier descriptors, per-type + per-instance scope, editor UI, library persistence, URL sharing, hover tooltip, pinned panel, server-side validation, immediate-apply hot-swap, host-only authority. Full vertical slice working with 18/18 Playwright tests.
T2 Rationale
Two categories of work:
- UX polish: features users will notice immediately — copy/paste, visual indicators, undo/redo, better conflict messages.
- Robustness fixes: close the documented simplifications from T1's Implementation Retrospective — turn-boundary queue (not immediate) + two-player consent (not host-only).
Architectural Decisions (inherited from T1 ADRs)
Most T2 work doesn't need new ADR decisions. Three small extensions:
T2-ADR-1: Turn-boundary queue
- Client sends
modifier-profile.updateat any time. - Server enqueues to
room.pendingProfile. Apply runs in the server'sonAfterMovehook after next move is validated. - If pending profile is invalid when apply fires → NACK to sender, clear pending, no broadcast.
- Multiple updates before apply: last-write-wins (replace pending).
- Replaces T1 simplification where swap applied immediately on receipt.
T2-ADR-2: Two-player consent
- Host sends
modifier-profile.proposewith candidate profile. - Server broadcasts
modifier-profile.proposal-pendingto opponent with profile contents. - Opponent sends
modifier-profile.consentwith approve/reject. - If approve → apply at next turn boundary (T2-ADR-1). If reject → clear proposal, broadcast
modifier-profile.rejected. - Timeout: 60s → auto-reject.
- Replaces T1's unilateral host authority.
T2-ADR-3: Undo/redo in editor
- Editor maintains a snapshot stack of working-profile states.
- Every meaningful user action (add/delete/edit a modifier) pushes a snapshot.
- Cmd/Ctrl+Z undoes; Cmd/Ctrl+Shift+Z redoes.
- Stack capped at 50 snapshots.
- Cleared on save or cancel.
Work Objectives
Core Objective
Polish the T1 modifier-profile system with features users expect from a mature editor + close the documented T1 simplifications (immediate-apply, host-only) to ship production-grade semantics.
Concrete Deliverables
Editor UX (packages/chess/src/ui/):
ModifierProfileEditor.tsx— add undo/redo stack, toolbar with history controlsPerTypePanel.tsx— add "Copy" + "Paste" buttons on each modifier rowPerInstancePanel.tsx— add copy/paste between pieces ("copy from b1" → "paste to g1")ConflictResolutionPanel.tsx— NEW: shows validation errors inline with suggested fixes + auto-resolve options
Board UX (packages/chess/src/ui/):
ModifiedPieceIndicator.tsx— NEW: small badge/glow on modified piecesGameView.tsx+Board.tsx— wire indicator into piece rendering
Server (packages/server/src/):
broadcast.ts—handleModifierProfileUpdatenow enqueues instead of applyinggame-session.ts— newapplyPendingProfile()method called afterapplyMove()- New handlers:
handleModifierProfilePropose,handleModifierProfileConsent rooms.ts—Room.pendingProfile,Room.proposalState,Room.proposalTimeoutHandleprotocol.ts— new messages:modifier-profile.propose,modifier-profile.proposal-pending,modifier-profile.consent,modifier-profile.rejected
Inspection (packages/chess/src/ui/):
ModifierPinnedPanel.tsx— enhanced source breakdown:- Per-instance entries labeled "(from layout square X)"
- Per-type entries labeled "(applies to all {color} {type}s)"
- Preset entries labeled "(from {preset name})"
- Base values labeled "(default)"
Docs:
- Update
docs/adr/modifier-profiles.md— add T2-ADR-1, T2-ADR-2, T2-ADR-3 sections - Update
docs/user/modifier-profiles.md— document new features
E2E (packages/chess/e2e/):
- Extend
modifier-profiles.spec.ts— 8 new scenarios
Definition of Done
bun run checkgreen (typecheck + lint + vitest)- Copy/paste between pieces: adding a per-instance modifier to b1, then copying to g1, produces identical entries at both squares
- Undo/redo: 5 actions then undo 3 times then redo 2 times produces state equal to 4-action state
- Visual indicator visible on modified pieces without requiring hover
- Turn-boundary queue: profile update during mid-move doesn't apply until after opponent's move resolves
- Two-player consent: black must approve before profile change broadcasts
- Proposal timeout: 60s no-response → auto-reject
- Inline conflict resolution shows specific error + suggested fix
- Source chain in pinned panel distinguishes all 4 sources
- Playwright e2e: 18 existing + 8 new = 26 scenarios all pass
- ADR + user docs updated
Must Have
- All 8 UX/polish deliverables working end-to-end
- Turn-boundary queue replaces immediate-apply (no regression — hot-swap still works)
- Two-player consent with 60s timeout
- Editor undo/redo with 50-snapshot cap
- Shared schema between client/server (no drift)
Must NOT Have (Guardrails)
- ❌ Custom modifier authoring (T3)
- ❌ Cross-piece aura effects (T3)
- ❌ Multi-profile stacking (post-T3)
- ❌ Editor persistence across sessions (reload = fresh editor, library is separate)
- ❌ Server-side undo (client-side only — no WS messages for undo state)
- ❌ Breaking changes to T1 WS messages (all additions are NEW messages;
modifier-profile.updatebecomes an alias forpropose+ auto-approve when single-player) - ❌ Mid-turn swap (turn-boundary gate is enforced)
- ❌ Client computing effective modifiers differently from server
Must NOT Have (AI Slop Patterns)
- ❌
as any,@ts-ignore, or bypassing strict TS - ❌ Generic names (
data,result,item,temp) - ❌ Empty catch blocks
- ❌
console.login production code - ❌ Premature abstraction
Verification Strategy
Test Decision
- Infrastructure: vitest + Playwright, same as T1.
- Approach: TDD for server + engine; Playwright scenarios for UX features.
QA Policy
Every task MUST include agent-executed QA scenarios. Evidence saved to .sisyphus/evidence/modifier-profiles-t2/task-{N}-{slug}.{ext}.
Execution Strategy
Parallel Execution Waves
Wave 1 (Server robustness foundation — PARALLEL):
├── T1: T2-ADR documentation (append to existing ADR)
├── T2: Turn-boundary queue server-side
└── T3: Two-player consent protocol
Wave 2 (Editor features — PARALLEL):
├── T4: Undo/redo snapshot stack in editor
├── T5: Copy/paste modifiers between pieces
└── T6: Inline conflict resolution panel
Wave 3 (Board UX + Inspection polish — PARALLEL):
├── T7: Modified-piece indicator on live board
├── T8: Enhanced source-chain in pinned panel
└── T9: Consent UI (proposal notification + approve/reject buttons)
Wave 4 (E2E + Docs — PARALLEL):
├── T10: Playwright e2e additions (8 scenarios)
├── T11: ADR updates
└── T12: User docs updates
Wave FINAL (4 parallel reviewers):
├── F1: Plan compliance audit
├── F2: Code quality review
├── F3: Manual QA
└── F4: Scope fidelity check
Dependency Matrix
| Task | Depends On | Blocks |
|---|---|---|
| 1 | — | 2-12 |
| 2 | 1 | 3, 10, 11 |
| 3 | 2 | 9, 10, 11 |
| 4 | 1 | 10 |
| 5 | 1 | 10 |
| 6 | 1 | 10 |
| 7 | 1 | 10 |
| 8 | 1 | 10 |
| 9 | 3 | 10 |
| 10 | 2-9 | F1-F4 |
| 11 | 1, 2, 3 | F1 |
| 12 | 4-9 | F1 |
TODOs
-
1. T2-ADR documentation
What to do:
- Append 3 new sections to
docs/adr/modifier-profiles.md:## T2-ADR-1: Turn-boundary queue— full semantics, last-write-wins, NACK on invalid at apply time## T2-ADR-2: Two-player consent— proposal/consent flow, 60s timeout, auto-reject## T2-ADR-3: Editor undo/redo— snapshot stack, 50-item cap, cleared on save/cancel
Must NOT do: Don't write any code in this task.
Recommended Agent Profile:
writing— no skillsParallelization: Wave 1 (solo). Blocks: ALL. Blocked By: None.
Acceptance Criteria:
- File has 3 new T2-ADR sections
grep -c "^## T2-ADR-" docs/adr/modifier-profiles.md→ 3
Commit:
docs(adr): T2 polish architecture decisions - Append 3 new sections to
-
2. Turn-boundary queue server-side
What to do:
- Replace
handleModifierProfileUpdate's immediate-apply with queue semantics:- Add
Room.pendingProfile?: ModifierProfilefield (may already exist from T1 — re-use or add) - On receive: validate shape, store in
room.pendingProfile, ack withmodifier-profile.queued - On
applyMove()success: check pendingProfile, validate against post-move session, apply viareconcileProfileSwap, broadcastmodifier-profile.updated, clear pending - If pending profile invalid at apply time: NACK to original sender with specific error code, clear pending
- Last-write-wins: new pending replaces old pending before apply fires
- Add
Must NOT do: Don't break T1 e2e tests. Don't apply mid-move.
Recommended Agent Profile:
deep— concurrency-sensitiveParallelization: Wave 1 (PARALLEL with T1, T3). Blocks: T3, T10, T11. Blocked By: T1.
References:
packages/server/src/broadcast.ts— currenthandleModifierProfileUpdatepackages/server/src/game-session.ts—applyMove, post-move hook locationdocs/adr/modifier-profiles.md§ T2-ADR-1
Acceptance Criteria:
- Update sent during move — profile does NOT apply until opponent completes their move
- 2 rapid updates → only the last is applied
- Invalid pending at apply time → NACK to sender, no broadcast
bun run test packages/server/src/ws.modifier-profile-update.test.ts— all pass (updated)
Commit:
feat(server): turn-boundary queue for modifier profile updates - Replace
-
3. Two-player consent protocol
What to do:
- New WS messages in
protocol.ts:modifier-profile.propose(client→server) — host sends candidate profilemodifier-profile.proposal-pending(server→opponent) — notify with profile contentsmodifier-profile.consent(client→server) — opponent sends approve/rejectmodifier-profile.rejected(server→host) — proposal rejected or timed out
broadcast.ts:handleModifierProfilePropose— store proposal + start 60s timeouthandleModifierProfileConsent— on approve: promote to pendingProfile (feeds into T2 queue); on reject: clear + broadcast rejected- On timeout: auto-reject
rooms.ts:Room.proposalState?: { profile, proposedBy, timeoutHandle, proposedAt }
- Solo mode / host-only mode:
modifier-profile.updatealias auto-approves
Must NOT do: Don't change
modifier-profile.updatesemantics in solo mode.Recommended Agent Profile:
deepParallelization: Wave 1. Blocks: T9, T10, T11. Blocked By: T2.
References:
docs/adr/modifier-profiles.md§ T2-ADR-2
Acceptance Criteria:
- Propose → opponent sees pending → approve → broadcast to both
- Propose → opponent rejects → only host sees rejected, no broadcast to board
- Propose → 60s timeout → auto-reject
- Solo mode: propose = immediate apply (no consent step)
Commit:
feat(server): two-player consent for modifier profile swaps - New WS messages in
-
4. Editor undo/redo snapshot stack
What to do:
- In
ModifierProfileEditor.tsx:const [history, setHistory] = useState<ModifierProfile[]>([initialProfile])const [historyIndex, setHistoryIndex] = useState(0)const currentProfile = history[historyIndex]- Every mutation path goes through
pushSnapshot(newProfile)which truncates forward history and caps at 50 undo()/redo()adjust historyIndex- Keyboard handlers for Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z
- Header toolbar with Undo/Redo buttons (disabled when at ends of stack)
data-testid="undo-button",data-testid="redo-button"
Recommended Agent Profile:
visual-engineering, skills: [interface-design]Parallelization: Wave 2. Blocks: T10. Blocked By: T1.
Acceptance Criteria:
- Add 3 modifiers → undo 2 → profile has 1 modifier
- Undo then redo → same state
- Cap at 50 snapshots (add 51st → oldest dropped)
- Save or cancel clears history
Commit:
feat(ui): modifier editor undo/redo - In
-
5. Copy/paste modifiers between pieces
What to do:
- In
PerInstancePanel.tsx:- When a square is selected: "Copy modifiers from this square" button
- When a different square is selected after copy: "Paste modifiers here" button (disabled when clipboard empty)
- Clipboard is component-local state (not OS clipboard)
- Paste appends all clipboard entries to the target square, with the kind preserved
- In
PerTypePanel.tsx:- "Copy to clipboard" on each row
- "Paste" button appends from clipboard
data-testid="copy-modifier-b1"etc.
Recommended Agent Profile:
visual-engineering, skills: [interface-design]Parallelization: Wave 2. Blocks: T10. Blocked By: T1.
Acceptance Criteria:
- Add HP+2 to b1 → copy → paste on g1 → both have HP+2
- Paste when clipboard empty: button disabled
- Copy doesn't delete source
Commit:
feat(ui): copy/paste modifiers between pieces - In
-
6. Inline conflict resolution panel
What to do:
- New component
ConflictResolutionPanel.tsx:- Shows validation errors from
validateProfile()inline - Each error has a "Fix" button with a specific auto-resolve action:
E_PROFILE_NO_KING→ "Add king to e1" button (or suggest switching layout)E_PROFILE_INVULN_KING→ "Remove invuln from king" buttonE_PROFILE_ORPHAN_INSTANCE(warning) → "Remove orphan entry" buttonE_PROFILE_ATTR_LIMIT→ "Show affected pieces" + manual resolution
- Panel visible whenever profile has errors/warnings; hidden when clean
- Shows validation errors from
- Wire into
ModifierProfileEditor.tsx— show panel at the top when dirty
Recommended Agent Profile:
visual-engineering, skills: [interface-design]Parallelization: Wave 2. Blocks: T10. Blocked By: T1.
Acceptance Criteria:
- Create profile with invuln on king → panel shows error + Fix button
- Click Fix → error resolved
- Panel hidden when profile validates clean
Commit:
feat(ui): inline conflict resolution panel - New component
-
7. Modified-piece indicator on live board
What to do:
- New component
ModifiedPieceIndicator.tsx:- Small colored dot (or glow/border) shown on pieces that have any active modifier
- Reads
MODIFIER_REGISTRY.list()and checkssession.get(pieceId, attr)for each - If any defined → indicator visible
- CSS: absolute-positioned top-right corner of piece square, 8px dot
- Wire into
Board.tsx— render indicator alongside piece
Recommended Agent Profile:
visual-engineering, skills: [interface-design]Parallelization: Wave 3. Blocks: T10. Blocked By: T1.
Acceptance Criteria:
- Piece with modifier: indicator visible
- Piece without modifier: no indicator
- Indicator updates reactively on profile swap
Commit:
feat(ui): modified-piece indicator on board - New component
-
8. Enhanced source-chain in pinned panel
What to do:
- Update
ModifierPinnedPanel.tsx:- For each modifier row, add source label:
- If piece's attr value matches a profile.perInstance entry at its square → "(per-instance: {square})"
- Else if matches a perType entry for this pieceType+color → "(per-type: all {color} {type}s)"
- Else if some active preset declared this attr → "(preset: {preset.name})"
- Else → "(default)"
- Reads the engine's active profile + presets via a new
getModifierSource(engine, pieceId, kind)helper inpackages/chess/src/modifiers/source.ts
- For each modifier row, add source label:
Recommended Agent Profile:
visual-engineering, skills: [interface-design]Parallelization: Wave 3. Blocks: T10. Blocked By: T1.
Acceptance Criteria:
- Per-instance modifier → source labels show "per-instance: b1"
- Per-type modifier → source labels show "per-type: all white knights"
- piece-hp preset → source labels show "preset: Hit Points"
Commit:
feat(ui): enhanced modifier source chain in pinned panel - Update
-
9. Consent UI (proposal notification + buttons)
What to do:
- New component
ModifierProposalDialog.tsx:- Shows when opponent has pending proposal (
modifier-profile.proposal-pendingreceived) - Displays summary of proposed profile changes
- Approve / Reject buttons
- 60s countdown timer
- Shows when opponent has pending proposal (
- Wire into
GameView.tsxvia WS message subscription - Host sees a "Proposal sent — waiting for opponent" state after proposing
Recommended Agent Profile:
visual-engineering, skills: [interface-design]Parallelization: Wave 3. Blocks: T10. Blocked By: T3.
Acceptance Criteria:
- Host proposes → opponent sees dialog within 500ms
- Approve → profile applies at next turn boundary
- Reject → dialog closes on both sides, host sees "Proposal rejected"
- Timeout → auto-reject
Commit:
feat(ui): consent dialog for modifier profile proposals - New component
-
10. Playwright e2e additions (8 feature scenarios + solo regression guards)
What to do:
- Add 8 new T2-feature tests to
packages/chess/e2e/modifier-profiles.spec.ts:- Editor undo/redo: add 3 modifiers, undo 2, redo 1
- Copy modifier between squares (per-instance)
- Paste button disabled when clipboard empty
- Conflict panel shows error + Fix works
- Modified-piece indicator visible without hover
- Source chain distinguishes per-instance vs per-type
- (multiplayer) Proposal → approve → both clients see update
- (multiplayer) Proposal → reject → no update
- Keep and expand
packages/chess/e2e/solo-smoke.spec.ts(already exists from T1 post-audit fix). Adds ongoing regression coverage for solo play — these tests caught the T1 Rules-Drawer-Esc bug. Ensure the following scenarios remain and are extended:- play-solo button navigates to /game with fresh board
- drag pawn e2→e4 resolves
- 4-ply sequence (e4 e5 Nf3 Nc6) completes
- Rules drawer opens, closes via Esc, board remains interactive (the T1 regression guard)
- No console errors on solo game start
- Add 2 more solo-regression scenarios in T2 since T2 touches drawer/editor further:
- Rules drawer: backdrop click closes drawer (workaround path still works)
- Modifier editor: Esc closes editor without dismissing drawer underneath (nested modal ordering)
Why both?
solo-smoke.spec.tsis the canary — it exercises the baseline game flow without any modifier-profile setup.modifier-profiles.spec.tscovers feature paths. Keeping both prevents T2/T3 work from silently regressing solo play.Recommended Agent Profile:
unspecified-high, skills: [playwright]Parallelization: Wave 4. Blocks: F1-F4. Blocked By: T2-T9.
Acceptance Criteria:
- 26/26 modifier-profile tests pass (18 from T1 + 8 new)
- 7+ solo-smoke tests pass (5 original + 2 T2 additions)
- Multiplayer suite unchanged, still passes
- Total Playwright runtime < 120s
Commit:
test(e2e): T2 polish vertical slice + solo regression guards - Add 8 new T2-feature tests to
-
11. ADR updates — Implementation Retrospective T2 addendum
What to do:
- Append to
docs/adr/modifier-profiles.md:## T2 Implementation Retrospectivesection noting:- T1's immediate-apply simplification resolved (T2-ADR-1)
- T1's host-only simplification resolved (T2-ADR-2)
- Editor UX parity with modern standards (undo/redo, copy/paste)
- Any deviations found during implementation
Recommended Agent Profile:
unspecified-lowParallelization: Wave 4. Blocks: F1. Blocked By: T1, T2, T3.
Commit:
docs(adr): T2 implementation retrospective - Append to
-
12. User docs updates
What to do:
- Update
docs/user/modifier-profiles.md:- Update "Hot-Swap" section: describe proposal/consent flow (remove T1's "host-only" note)
- Add "Editor Features" section: undo/redo (keyboard shortcuts), copy/paste, conflict resolution
- Add "Board Indicators" section: modified-piece glow
- Update "In-Play Inspection" section: mention enhanced source chain
- Update "Known Limitations" section: remove T1 items, add T3 preview
Recommended Agent Profile:
unspecified-lowParallelization: Wave 4. Blocks: F1. Blocked By: T4-T9.
Commit:
docs(user): T2 modifier profile features - Update
Final Verification Wave
-
F1. Plan Compliance Audit —
oracleVerify all 12 tasks' deliverables exist. Verify T1 simplifications resolved. Check evidence files. Output:Must Have [N/N] | Must NOT Have [N/N] | ADR Decisions [3/3] | VERDICT: APPROVE/REJECT -
F2. Code Quality Review —
unspecified-highbun run check. Scan for slop patterns. Verify server-shared schemas. Output:Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass] | VERDICT -
F3. Manual QA —
unspecified-high(+playwright) Execute all 26 Playwright scenarios. Multiplayer proposal/consent roundtrip. Undo/redo combos. Output:Scenarios [N/N] | Integration [pass] | VERDICT -
F4. Scope Fidelity —
deepNo T3 features smuggled (no custom authoring, no auras, no multi-profile stacking). No breaking WS changes. Output:Tasks [N/N compliant] | Contamination [CLEAN] | VERDICT
Commit Strategy
docs(adr): T2 polish architecture decisionsfeat(server): turn-boundary queue for modifier profile updatesfeat(server): two-player consent for modifier profile swapsfeat(ui): modifier editor undo/redofeat(ui): copy/paste modifiers between piecesfeat(ui): inline conflict resolution panelfeat(ui): modified-piece indicator on boardfeat(ui): enhanced modifier source chain in pinned panelfeat(ui): consent dialog for modifier profile proposalstest(e2e): T2 polish vertical slicedocs(adr): T2 implementation retrospectivedocs(user): T2 modifier profile features
Success Criteria
bun run check # all green
bun run test packages/server/ # all pass (including updated ws tests)
bunx playwright test e2e/modifier-profiles.spec.ts # 26/26 pass
Final Checklist
- All 9 feature tasks shipped end-to-end
- T1 simplifications (immediate-apply, host-only) fully resolved
- 26 Playwright e2e scenarios green
- ADR + user docs updated
bun run checkgreen- F1-F4 all APPROVE
- User explicit "okay"