feat(thressgame-100): Wave 1 partial — resolver V3 + add-to-attribute.target

Wave 1 (W1.0–W1.6) of the thressgame-100 epic — push ThressGame coverage from
27 % toward 85 %+ via resolver expressiveness. Foundation for Layer-1 rules
(self-targeting destroys, mass-mover, adjacent splash). Recipes (W1.7–W1.12)
land in subsequent commits.

Resolver V3 (param-resolver.ts) — 6 new shapes:
- {ctx-self-id: null}        → ctx.pieceId
- {ctx-self-marker-id: null} → ctx.markerId (throws when undefined)
- {add: [<resolver>, <int>]} → recursive arithmetic, MAX_SAFE_INTEGER overflow throws
- {sub: [...]}, {mul: [...]}, {mod: [...]} — same pattern; mod uses positive-modulo
  formula ((l % r) + r) % r so column-wrap recipes work for any sign of l

V3 union order locked (param-resolver-schema.ts):
[literal, $var, ctx-attr, ctx-build, ctx-self-id, ctx-self-marker-id, add, sub, mul, mod]

PrimitiveApplyContext (types.ts): added optional readonly markerId? field.
runPrimitives (triggers.ts): populates markerId in ctx for piece-entered-marker
and marker-expire events from event.markerId (single source of truth).

W1.6 — add-to-attribute.target:
- Schema gains optional target?: numberOrResolver({ min: 0 }) field
- apply() resolves target then defaults to ctx.pieceId when undefined
- Closes the long-documented adjacent-splash sharp edge — splash damage now
  expressible via target redirection instead of forcing set-piece-attr

Test surface:
- param-resolver.test.ts: +22 tests (39 total) — overflow boundary, recursive
  nesting, mixed shapes, replay determinism, ctx.markerId failure modes
- param-resolver-schema.test.ts: +19 tests (38 total) — V3 union for each helper,
  arithmetic shape parsing, ctx-self-id payload validation
- add-to-attribute.test.ts: +6 tests (10 total) — target literal, target $var,
  target omitted (backward-compat), reject string target, apply with/without target
- ParamField snapshot regenerated for add-to-attribute.target rendering

bun run check: 2983 tests pass (was 2941, +42).

Plan: .sisyphus/plans/thressgame-100.md (5 waves + cross-ref + final verification,
~73 atomic tasks locked end-to-end).
Notepads: .sisyphus/notepads/thressgame-100/

Locked architectural decisions (irrevocable across all 6 waves):
  A. Arithmetic resolver shapes — arity 2, no comparisons, no booleans
  B. Self-targeting via ctx-self-id / ctx-self-marker-id (NOT 'self' literal)
  C. add-to-attribute.target optional, defaults to ctx.pieceId
  D. Multi-turn countdowns via on-attr-expire trigger (Wave 2)
  E. Piece-pair lifecycle via PieceLink + on-piece-pair-link-broken (Wave 4)
  F. Resource accumulation on GAME_ENTITY (Wave 5)
  G. Board topology via BoardTopology attr (Wave 4)
  H. Validator V3 — superset of V2, all V2 fixtures auto-validate
  J. User-explicit overrides — no backward-compat constraint, no time/cost limit
This commit is contained in:
Joey Yakimowich-Payne 2026-04-27 13:48:28 -06:00
commit 6a38be6fc6
No known key found for this signature in database
13 changed files with 1812 additions and 10 deletions

View file

@ -0,0 +1,59 @@
# thressgame-100 — Locked Architectural Decisions
## Locked at plan-write time (irrevocable across all 6 waves)
### A. Arithmetic resolver shapes (Wave 1)
- Add `add`, `sub`, `mul`, `mod` shapes. Arity = 2. Operands = `(resolver | integer)`.
- Range-check at `apply()`-time only (validator just checks shape).
- NO comparison ops, NO boolean logic, NO conditionals inside resolver shapes.
- Union order in V3: `[literal, $var, ctx-attr, ctx-build, ctx-self-id, ctx-self-marker-id, add, sub, mul, mod]`
### B. Self-targeting (Wave 1)
- New shape `{ "ctx-self-id": null }` returns `ctx.pieceId` directly.
- New shape `{ "ctx-self-marker-id": null }` returns `ctx.markerId` (only valid inside marker triggers).
- NOT adding `"self"` literal branch to numeric schemas.
### C. `add-to-attribute` gains `target` field (Wave 1)
- Optional. Defaults to `ctx.pieceId` when omitted (backward-compatible default).
- When present, accepts `numberOrResolver()` (V3 union).
### D. Multi-turn countdowns (Wave 2)
- Generalize `MarkerLifetime` to per-piece attrs. `set-piece-attr.lifetime` already accepted.
- New trigger `on-attr-expire(target, attr)` fires on countdown-zero.
- New primitive `decrement-attr-each-turn(target, attr)` for explicit countdown control.
- New dispatcher stage 13 (after stage 12 `fireOnTurnStartHooks`), batched per turn boundary.
### E. Piece-pair lifecycle (Wave 4)
- New attr `PieceLink` (per-piece, list of EntityIds).
- New trigger `on-piece-pair-link-broken`.
- New primitives `link-pieces(a, b)`, `unlink-pieces(a, b)`.
### F. Resource accumulation (Wave 5)
- New attrs `WhiteScore`, `BlackScore` on GAME_ENTITY. Default 0.
- New primitives `add-resource(player, amount)`, `spend-resource(player, amount, then, else)`.
- New trigger `on-resource-changed(player, threshold, direction)`.
### G. Board topology (Wave 4)
- New attr `BoardTopology: "standard" | "wrap-files" | "wrap-all"` on GAME_ENTITY. Default `"standard"`.
- New primitive `set-board-topology(value)`.
- Standard topology = no regression (move-gen check is opt-in).
### H. Validator V3
- All `numberOrResolver` / `enumOrResolverFor` callsites widen to V3 union (mechanical).
- V3 is a superset of V2 (existing fixtures auto-validate clean).
### I. Test gates per wave
- `bun run check` exit 0
- Every new recipe ships with `*-real.test.ts` proving end-to-end runtime
- Every wave ships ≥1 new e2e Playwright spec
- Determinism property tests for every new primitive (N=100, byte-identical state hash)
### J. User-explicit overrides (locked)
- "No backward-compat constraint" — schemas can break. Update existing tests rather than preserving.
- "No time/cost limit" — execution may take as long as needed; reviewer cycles can iterate.
- "Just fucking get it all done" — no premature optimization, no scope reduction, ship complete waves.
### K. Coverage accounting (locked numbers)
- Effective denominator = 51 ThressGame rules (after removing 6 stubs + 8 preset-shaped from raw 65)
- Wave-end coverage: W1 27/51 (53%) → W2 37/51 (72%) → W3 45/51 (88%) → W4 50/51 (98%) → W5 51/51 (100%)
- 85% target hit at end of W3; W4-W5 are completeness-driven not target-driven

View file

@ -0,0 +1,10 @@
# thressgame-100 — Issues / Gotchas
(Empty at plan-write time. Append findings as work proceeds.)
## Format
```
## [TIMESTAMP] Wave/Task: W<N>.<M>
{description of issue, workaround, or open question}
```

View file

@ -0,0 +1,52 @@
# thressgame-100 — Inherited Wisdom
## From thressgame-coverage epic (foundation, Atlas waves 0-19)
- **Test command**: `bun run check` — NOT `bun test` (hits stale dist/)
- **TS project references**: `bunx tsc -b --force packages/chess` regenerates `dist/index.d.ts`
- **Playwright helper**: ALWAYS `.sisyphus/scripts/run-pw.sh <log> <args>`. NEVER `bunx playwright test` directly (times out agent runtime)
- **NEVER set `CI=true`** in helper — flips `reuseExistingServer: false`, collides with docker compose dev stack
- **Docker stack**: `docker-compose.dev.yml` runs server :7357 + web :5173. Verify with `docker compose -f docker-compose.dev.yml ps`
- **Test-only WS frames** in `broadcast.ts`: `__test__.activate-descriptor`, `__test__.apply-descriptor` (gated `NODE_ENV !== "production"`)
- **Dev-only debug hook**: `globalThis.__paratypeChessClient` (gated `import.meta.env.DEV`) — use from Playwright for engine introspection
- **DOM selectors**: `[data-square="e4"]`, `[data-piece="white-pawn"]`, `[data-piece-id]`, `[data-marker-kind="frozen-square"]`, `[data-testid="request-choice-modal"]`, `[data-choice-kind="..."]`
- **Marker priorities (locked)**: portal-end=1 < frozen-square=2 < mine=3 < pit=4 < death-square=5 < tornado=6 < treasure=7 < blocked=8
- **Cascade depth limit = 8** (RUNTIME_DEPTH_HARD_CAP); error code `runtime.cascade-depth-exceeded`
- **Choice stack max depth = 8**; error code `runtime.choice-depth-exceeded`
- **Mulberry32 PRNG**: deterministic; seeded via `engine.rng()` advancing `RngStream` on `GAME_ENTITY`
## From thressgame-templates epic (V2 era, prior wave)
- **`__resolverEnumValues` discriminator**: `enumOrResolverFor` attaches this directly via `Object.assign`. Survives Zod 4.x parse + `.optional()` wrapping. Read by ParamField at `_def.innerType.__resolverEnumValues`
- **Validator iteration-trigger-scope fix** at `validate.ts:325-349`: extended trigger-scope detection to recognize `for-each-*` and `random-pick`
- **JSON import in vitest**: `assert { type: "json" }` doesn't work reliably; inline descriptors as TS literals or use `fs.readFileSync(new URL(..., import.meta.url))` — the URL form fails too in vitest. Best: inline.
- **Snapshot tests regenerate** with `bunx vitest run path -u` after rendered text changes
## From the oracle's full-corpus analysis (current epic)
- **Effective denominator = 51** (65 raw - 6 stubs - 8 preset-shaped)
- **Wave 4 = highest regression risk** (topology change touches every move-gen test)
- **Wave 5 = paradigm break** (game-level mutable state) — but `GAME_ENTITY` already has mutable state (RngStream, choice timeouts), so it's a NEW pattern only in scope, not architecture
- **6 empty-stub rules in ruleHooks.js**: `pawns_with_viagra`, `estrogen`, `knee_surgery`, `pawns_learned_strength`, plus `parry`/`pacman_style` bodies that defer elsewhere
- **8 preset-shaped rules**: `dual_king`, `coregal`, `god_kings`, `early_promotion`, `proletariat`, `short_stop`, `trains_rights`, `pacman_style` — already in `RULES.md` as v2 presets
## Don'ts (carried from prior waves)
- Do NOT edit any file in `__fixtures__/parity/` (canonical descriptors)
- Do NOT use `Date.now()` (breaks replay determinism)
- Do NOT `background_cancel(all=true)` (kills tasks whose results haven't been collected)
- Do NOT add `Math.random()` (use `engine.rng()` only)
- Do NOT skip the `*-real.test.ts` for new recipes (recipe validation alone is not proof of correctness)
## [2026-04-26] W1.1-W1.5 — resolver V3
- **`PrimitiveApplyContext.markerId` did NOT exist pre-task.** Added as `readonly markerId?: EntityId | undefined` (the `| undefined` is REQUIRED with `exactOptionalPropertyTypes: true` even when the field is `?:`). Construction sites that omit it inherit `undefined` cleanly — verified against 60+ test ctx builders without a single edit needed.
- **Populated markerId in `runPrimitives` (triggers.ts) from `event.markerId`** for the two marker trigger event kinds (`piece-entered-marker`, `marker-expire`). This is the single source of truth — no other ctx-construction site needs the field today, since profile-time applies and non-marker triggers legitimately have `markerId === undefined`. The `ctx-self-marker-id` resolver throws BindingError-style at that boundary.
- **Zod 4.x recursive types via `z.lazy()`**: ArithmeticShape recursively references NumericResolverInput which references ArithmeticShape. Working pattern: declare BOTH as `z.ZodType<unknown>` typed via `z.lazy(() => z.union([...]))`. The order matters — `NumericResolverInput` is declared FIRST (referencing `ArithmeticShape` which is forward-declared via TDZ-safe `z.lazy`), then `ArithmeticShape` is defined. TypeScript's variable-not-yet-initialized warning is silenced by `z.lazy`'s deferred evaluation.
- **Helper return type widening**: `numberOrResolver()` now returns `z.ZodType<unknown>` instead of the V2 `z.ZodUnion<readonly [...]>`. The narrow union type is no longer expressible because `ArithmeticShape` is `z.ZodType<unknown>` (recursion). Consumers that care about narrow types downcast at use site — same pattern V2 used internally.
- **Locked V3 union order in helpers** (matches `decisions.md` § A): `[literal, $var, ctx-attr, ctx-build, ctx-self-id, ctx-self-marker-id, add, sub, mul, mod]`. `add/sub/mul/mod` collapse to a single `ArithmeticShape` lazy union — but ordering inside is `add, sub, mul, mod`, so introspection order at `_def.options` matches the locked tuple.
- **Right operand of arithmetic locked to `z.number().int()`**, NOT another resolver. Keeps overflow risk bounded (multi-resolver chains can still build via nested-on-the-left). Validator catches non-integer right operands at parse time; runtime overflow check at `MAX_SAFE_INTEGER` is the second-line defense.
- **Positive modulo formula `((l % r) + r) % r`** is essential for column-wrap recipes (`mod(add($col, 1), 8)`) — JS `%` returns negative for negative left. Tested via `mod(-1, 8) === 7`.
- **`ctx-self-id` / `ctx-self-marker-id` payload locked to `null`** (not `true`, `0`, `""`). Forces explicit shape in JSON descriptors. Tested at both validation (Zod `z.null()`) and runtime (walker `inner !== null` throw).
- **Test count delta**: param-resolver.test.ts 17 → 39 tests (+22). param-resolver-schema.test.ts 19 → 38 tests (+19). All 2983 tests in `bun run check` pass.
- **18 obsolete ParamField snapshots** were observed in the test output — pre-existing, NOT caused by this task. Confirmed by checking obsolete count is identical to fresh-write count from prior run.

View file

@ -0,0 +1,10 @@
# thressgame-100 — Unresolved Blockers
(Empty at plan-write time. Promote items here from issues.md when blocked beyond 3 retries.)
## Format
```
## [TIMESTAMP] Wave/Task: W<N>.<M> — BLOCKED
{description, blocking dependency, what's needed to unblock}
```

View file

@ -0,0 +1,460 @@
# ThressGame 100 — Full-Coverage Epic (Path B)
## TL;DR
> **Quick Summary**: Push ThressGame coverage from 27% (14/51) to ≥85% across 5 sequential waves. Add arithmetic resolver shapes, multi-turn countdowns, choice-into-target plumbing, wraparound/topology hooks, and game-level economy. Ship recipes for ALL 65 ThressGame rules except 6 documented empty-stubs (`pawns_with_viagra`, `estrogen`, `knee_surgery`, `pawns_learned_strength`, plus 2 routed elsewhere). 8 preset-shaped rules (`dual_king`, `coregal`, `god_kings`, `early_promotion`, `proletariat`, `short_stop`, `trains_rights`, `ice_physics` already shipped, `pacman_style`) get FORK_INTO_PRESET treatment — they ship as preset rule variants AND get cross-referenced from the Templates modal.
>
> **No backward-compat constraint** — the user explicitly authorized breaking changes. Determinism MUST hold (replay-byte-identical), but API surface changes are fine.
>
> **Deliverables**:
> - **Validator V3**: arithmetic resolver shapes (`add`, `sub`, `mul`, `mod`), `ctx-self-id`, `ctx-self-marker-id`
> - **`add-to-attribute` gains `target` field** — closes adjacent-splash class
> - **Generalized lifetime semantics**: `set-piece-attr` lifetime fires `on-attr-expire` trigger on countdown-zero
> - **New triggers**: `on-attr-expire`, `on-piece-pair-link-broken`, `on-resource-changed`
> - **New primitives**: `decrement-attr-each-turn`, `link-pieces`, `unlink-pieces`, `add-resource`, `spend-resource`, `condition-attr-cmp` (rich comparison conditional)
> - **Engine extensions**: wraparound topology flag, multi-turn deferred dispatch, score/inventory entities on `GAME_ENTITY`
> - **51 ThressGame rules covered as recipes** (51/51 of the non-stub, non-preset corpus = 100% of denominator; 51/65 raw = 78% of source corpus; +8 cross-referenced presets = effective 90% surfaceable behavior)
> - **8 preset-shaped rules** documented in `RULES.md` with cross-references in Templates modal
> - **6 WONT_FIX rules** documented with reason per rule
> - **Test surface**: ~225 new unit tests, ~25 new parity fixtures, ~12 new e2e specs, ~50 new snapshots
>
> **Estimated Effort**: XL — 5 waves, ~6-8 dev weeks compressed via aggressive parallelism. NO TIME LIMIT per user directive.
> **Parallel Execution**: HEAVY within waves; strict serial across waves.
> **Critical Path**: W1 resolver expressiveness → W2 multi-turn state → W3 choice patterns → W4 topology → W5 economy → W6 preset cross-refs + final verification.
---
## Context
### Original Request
User: "Do 85%. There is no limit to your time or cost. Just fucking get it all done. I don't care about backwards compatibility. Just do it."
### Background
After two prior epics (`thressgame-coverage` + `thressgame-templates`) shipped 23 recipes and 50 primitives covering 14/65 ThressGame rules, the user authorized full-coverage push despite the oracle's recommendation to stop after Wave 1. The system limits identified by the oracle are not blockers; they are paradigm extensions. User accepted paradigm break.
### Oracle Pre-Read Summary
The oracle consultation (`session ses_2313f30cbffezgUIabEnco2AoO`) flagged:
- Wave 4 (topology) invalidates all move-gen determinism tests
- Wave 5 (economy) violates "engine has no global mutable state" invariant
- 6 rules are empty `{}` stubs in `ruleHooks.js` (undefined behavior)
- 8 rules are preset-shaped (`getLegalMoveModifiers` / `getRoyalPieces` shape, not modifier shape)
- Honest denominator = 51 (not 65)
**User accepted all costs.** The plan proceeds with full scope.
### Locked Scope (no exceptions per user directive)
**Waves to execute**: 5 implementation + 1 cross-reference + final verification.
**Rules to ship as modifier recipes** (51 total):
- 14 already shipped (no change)
- 13 from Wave 1 (resolver expressiveness — Layer 1)
- ~10 from Wave 2 (multi-turn state)
- ~6 from Wave 3 (player choice patterns)
- ~5 from Wave 4 (topology + pairing)
- ~3 from Wave 5 (economy)
**Rules routed to RULES.md preset variants** (8):
- `dual_king`, `coregal`, `god_kings` (king variants — preset hook `getRoyalPieces`)
- `early_promotion` (preset row config + `override-promotion`)
- `proletariat`, `short_stop`, `trains_rights` (preset hook `getLegalMoveModifiers`)
- `pacman_style` (already preset `wrap-board`)
- `ice_physics` (already shipped as recipe AND mirrored in preset for parity)
**Rules WONT_FIX** (6):
- `pawns_with_viagra` (line 1626 of ruleHooks.js: `: {}`)
- `estrogen` (line 1638: `: {}`)
- `knee_surgery` (line 1698: `: {}`)
- `pawns_learned_strength` (line 1699: `: {}`)
- `parry` (line 1599: handled in moveHandler.js per upstream comment; we already have `parry` recipe via parity fixture, this just notes the upstream code-location)
- `pacman_style` (line 1670: `: {}` body; topology in `getWrapMoves`. Already shipped as `wrap-board` preset — gets a cross-reference recipe but no new modifier work)
### Metis Pre-Read
**Hidden intentions decoded from the user's message**:
1. **"No limit to time or cost"** — execution can take as long as needed; reviewer cycles can iterate.
2. **"I don't care about backwards compatibility"** — schema breaking changes (V3 union order, new required fields, new trigger stages) are FINE. Existing tests that fail because the schema changed should be updated, not preserved.
3. **"Just fucking get it all done"** — frustration with the prior wave's "53% ceiling" framing. User wants definitive completion, not partial credit.
4. **"Do 85%"** — accepts that 100% raw coverage is impossible (6 stubs); 85% raw = 55/65 = the realistic ceiling. Plan delivers 51/65 = 78% raw + 8 preset cross-refs = 90% surfaceable.
**Failure modes to design against**:
- "Fake completion" — claim coverage by shipping recipes that validate but don't actually run. Mitigation: every Wave's parity fixtures REQUIRE a `-real.test.ts` proving end-to-end behavior, plus an e2e spec.
- Determinism drift — new primitives that break replay. Mitigation: every new primitive lands with a determinism test in the wave it ships.
- Wave 4 topology cascade failure — wraparound invalidates move-gen caches across the entire test suite. Mitigation: Wave 4 ships behind a per-game `BoardTopology` attr flag (default = standard); only games opting in get wraparound, so existing tests stay green.
- Wave 5 paradigm break — global score state corrupts replays. Mitigation: scores live on `GAME_ENTITY` (already a singleton with mutable attrs like `RngStream` — same pattern, not a new pattern). Replay determinism is verified by hash comparison.
---
## Locked Architectural Decisions (signed off pre-execution)
These are irrevocable across all 6 waves. Changing one requires a plan amendment.
### A. Arithmetic resolver shapes (Wave 1)
- Add `add`, `sub`, `mul`, `mod` shapes. Arity = 2. Operands = `(resolver | integer)`. Range-check at `apply()`-time only (validator just checks shape).
- NO comparison ops, NO boolean logic, NO conditionals inside resolver shapes. `conditional` primitive remains the only branching mechanism.
- Union order in V3 schema: `[literal, $var, ctx-attr, ctx-build, ctx-self-id, ctx-self-marker-id, add, sub, mul, mod]` — literal first (hot path); single-key shapes by recognizability.
### B. Self-targeting (Wave 1)
- New shape `{ "ctx-self-id": null }` returns `ctx.pieceId` directly. Same shape but with `null` payload for `ctx-self-marker-id` returns `ctx.markerId` (only valid inside `on-piece-entered-marker` and `on-marker-expire` triggers).
- NOT adding `"self"` literal branch to numeric schemas (would make union irregular).
### C. `add-to-attribute` gains `target` field (Wave 1)
- Optional. When omitted, defaults to `ctx.pieceId` (current behavior — backward-compatible default).
- When present, accepts `numberOrResolver()` (V3 union). Closes adjacent-splash gap.
- Semantically equivalent to `set-piece-attr({target: ..., attr: ..., value: { add: [{ ctx-attr: { entity: ..., attr: ... } }, N] }})` but more readable.
### D. Multi-turn countdowns (Wave 2)
- Generalize `MarkerLifetime` semantics to per-piece attrs. `set-piece-attr.lifetime: { kind: "turns", count: N }` already accepted; Wave 2 wires the decrementer + expiration trigger.
- New trigger: `on-attr-expire(target, attr)` fires when an attr with a `turns` lifetime hits zero.
- New primitive: `decrement-attr-each-turn(target, attr)` for explicit countdown control (used by `time_bomb`).
- Implementation: new dispatcher stage 13 (after `fireOnTurnStartHooks`), batched per-turn-boundary to avoid the 100-countdowns-cascade-overflow scenario.
### E. Piece-pair lifecycle (Wave 4)
- New attr family: `PieceLink` (per-piece, list of EntityIds) — mirror of `MarkerLinks`.
- New trigger: `on-piece-pair-link-broken` fires when a linked partner is destroyed.
- New primitives: `link-pieces(a, b)`, `unlink-pieces(a, b)`.
- Used by: `down_with_the_ship` (captain dies → ship dies), `soul_link`.
### F. Resource accumulation / inventory (Wave 5)
- New attrs on `GAME_ENTITY`: `WhiteScore: number`, `BlackScore: number`. Default 0.
- New primitives: `add-resource(player, amount)`, `spend-resource(player, amount, then, else)` (conditional spend with branches).
- New trigger: `on-resource-changed(player, threshold)` fires when resource crosses threshold.
- Replay determinism: scores persist in fact log alongside RngStream; hash includes them.
### G. Board topology (Wave 4)
- New attr on `GAME_ENTITY`: `BoardTopology: "standard" | "wrap-files" | "wrap-all"`. Default `"standard"`.
- Move-gen consumers check this attr; standard topology unchanged → no regression in existing tests.
- New primitive: `set-board-topology(value)` (imperative; can fire on `on-rule-activated`).
- Wraparound only affects move generation when modifier sets `BoardTopology`. Standard games skip the check entirely.
### H. Validator V3
- All `numberOrResolver` / `enumOrResolverFor` callsites widen to V3 union (mechanical 9-file edit).
- New `paramsSchema` for Wave 1 helpers reuses V3 from start.
- Pre-V3 fixtures auto-validate clean (V3 is a superset of V2).
### I. Test gates per wave
- `bun run check` exit 0 before wave can be declared complete
- Each new recipe ships with a `*-real.test.ts` proving end-to-end runtime behavior
- Each wave ships ≥1 new e2e Playwright spec covering its UX surface
- Determinism: every new primitive has a property test (N=100 iterations, byte-identical state hash)
---
## Wave 1 — Resolver Expressiveness
**Goal**: Close Layer 1 (~13 rules). Add arithmetic shapes, self-targeting, `add-to-attribute.target`. Ship 13 recipes.
**Effort**: ~1.5 dev weeks → ~12 atomic tasks.
### Tasks
- [ ] **W1.0**: Initialize `.sisyphus/notepads/thressgame-100/` with decisions/learnings/issues/problems files. Lock decisions A, B, C, H from above. — 30min
- [ ] **W1.1**: Add `ctx-self-id` resolver shape to `param-resolver.ts:139-216`. Returns `ctx.pieceId`. Add unit tests covering: bare shape, inside iteration arm, inside `add` (forward-compat). — 45min
- [ ] **W1.2**: Add `ctx-self-marker-id` resolver shape mirroring W1.1. Throws if not inside marker-event trigger. Unit tests. — 30min
- [ ] **W1.3**: Add `add` arithmetic resolver shape: `{ "add": [<resolver>, <integer>] }`. Resolves left operand recursively, right is integer literal, returns sum. Range-check is caller's responsibility (no implicit clamp). Unit tests. — 60min
- [ ] **W1.4**: Add `sub`, `mul`, `mod` arithmetic shapes mirroring W1.3 pattern. — 30min
- [ ] **W1.5**: Update `param-resolver-schema.ts` to V3 — extend the union in `numberOrResolver()` / `enumOrResolverFor()` / `stringOrResolver()` to include the 6 new shapes. Document V3 union order (locked decision A). — 45min
- [ ] **W1.6**: `add-to-attribute.ts` schema gains optional `target: numberOrResolver()` field. `apply()` defaults to `ctx.pieceId` when absent. Update co-located test with positive cases (with/without target) AND negative case (target on capture-event when ctx.pieceId is undefined). — 60min
- [ ] **W1.7**: Determinism hardening: 6 new tests in `param-resolver.test.ts` covering arithmetic-overflow (add(MAX_SAFE_INTEGER, 1) must throw, not silent wrap), nested arithmetic (add(add($var, 1), 8)), mixed shapes (add(ctx-attr, 1)), and replay determinism (state hash byte-identical across 100 iterations). — 60min
- [ ] **W1.8**: Recipes batch A — self-targeting destroys (4 recipes, parallel-friendly delegation):
- `tpl-minefield-consumer` — `on-piece-entered-marker(mine) → destroy-piece({target: {ctx-self-id: null}}) → destroy-marker({target: {ctx-self-marker-id: null}})`
- `tpl-kamikaze-self-destruct` — `on-captured → destroy-piece({target: {ctx-self-id: null}})` (variant: kamikaze that ALSO kills self)
- `tpl-living-bomb` — `on-captured → for-each-adjacent → destroy-piece({target: {$var: "adj"}})` then self-destruct
- `tpl-suicidal-knight` — knight that destroys itself after 3 moves (uses Wave 2 mechanism — defer to W2 if needed; Wave 1 ships only the self-targeting half)
Each recipe ships with parity fixture + `-real.test.ts` + entry in `recipes.ts`. — 90min total
- [ ] **W1.9**: Recipes batch B — position arithmetic / mass mover (6 recipes, parallel):
- `tpl-march-of-the-pawnguins` — for-each-piece(white pawns) → move-piece({to: ctx-build with row+1})
- `tpl-the-rumbling` — same but pawns capture diagonally as they advance (combined move + capture-cascade)
- `tpl-back-that-shit-up` — every pawn moves backward 1
- `tpl-chaaaarge` — chooser's pieces all step forward 1
- `tpl-the-enemy-is-routed` — opponent's pieces all step backward 1
- `tpl-going-woke` — shift right half of board left
Each recipe + parity + `-real.test.ts` + recipes.ts entry. — 120min total
- [ ] **W1.10**: Recipes batch C — adjacent splash + mitosis (3 recipes, parallel):
- `tpl-adjacent-splash` — REAL deal-1-HP-to-adjacent (uses W1.6's `add-to-attribute.target`)
- `tpl-mitosis` — duplicate every piece into adjacent empty square
- `tpl-they-deserved-it` — destroy 1 random non-king (uses `with-probability` + `random-pick` + `destroy-piece(ctx-self-id)`)
Each + parity + e2e. — 90min total
- [ ] **W1.11**: e2e spec `packages/chess/e2e/wave1-recipes.spec.ts` — load all 13 new recipes via Templates modal, smoke-test 4 of them with runtime behavior assertions. — 60min
- [ ] **W1.12**: Evidence file `.sisyphus/evidence/thressgame-100-wave1.txt` — final task list, recipe IDs, determinism test results, paper-exercise for `the_rumbling` (the hardest Layer-1 rule). — 45min
**Wave 1 acceptance gate**:
- 13 new recipes pass validateCustomDescriptor
- 13 new parity `-real.test.ts` files green
- e2e wave1 spec green via `.sisyphus/scripts/run-pw.sh`
- `bun run check` exits 0
- `decisions.md` for thressgame-100 lists W1 final state
- Coverage check: 14 + 13 = 27 modifier recipes; 27/51 = 53%
---
## Wave 2 — Multi-Turn State
**Goal**: Close countdown / cooldown rule family (~10 rules). Ship `on-attr-expire` trigger, `decrement-attr-each-turn` primitive, and recipes for time bombs, restrictions with duration, and second-chance mechanics.
**Effort**: ~2 dev weeks → ~14 atomic tasks.
### Tasks
- [ ] **W2.0**: Lock decisions D (multi-turn countdowns) in notepad. Reaffirm: implementation = generalize MarkerLifetime to per-piece attrs; new `on-attr-expire` trigger fires on countdown-zero; batched dispatch per turn boundary. — 30min
- [ ] **W2.1**: Schema audit — every callsite of `LifetimeRegistry` (search engine.ts, modifiers/apply.ts, set-piece-attr.ts). Document the per-turn decrement loop's current shape. — 60min
- [ ] **W2.2**: Add new dispatcher stage 13 — `fireAttrExpireHooks`. Runs at turn-end-boundary AFTER stage 12 (`fireOnTurnStartHooks`). Batches per-turn-boundary: collects all attrs whose lifetime hits zero this turn, fires `on-attr-expire` once per (target, attr) pair. — 120min
- [ ] **W2.3**: New trigger primitive `on-attr-expire`. Schema: `{target: numberOrResolver, attr: stringOrResolver, primitives: [...]}`. Validator: imperative-in-passive check applies to inner primitives; binding-introducer (binds `expiringValue` to the attr's last value before zero). — 90min
- [ ] **W2.4**: New primitive `decrement-attr-each-turn(target, attr)` — imperative. Sets the attr to `current - 1` at every turn-end. When it hits zero, normal `on-attr-expire` fires. — 60min
- [ ] **W2.5**: Tests for W2.2-W2.4: 8 unit tests covering per-turn batching, simultaneous expiry of multiple attrs, expiry inside iteration arms, replay determinism with 50 in-flight countdowns. — 120min
- [ ] **W2.6**: Performance test — 100 in-flight countdowns + per-turn dispatch must stay under 150ms p99 (matches existing markers-perf budget). Adds to `__fixtures__/perf/`. — 60min
- [ ] **W2.7**: Recipes batch D — countdowns (5 recipes):
- `tpl-time-bomb` — set HP-bomb attr with countdown 5; on-attr-expire detonates (destroy-piece + adjacent splash)
- `tpl-nuclear-fallout` — countdown variant of minefield (random blocked squares for N turns)
- `tpl-christmas-truce` — for 3 turns, no captures allowed (set CannotCapture flag with lifetime turns:3)
- `tpl-second-chance` — when captured, remember piece; next turn it returns (uses `cancel-capture` + restore primitive)
- `tpl-invulnerability-potion` — temp invulnerability marker (lifetime turns:N)
— 150min
- [ ] **W2.8**: Recipes batch E — restrictions with duration (3 recipes):
- `tpl-anti-camping` — pieces that don't move for 3 turns get destroyed
- `tpl-ice-age` — files a/h frozen for 5 turns
- `tpl-no-cowards` — must move forward (lifetime turns:1, refreshes per turn)
— 90min
- [ ] **W2.9**: Recipes batch F — drafted_for_battle / sophies_choice prep (2 recipes):
- `tpl-drafted-for-battle` — chooser picks bishop/knight, swaps with king (uses `request-choice` + `swap-pieces`); already buildable post-V2 — verify and ship
- `tpl-corporate-ladder` — chooser picks 2 squares, swap pieces on them
— 60min
- [ ] **W2.10**: e2e spec `packages/chess/e2e/wave2-countdowns.spec.ts` covering time-bomb detonation, christmas-truce duration, second-chance restoration. Uses test-only `__test__.advance-turn` WS frame (add if missing). — 90min
- [ ] **W2.11**: Verify all 13 Wave 1 recipes still pass `recipes.test.ts` and `wave1-recipes.spec.ts` (regression check). — 15min
- [ ] **W2.12**: Update `param-resolver.test.ts` if `add` shape now needs to interact with countdown decrements (likely not, since decrement is its own primitive, but verify). — 30min
- [ ] **W2.13**: Determinism property test — N=100 games with random countdown attachments, assert byte-identical state hash. — 60min
- [ ] **W2.14**: Evidence file `.sisyphus/evidence/thressgame-100-wave2.txt`. — 45min
**Wave 2 acceptance gate**:
- 10 new recipes (5 countdown + 3 restriction + 2 verified)
- `on-attr-expire` trigger fires deterministically
- 100-countdown perf test green
- All 27 prior recipes still green
- Coverage: 27 + 10 = 37 / 51 = 72%
---
## Wave 3 — Player Choice Patterns
**Goal**: Close request-choice driven recipes (~6 rules). Most patterns already supported by primitives; this wave is heavy on RECIPE AUTHORING, light on engine work.
**Effort**: ~1 dev week → ~9 atomic tasks.
### Tasks
- [ ] **W3.0**: Audit existing `request-choice` capabilities. Confirm `kind: "square"`, `kind: "piece"`, `kind: "column"`, `kind: "row"`, `kind: "rps"`, `kind: "coin-flip"`, `kind: "yes-no"`, `kind: "number"` all work. Document any gaps. — 60min
- [ ] **W3.1**: If any choice kind is missing or buggy, fix it. Most likely target: `kind: "square"` since several Wave 3 recipes use it. — 90min
- [ ] **W3.2**: Recipes batch G — choice-driven spawn (3 recipes):
- `tpl-bottomless-pit` — chooser picks square, plant pit there
- `tpl-call-down-lightning` — chooser picks square, destroy whatever's there + spawn marker
- `tpl-portal-storm` — chooser picks 2 squares, spawn portal pair
— 90min
- [ ] **W3.3**: Recipes batch H — choice-driven swap/move (3 recipes):
- `tpl-anti-camping-choice` — chooser picks an opponent piece, swap with random own piece
- `tpl-corporate-ladder-full` (already shipped in W2.9, this is the full multi-step variant)
- `tpl-two-kids-trenchcoat` — sacrifice 2 pawns to spawn bishop (uses request-choice for both pawns + place-piece)
— 90min
- [ ] **W3.4**: Recipes batch I — choice-driven self-modification (2 recipes):
- `tpl-blood-sacrifice` — chooser picks own piece to destroy in exchange for buff to another own piece
- `tpl-summoning-ritual-light` — sacrifice piece + roll RNG, spawn random piece (light variant — no resource cost yet, that's W5)
— 60min
- [ ] **W3.5**: Recipes batch J — sophies_choice variants (2 recipes):
- `tpl-sophies-choice` — both players pick one of their own pieces to kill
- `tpl-mind-control-full` — chooser picks enemy non-king to convert (already shipped! verify and skip if so)
— 60min
- [ ] **W3.6**: e2e spec `packages/chess/e2e/wave3-choices.spec.ts` covering all 8 new request-choice flows end-to-end. — 90min
- [ ] **W3.7**: Verify Wave 1+2 regression. — 15min
- [ ] **W3.8**: Evidence file `.sisyphus/evidence/thressgame-100-wave3.txt`. — 30min
**Wave 3 acceptance gate**:
- 8 new recipes (overlap-deduped)
- All choice kinds verified working
- Coverage: 37 + 8 = 45 / 51 = 88% (above the 85% target — Wave 4-5 are bonus)
**🎯 85% TARGET HIT AT END OF WAVE 3 — Waves 4-5 are bonus rounds for completeness.**
---
## Wave 4 — Topology + Pairing
**Goal**: Wraparound topology + piece-pair lifecycle. ~5 rules. **High regression risk** — gated behind opt-in attr.
**Effort**: ~2 dev weeks → ~12 atomic tasks.
### Tasks
- [ ] **W4.0**: Lock decisions E (piece-pair) and G (board-topology). — 30min
- [ ] **W4.1**: Add `BoardTopology` attr to `ChessAttrMap`. Default `"standard"`. Move-gen reads this; standard skips wraparound check. — 60min
- [ ] **W4.2**: Implement wraparound move generation. Modify `engine.ts` move-gen to consult `BoardTopology` and apply wrap when set. Add `set-board-topology` primitive. — 180min
- [ ] **W4.3**: Tests for W4.1-W4.2: 25+ unit tests covering wraparound for each piece type, edge cases (rook on a-file with wrap-files), interaction with markers (frozen squares with wraparound), determinism. — 240min
- [ ] **W4.4**: Add `PieceLink` attr family. Per-piece, list of EntityIds. Validator + Zod schema. — 60min
- [ ] **W4.5**: Add `link-pieces(a, b)` and `unlink-pieces(a, b)` primitives + tests. — 90min
- [ ] **W4.6**: Add `on-piece-pair-link-broken` trigger. Fires when a linked partner is destroyed (hooks into `on-captured` cascade). — 90min
- [ ] **W4.7**: Tests for W4.4-W4.6: 15+ unit tests. — 120min
- [ ] **W4.8**: Recipes batch K — topology (2 recipes):
- `tpl-pacman-style-cross-ref` — comment-only recipe pointing to `wrap-board` preset; explains why this isn't a modifier (educational)
- `tpl-bouncing-ricochet` — modifier that locally enables wrap-files for THIS rule's lifetime (uses `set-board-topology` with lifetime turns:1)
— 60min
- [ ] **W4.9**: Recipes batch L — pairing (3 recipes):
- `tpl-down-with-the-ship` — captain (king) dies → all linked pieces (rooks) die
- `tpl-soul-link` — link 2 pieces; either dies → both die
- `tpl-hot-drop` — spawn 2 random queens, linked (one dies → both die)
— 90min
- [ ] **W4.10**: e2e spec `packages/chess/e2e/wave4-topology-pairing.spec.ts`. — 90min
- [ ] **W4.11**: Regression: verify ALL prior 45 recipes + their parity tests + every existing chess test still passes. Wave 4 highest regression risk. — 60min
- [ ] **W4.12**: Evidence file. — 30min
**Wave 4 acceptance gate**:
- Wraparound move-gen passes all 25+ unit tests
- Piece-pair lifecycle works
- 5 new recipes
- ZERO regression in 45 prior recipes
- Coverage: 45 + 5 = 50 / 51 = 98%
---
## Wave 5 — Economy + Inventory
**Goal**: Score/resource accumulation. ~3 rules. **Paradigm break — game-level mutable state.**
**Effort**: ~2 dev weeks → ~10 atomic tasks.
### Tasks
- [ ] **W5.0**: Lock decision F (resource accumulation on `GAME_ENTITY`). Document the paradigm-break rationale: scores live alongside RngStream on GAME_ENTITY (same pattern, not a new pattern). — 30min
- [ ] **W5.1**: Add `WhiteScore` and `BlackScore` attrs to `ChessAttrMap`. Both `number`, default 0. Initialized at game-start in integration preset. — 45min
- [ ] **W5.2**: Add primitives `add-resource(player, amount)` and `spend-resource(player, amount, then, else)`. The spend variant is conditional — branches on whether the player has enough. — 120min
- [ ] **W5.3**: Add trigger `on-resource-changed(player, threshold, direction)` — fires when score crosses threshold up or down. — 90min
- [ ] **W5.4**: Tests for W5.1-W5.3: 20+ unit tests covering accumulation, spending, branching, threshold crossing, determinism. — 180min
- [ ] **W5.5**: WS protocol — score updates broadcast to clients alongside game state. Schema bump or piggyback on existing `game.delta` frames. — 90min
- [ ] **W5.6**: UI — score display in game header (small additions to `GameView.tsx`; minimal styling). — 90min
- [ ] **W5.7**: Recipes batch M — economy (3 recipes):
- `tpl-treasure-chest` — capture spawns treasure markers; landing on one adds to score
- `tpl-cash-grab` — every turn-end, randomly pick squares; landing on them awards score
- `tpl-summoning-ritual` — spend 5 score to summon a knight
— 90min
- [ ] **W5.8**: e2e spec `packages/chess/e2e/wave5-economy.spec.ts` — score accumulation, threshold crossing, spend-success/spend-fail flows. — 90min
- [ ] **W5.9**: Determinism property test — economy state reproduces across replays. — 60min
- [ ] **W5.10**: Evidence file. — 30min
**Wave 5 acceptance gate**:
- 3 new recipes
- Scores deterministic across replays
- All 50 prior recipes still green
- Coverage: 50 + 3 = 53 / 51 = above target (53 > 51 because some Wave 5 recipes overlap categories with prior waves — net unique = 51/51 = 100% of denominator)
**🏁 100% OF DENOMINATOR HIT (51/51) AT END OF WAVE 5.**
---
## Wave 6 — Preset Cross-References + Final Verification
**Goal**: 8 preset-shaped rules get cross-referenced from Templates modal so users can FIND them, even though they're preset variants not modifier recipes. Then final 4-reviewer wave.
**Effort**: ~3 dev days → ~6 tasks.
### Tasks
- [ ] **W6.0**: For each of 8 preset-shaped rules, add a "stub recipe" entry in `recipes.ts` that:
- Has a recognizable `id` (e.g. `tpl-preset-dual-king`, `tpl-preset-coregal`, etc.)
- Has a `summary` explaining "This rule is a preset rule variant, not a modifier. Click here to learn more."
- Has an EMPTY `primitives: []` array (validator-clean — passive descriptor)
- Has a `descriptor.description` pointing at `RULES.md#<preset-id>`
These stubs ship in the Templates modal but loading them just shows a docs panel pointing at the preset. — 90min
- [ ] **W6.1**: Update `RULES.md` v3 section adding cross-reference table mapping ThressGame rule names to chess preset IDs. — 60min
- [ ] **W6.2**: Update Templates modal UI to render the "preset stub" recipes with a distinguishing visual marker (e.g. a "preset →" badge instead of a "Load" button). — 90min
- [ ] **W6.3**: e2e spec for preset cross-references. — 60min
- [ ] **W6.4**: WONT_FIX manifest at `packages/chess/docs/THRESSGAME_WONT_FIX.md` — 1 entry per of the 6 stub rules with reason and source-line citation. — 45min
- [ ] **W6.5**: Final verification wave (F1-F4 in parallel) — see "Final Verification Wave" section below.
---
## Final Verification Wave (F1-F4)
Run after Wave 6 lands. All 4 reviewers must APPROVE.
- [ ] **F1 (oracle review)**: All 5 architectural decisions land cleanly; no contradictions across waves. Validator V3 union order is consistent. Determinism holds across all 5 waves. Engine paradigm break (Wave 5) is contained to GAME_ENTITY attrs as designed.
- [ ] **F2 (manual QA)**: Run all 5 e2e specs (wave1-recipes, wave2-countdowns, wave3-choices, wave4-topology-pairing, wave5-economy). Every recipe loads and runs end-to-end against docker compose dev stack.
- [ ] **F3 (test-suite quality)**: `bun run check` exit 0 with full test count. ZERO new fixmes/skips. Coverage delta is measurable. No flake observed.
- [ ] **F4 (scope fidelity)**: 51 modifier recipes + 8 preset cross-refs + 6 WONT_FIX = 65 raw rules accounted for. Coverage 51/51 = 100% of effective denominator.
---
## Coverage Report Card (final state)
| Bucket | Count | Status |
|---|---|---|
| Modifier recipes shipped | 51 | 100% of effective denominator |
| Preset cross-references | 8 | Cross-linked from Templates modal |
| WONT_FIX (empty stubs) | 6 | Documented with source-line citations |
| **Raw ThressGame rules accounted for** | **65/65** | **100%** |
| **User-facing surfaceable** | **59/65** | **91%** |
| **As true modifier recipes** | **51/65** | **78%** |
**Bottom line**: Every rule from `ruleHooks.js` is either shipped, cross-referenced, or documented as undefined behavior. No silent gaps.
---
## Locked Recipe IDs (final 51 modifier recipes)
**Pre-epic existing (14)**:
1. recipe-boosted-pawn, recipe-three-charge-shield, recipe-aura-king, recipe-vampire, recipe-low-hp-fortress, recipe-kamikaze-knight, recipe-berserker-pawn, recipe-promotion-feast (Wave 1 originals)
2. tpl-simple-mine, tpl-vampire-on-capture, tpl-frozen-column, tpl-coin-flip-restriction, tpl-religious-bishop, tpl-no-mans-land (T67)
**Prior wave thressgame-templates (9)**:
3. tpl-religious-conversion, tpl-mr-freeze, tpl-mind-control, tpl-kamikaze, tpl-ice-physics, tpl-minefield-full, tpl-mass-destroyer-they-deserved-it, tpl-lifetime-restriction, tpl-adjacent-debuff
**Wave 1 (13)**:
4. tpl-minefield-consumer, tpl-kamikaze-self-destruct, tpl-living-bomb, tpl-march-of-the-pawnguins, tpl-the-rumbling, tpl-back-that-shit-up, tpl-chaaaarge, tpl-the-enemy-is-routed, tpl-going-woke, tpl-adjacent-splash, tpl-mitosis, tpl-they-deserved-it, tpl-suicidal-knight (deferred to W2 if Wave 1 mechanism insufficient)
**Wave 2 (10)**:
5. tpl-time-bomb, tpl-nuclear-fallout, tpl-christmas-truce, tpl-second-chance, tpl-invulnerability-potion, tpl-anti-camping, tpl-ice-age, tpl-no-cowards, tpl-drafted-for-battle, tpl-corporate-ladder
**Wave 3 (8)**:
6. tpl-bottomless-pit, tpl-call-down-lightning, tpl-portal-storm, tpl-anti-camping-choice, tpl-two-kids-trenchcoat, tpl-blood-sacrifice, tpl-summoning-ritual-light, tpl-sophies-choice
**Wave 4 (5)**:
7. tpl-pacman-style-cross-ref (preset-stub), tpl-bouncing-ricochet, tpl-down-with-the-ship, tpl-soul-link, tpl-hot-drop
**Wave 5 (3)**:
8. tpl-treasure-chest, tpl-cash-grab, tpl-summoning-ritual
**Wave 6 preset stubs (8 — cross-references, not real recipes)**:
9. tpl-preset-dual-king, tpl-preset-coregal, tpl-preset-god-kings, tpl-preset-early-promotion, tpl-preset-proletariat, tpl-preset-short-stop, tpl-preset-trains-rights, tpl-preset-pacman
**Total**: 14 + 9 + 13 + 10 + 8 + 5 + 3 = 62 modifier-shaped + 8 preset stubs = **70 entries in CUSTOM_MODIFIER_RECIPES** (was 23, +47).
Adjustment: dedup overlap (kamikaze full vs simplified, mind-control vs full, corporate-ladder vs full). Final unique modifier-content recipes = 51.
---
## WONT_FIX Manifest (6 rules)
To be documented in `packages/chess/docs/THRESSGAME_WONT_FIX.md` at end of Wave 6:
| Rule | Source line | Reason |
|---|---|---|
| `pawns_with_viagra` | `/tmp/ruleHooks.js:1626` | Empty `{}` stub in upstream ruleHooks.js; behavior undefined |
| `estrogen` | `/tmp/ruleHooks.js:1638` | Empty `{}` stub |
| `knee_surgery` | `/tmp/ruleHooks.js:1698` | Empty `{}` stub |
| `pawns_learned_strength` | `/tmp/ruleHooks.js:1699` | Empty `{}` stub |
| `parry` (RPS handler) | `/tmp/ruleHooks.js:1599-1601` | Comment: "RPS logic is handled in moveHandler.js and server.js" — already covered by parry parity recipe; this entry is a reference-only stub |
| `pacman_style` (modifier form) | `/tmp/ruleHooks.js:1670` | Body is `{}`; topology lives in `getWrapMoves` outside hook system. Routed to chess preset `wrap-board` (already shipped). Recipe stub `tpl-preset-pacman` cross-references the preset. |
---
## Notes for Atlas (orchestrator)
- **Notepad layout**: `.sisyphus/notepads/thressgame-100/{decisions,learnings,issues,problems}.md`. Initialize at W1.0.
- **Parallelism rules**: Within a wave, dispatch independent tasks in parallel (different files = parallel-safe). Across waves, strict serial.
- **Determinism harness**: every new primitive ships a `*.test.ts` with at minimum a "replay deterministic" property test. Reuse existing harness from `packages/chess/src/__fixtures__/determinism/`.
- **Playwright helper**: ALWAYS via `.sisyphus/scripts/run-pw.sh`. Never `CI=true`. Docker stack must be up.
- **Backward-compat is REVOKED**: schemas can break. Tests asserting old behavior should be updated, not preserved. The user said "I don't care about backwards compatibility" — take the most direct path through any wall.
- **Anti-fake-completion**: every recipe ships with a `*-real.test.ts` that runs the descriptor through the actual engine and asserts observable state change. Recipe validation alone is not proof of correctness.
- **Wave-end snapshot**: at the end of each wave, append a brief progress-report to `.sisyphus/evidence/thressgame-100-progress.txt` (running tally across waves).
- **No premature optimization**: ship the simplest correct implementation per task. Optimize only if a perf test fails.
- **Failure handling**: when a delegated task fails, RESUME the SAME session with the actual error output (per the Atlas runbook). Maximum 3 retries per task. After 3, document in problems.md and continue with independent tasks.