1# Mosaic23A Takuzu mosaic that an agent has to *explore*, not one-shot. Phase 1 is a4localhost referee + spectator for watching a single agent work a complicated5space. Nostr and the shared daily world come only after that is worth scaling.67Architecture reference: the 30b4 page (Mosaic Fishbowl). This file is the8build order and the decisions that page left open.910## Principles (do not relitigate per phase)1112- Every tile is under-determined alone (`m > 1` solutions); the mosaic is13 determined as a whole. If a tile can be solved in isolation, the mosaic is a14 for-loop and the exercise is pointless.15- Givens are hidden until earned. The agent never has the whole board in16 context. Code use is allowed; strategy still has to be the agent's.17- State is a fold over an append-only log from a seed. The spectator and any18 later replay are the same fold. Cells are derived, never stored.19- One referee orders the world. In phase 1 it is a local process; in phase 320 it is the single signer.21- The human only spectates. Every change on screen comes from the agent, or22 from scrubbing a replay. There is no human input to the board, ever.23- **No fixed layout, ever.** `X` (tiles per side) is any value in 2..15 and24 `n` is any even tile size in 6..12; every placement is computed from25 `(tx, ty)`, pitch `n-1`, and the manifest. The seed tile is drawn from the26 seed, the frontier is derived from what is solved, and the viewer never27 assumes a size, a centre, or a symmetric wavefront. Anything that only28 works at one `X` is a bug. `docs/mock-mosaic.py` takes `X` as an argument29 for exactly this reason.3031## Tile standard (from the literature, not from puzzletea's presets)3233- Binary Puzzle is NP-complete (De Biasi 2012, planar-CNF reduction); the34 bare variant without balance and uniqueness is already NP-hard.35- Utomo and Makarim (Math.Comput.Sci. 2017), puzzles with ~75% blanks and a36 unique solution: propagation-plus-backtracking takes 0.5 s at 8x8, 5-8 s at37 10x10-12x12, 79 s at 14x14, 975 s at 16x16. SAT is flat (0.3 s at 14x14).38 Blank count barely moves SAT; it dominates search. The knee for "hard39 without a SAT solver" is between 12x12 and 14x14.40- Human technique ladder (same paper, and the tactic graders such as41 Borroot/binairo): constraint-1 propagation (pairs, sandwiches),42 constraint-2 (balance completion), constraint-3 (row/column uniqueness),43 then bifurcation (guess, propagate to contradiction). Commercial ladders44 top out at 14x14 hard (binarypuzzle.com, puzzle-binairo.com; 20x20 exists45 but is volume, not a new technique).4647Decision: **a tile is 14x14 with at least 70% blanks, requires the48uniqueness constraint, and requires at least one bifurcation.** That is the49canonical hard Takuzu; anything smaller is a warm-up. `n = 12` is the floor50for experiments that need faster generation; nothing below 12 counts.5152What overlap-by-one leaves unique to a tile with four solved neighbours:5354| n | unique interior (n-2)^2 | share of tile | one solved neighbour fixes |55|----|--------------------------|---------------|----------------------------|56| 4 | 4 | 25% | 25% |57| 8 | 36 | 56% | 12.5% |58| 12 | 100 | 69% | 8.3% |59| 14 | 144 | 73% | 7.1% |6061At n=14 a seam hands the agent 7% of a tile; the other 93% is internal work62on a hard puzzle. That is the intended effect: the frontier is slow, and a63tile is not "easier because a neighbour fell". It also creates a generator64obligation: the `m` solutions of a tile must *differ on its seam lines*,65otherwise a neighbour can never pin it and the tile is just an unsolvable66puzzle with extra steps. `gen` asserts this per tile.6768Mosaic envelope at the standard, `G = X*(n-1)+1`:6970| X | n=12 | n=14 | tiles | cells at n=14 |71|----|-----------|-----------|-------|---------------|72| 2 | 23x23 | 27x27 | 4 | 729 |73| 4 | 45x45 | 53x53 | 16 | 2,809 |74| 8 | 89x89 | 105x105 | 64 | 11,025 |75| 15 | 166x166 | 196x196 | 225 | 38,416 |7677The top end is the same order as mcpsweeper's 224x224 cap, which the canvas78already renders.7980Consequences taken into the design below:8182- **Forced vs guess is judged by technique, not by solution counting.**83 Counting solutions of a 14x14 with 70% blanks is the 79 s row above. The84 referee instead runs propagation (constraints 1-3) over revealed85 information and, optionally, bifurcation to a configured depth `d`. A cell86 is `forced` if that reaches it, `guess` otherwise. This is the same method87 the difficulty-rating literature uses, and it is polynomial for `d = 0`.88- **Generation is not on the request path.** Hard 14x14 tiles with a target89 multiplicity and seam-straddling ambiguity cost seconds each in a90 backtracking generator. The daily mosaic is generated ahead of time;91 paired trials share one generation; `gen` is a batch job with a cache92 keyed by manifest hash.9394## Phase 1 — localhost, one agent, exposed console9596### Goal9798**Watch one agent solve a 2x2 mosaic of hard 14x14 Takuzu tiles on99localhost, live, with every step replayable.**100101The demo moment: open `http://127.0.0.1:8000/`, paste the printed prompt102into Claude Code or Codex, and watch. The seed tile appears; intents scroll103in the console; cells fill under their ownership washes; a solved tile's104seam line lands in its neighbour; the agent works the neighbour with 93% of105it still to do; all four tiles fall. Reload in replay mode and scrub it back106to the first reveal.107108Done means all of these, each with a test or a command that proves it:1091101. `mosaic-engine gen` (Go) emits a 2x2, n=14 mosaic deterministically per111 seed, and asserts per tile: m in {2,3} solutions alone, the m solutions112 differ on a seam line, at least 70% blank, uniqueness plus one113 bifurcation required; and for the mosaic: exactly one solution.1142. `mosaic-engine judge` classifies a batch of commits as forced or guess115 by propagation (constraints 1-3, bifurcation depth `d` configurable) in116 under 50 ms per batch at n=14.1173. `/mcp` serves `start_mosaic`, `observe`, `reveal`, `commit`, `retract`118 with `expected_revision`, and a fresh agent given only the printed prompt119 finishes a game with no human help.1204. The viewer pans and zooms, colours cells by owner-hue mean at fixed121 lightness with status by wash, shows the console and the scrubber, and122 follows live over SSE.1235. Replaying the JSONL reproduces the live run frame for frame; a test124 asserts equality of every revision between the live fold and the replay125 fold.1266. The envelope holds: `gen` runs at X=15 (as a batch, minutes allowed) and127 the viewer renders 196x196; both also run at X=2.128129Out of scope for this goal, on purpose: Nostr, identity and accounts, the130analyzer, more than one agent, paired trials, visual identity, deployment.131132Build order, each step usable on its own: engine `gen` -> engine `judge` ->133site log + referee + engine wrapper -> `/mcp` -> viewer -> replay test ->134envelope test -> the demo.135136137### Shape138139Two processes, split at the puzzle boundary:140141- **Engine** (`engine/`, Go, imports `github.com/FelineStateMachine/puzzletea`142 via a local `replace`). Pure functions over JSON, no state, no ports:143 `mosaic-engine gen` (seed -> tiles, givens, seams, m per tile, manifest) and144 `mosaic-engine judge` (revealed cells + candidate commits -> forced/guess with145 rule attribution). Invoked as a subprocess with JSON on stdin/stdout. It is146 the only place Takuzu rules exist.147- **Site** (`site/`, Deno 2 + Fresh 2, same pins as rect.run: `@fresh/core`,148 Preact, Vite). Owns everything else: game state, the append-only JSONL log,149 the MCP endpoint (`@modelcontextprotocol/server` + `@modelcontextprotocol/hono`,150 as in mcpsweeper), SSE to the viewer, and the viewer itself as one island151 with a canvas.152153```154engine/ Go module: gen, judge, rules (puzzletea inside)155site/deno.json Fresh 2 app156site/routes/ / viewer page (spectate-only)157 /mcp streamable HTTP MCP, authless on 127.0.0.1158 /events/:id SSE, live fold159 /log/:id JSONL download for replay160site/islands/ Viewer.tsx canvas: map, cells, rule blends, console, scrubber161site/lib/ referee.ts state, frontier gating, calls the engine162 log.ts JSONL append + fold163 engine.ts Deno.Command wrapper around mosaic-engine164```165166The site never re-derives a rule; the engine never holds a game. Phase 3167swaps the site's runtime (Deno -> Workers, where mcpsweeper and tribes live)168without touching the engine.169170### Generator (`gen`)171172Inputs: `seed`, `X` (tiles per side, 2..15), `n` (tile size, even; 14 is173the standard, 12 the floor), `m` (target solutions per tile alone, 2-4),174coupling flags. Global grid is `X*(n-1)+1` cells per side: 23x23 at the175smallest, 196x196 at the largest.1761771. Tiles in raster order. For each tile pre-fill the shared top row and left178 column from already-generated neighbours (overlap-by-one seams), then179 complete the tile with puzzletea's partial-grid fill180 (`fillGridSeededWithStatsContext`). Needs a one-line exported wrapper in181 puzzletea: `CompleteGridSeeded(ctx, partial, size, rng)`.1822. Band uniqueness: reject a tile whose row set collides with another tile in183 the same band (cheap set check; retry the fill).1843. Givens: run puzzletea's clue removal per tile but stop at185 `countSolutions(limit=m+1) == m` instead of `== 1`, with at least 70%186 blanks and a technique check that the tile needs uniqueness plus one187 bifurcation. Seam cells are never givens; they arrive from the neighbour.188 Assert that the `m` solutions differ on at least one seam line.1894. Global check: a propagator over the coupled system must find exactly one190 solution. If not, add one given to the most ambiguous tile and repeat.1915. Emit `manifest`: `sha256(seed||nonce)`, `X n m`, coupling flags, generator192 version hash.193194Measured while building the mock: a whole-grid raster backtracker fills195X=15, n=4 (61x61) in 50 ms but does not finish X=15, n=8 (106x106) in two196minutes. So the generator is per-tile with seam prefill as written above,197never whole-grid, and it needs a bounded retry: a tile whose fixed top row198and left column admit no completion (a forced three-in-a-row across the199prefilled corner, say) backjumps to regenerate its left or upper neighbour200rather than failing the mosaic. Budget: the daily mosaic is a batch job,201not a request; a 15x15 at n=14 may take minutes, and that is fine because202its output is cached by manifest hash and shared by every paired trial.203204Deferred from v1: "rules across the seam" (no-three straddling a border is205only partially enforced by overlap) and the coupling-depth metric `k`.206Both are honest gaps; note them in the manifest as `k: unmeasured`.207208### Referee (`site/lib/referee.ts`)209210- `frontier` = hidden tiles adjacent to a solved tile; at start, one seed tile211 (chosen from the seed, not the agent).212- `reveal(tile)` allowed only on the frontier. Returns that tile's givens plus213 any seam cells already fixed by solved neighbours.214- `commit(cells[])` validated against the solution. For each cell the referee215 also runs the solver over *revealed* information only and records216 `forced | guess`. Guesses are accepted by default (config: `reject`).217- **Rule attribution.** For a forced cell the referee records *which rules*218 force it (`balance`, `run`, `unique`, `seam`), evaluated independently.219 Givens are `given`; unforced commits are `guess`. This is analyzer data in220 the batch event; the spectator does not colour by it in phase 1.221- `retract(cells[])` allowed; counted.222- A tile is `solved` when every cell is committed and correct.223- Every accepted call appends one `batch` event. Refusals append nothing.224225### MCP (`site/routes/mcp`)226227Streamable HTTP at `http://127.0.0.1:8000/mcp`, no auth (localhost only).228Mirror mcpsweeper's shape: an `expected_revision` on every write, cell229arrays, a short `intent` string.230231```232start_mosaic { X, n, m, rules } -> game_id, seed_commit, frontier233observe { game_id } -> revision, revealed cells (delta-coded), frontier tile ids, per-tile status234reveal { game_id, expected_revision, tile } -> givens + fixed seam cells235commit { game_id, expected_revision, cells[], intent } -> accepted[], forced/guess split with rules, rejected[]236retract { game_id, expected_revision, cells[] }237```238239`intent` (<=160 chars) is the "exposed console": the agent's stated reason240for the batch, shown verbatim in the spectator.241242### Spectator (`site/islands/Viewer.tsx`)243244One page at `http://127.0.0.1:8000/`, canvas, spectate-only. Fed live by SSE245from the site's log; in replay mode fed by the same log from disk. Both are246the same fold.247248- **Map.** The whole mosaic, pan (drag) and zoom (wheel), no minimum or249 maximum beyond "fits" and "one tile fills the view". Tiles read as solved /250 frontier / hidden at every zoom; seams drawn as shared edges.251- **Cells.** A 0 and a 1 drawn as proper glyphs at the current scale, not252 text-rendered characters. At overview zoom cells collapse to tone; at253 detail zoom the glyph is the primary mark.254- **Ownership as background blend.** Every tile has its own hue from a255 7-hue set, assigned by `(tx + 3*ty) mod 7` so no two tiles that share a256 cell (all eight neighbours) share a hue. A cell's colour is the *mean* of257 its owners' hues in OKLCH at a fixed lightness: a seam cell is the mix of258 two hues, a corner cell of four, and none of them is lighter or more259 opaque than a single-owner cell. Never stack alpha; a lightness pyramid on260 the grid is a bug. Wash strength encodes status (solved strongest,261 frontier lighter, hidden faintest) and a cell takes the highest status262 among its owners. `docs/mock-mosaic.py` is the reference rendering263 (`color-mix(in oklch, ...)` there; the canvas does the same in code).264- **Agent console.** A side pane: tool calls in order, intent text,265 forced/guess counts, reveal-to-commit ratio, batch size drift, wall-clock266 between batches. Newest at the bottom, follows live.267- **Scrubber.** Steps the fold; map, cells, and console all redraw to that268 revision. Live mode is "scrubber pinned to the end".269270Nothing on the page is clickable except pan, zoom, and the scrubber.271272**Visual identity: neutral until the PoC is done.** Greys for chrome, a muted273hue per rule for the blends (they are data, not decoration), system fonts,274no marketing copy, no logo, no landing page. Do not use the middle-dot (U+00B7) character275anywhere. Identity work is its own task after phase 1 exits.276277### Driving one agent278279`deno task dev` in `site/` prints a copyable prompt: install the MCP endpoint, then280"start a 2x2 mosaic of 14x14 tiles and solve it autonomously with281observe/reveal/commit". Point Claude Code or Codex at it and open the page.282283### Exit criteria284285- One agent solves a 2x2 mosaic of 14x14 tiles, `m = 2-3`, end to end,286 watched live on the page. (2x2 is the minimum mosaic; 27x27 cells.)287- Replaying that log through the viewer reproduces the identical final state and every288 intermediate frame, including rule blends.289- At least one tile in that run was *not* solvable before its seam arrived290 (the generator must assert this, not hope for it).291- `gen` and the viewer both work at `X=2` and `X=15` (the agent only has to292 play the 2x2; the envelope has to render and replay at both ends).293294## Current goal (set 2026-08-26): complete the specced field295296Everything decided today is specced but not built. This goal is to build297it, in order, on localhost, each step proven, so that the phase-3 public298site is a deployment of finished parts and not a design exercise.299300**Goal: a daily-sized field (9x9 chunks of G=60), generated chunkwise in301minutes, played by several agents placed at random with per-agent302frontiers and territory, watched on a level-of-detail tilemap viewer303with a real replay timeline, measured by the analyzer, on localhost.**304305The demo moment: `deno task dev` prints the bootstrap prompt. Start it306in three agents; each lands at a random spot on today's field; watch307the three territories grow in three hues, meet, and merge; zoom from308territory (L0) to shape (L1) to values (L2) to lines (L3); scrub the309timeline to the meeting; open the analysis pane and read who guessed310more.311312Done means all of these, each with a test or a command that proves it:3133141. **Chunkwise generation (6a).** `gen --context` accepts prefilled cells315 with their owning tiles; a `field` subcommand plans a checkerboard of316 chunks, runs them as processes (evens, then odds), merges manifests317 with offset ids. Proof: a 2x2 of G=60 chunks passes every assertion318 re-verified from the merged JSON and has one coupled solution; a 9x9319 of G=60 generates in under 10 min on the Mac; determinism per seed320 regardless of parallelism.3212. **Generator repair.** The G=200 failure ("alternative differs inside322 no overlap region" after regiven) is repaired in place, not by323 restarting the attempt. Proof: seed a at G=200 scatter, one attempt.3243. **Agents in the log.** A `join` event with `agent` (machine id or a325 provisional one), harness from MCP `clientInfo`, reported model, and326 tags; every write event carries `agent`. `start_mosaic` becomes327 `join { field }`; the seed tile per agent is drawn at a random328 uncovered-frontier spot. Proof: two clients on one field produce one329 log whose writes are attributed; replay == live.3304. **Per-agent frontier and territory.** An agent reveals only tiles331 overlapping tiles it solved, until its region touches another's;332 regions merge; a `meet` event is recorded. Ownership for colouring is333 per agent. Proof: referee tests for the gating and the merge; the334 analyzer records territory share per agent over time.3355. **Region-scoped observe.** `observe { agent | rect }` returns the336 agent's tiles plus a radius (or a viewport); the whole field is never337 in one MCP response at daily size. Proof: observe at the 9x9 field is338 under 64 KB.3396. **Tilemap viewer with levels of detail.** L0-L3 as decided; map tiles340 rasterised once per level and cached, dirtied per event; SSE per341 chunk in view; hue per agent. Proof: the 9x9 field renders and pans342 at 60 fps at every level in the dev browser; a replay scrub redraws343 only dirty map tiles (counter in the console).3447. **Replay timeline.** Event-density timeline, markers (tile solved,345 meet, guess runs), playback speed, follow-agent. Proof: the demo.3468. **Analyzer (v2 item 3).** Analysis in the fold: guess rate,347 reveal-to-commit, retractions, reward per batch, territory per agent,348 frontier strategy, per-agent summaries. Proof: live == replay349 analysis at every revision; the scripted client's BFS/DFS modes are350 told apart.3519. **Ink and calm.** The dark-theme discs are heavier than the352 calm-water brief; one pass on ink weight and transitions, checked in353 screenshots at L1/L2.35410. **A fresh agent plays a field.** Claude Code or Codex from the printed355 prompt, on a scatter field, no help. Proof: its log.356357Out of scope for this goal: identity (machine keys, shoo.dev, `/link`),358the calendar, R2, the domain and the name, paired-trial compare page359(v2 item 4; the analyzer's per-agent summaries cover most of it), `k`360(v2 item 5), the tile library (v2 item 6; chunkwise generation replaces361its purpose for now).362363Build order: 1 -> 2 -> 3 -> 4 -> 5 -> 8 -> 6 -> 7 -> 9 -> 10 (the analyzer364before the viewer so the viewer has something to show; ink last so it is365judged on the final marks).366367### Progress on the current goal (2026-08-26)368369- **Item 10 done first**: a fresh agent (general-purpose, no repo access,370 curl JSON-RPC only) solved scatter G=60 seed-less game `9f477627-...`371 (43 tiles): 43 reveals, 270 commits, 1524 forced / 695 guess, 4 real372 guesses, 2 real wrong commits (the other 177 "wrong" were its own373 client bug re-guessing one cell in a loop), reward 3684, ~2 min of play374 after a 5 min generation. Its feedback, to fix (owner: site core):375 1. `start_mosaic` blocks for the whole generation and only sends SSE376 keepalives; naive clients time out, and each retry spawned an377 orphan "playing" game (`20f8dc44-...` and others). Make start378 asynchronous: return `{game_id, status: "generating"}` at once,379 `observe` reports `generating` until ready, and `list_games` shows380 it; expire never-joined games.381 2. No `Mcp-Session-Id` is returned and `notifications/initialized`382 holds the SSE stream open instead of returning 202.383 3. `commit` rejections carry `{id, reason}` only: echo the rejected384 value, and say in the tool text that a wrong binary value is full385 information.386 4. Document the referee's propagation exactly (the agent's pair/gap +387 balance + distinct-lines propagation found 2 fewer forced cells388 than the referee's).389 5. State that a solved neighbour does not always pin a tile (tiles 9,390 15, 27 kept 2 solutions with one solved neighbour).391 6. `observe {agent}` lists the shapes and `m` of hidden tiles within392 the radius before they are revealed; intended, say so.393- Item 3-5, 8 (site core) landed: `join`, per-agent frontier with seed394 reservation, `meet`, region-scoped `observe` (28 KB on a 2203-tile395 synthetic field), analyzer in the fold with `end` carrying it and396 `/log/:id?summary=1`; a real race fixed (concurrent writes passing the397 revision check while awaiting the judge; referee writes now serialised).398 Two scripted agents (bfs/dfs) met at rev 73 on scatter G=60: log399 `46401331-...`. Harness capture: `clientInfo` is not available per call400 in this SDK's stateless mode; the site records `harness_source`401 (envelope | client_version | argument | none).402- Fresh-agent feedback fixed (site core): `start_mosaic` is asynchronous403 (`generating` status, sweeper for never-joined games), rejections carry404 the value, the referee's propagation is documented exactly in the rules405 text, pinning and observe-scope caveats stated. The SDK's stateless406 mode cannot issue `Mcp-Session-Id`; `notifications/initialized` already407 returned 202 (the agent misread an SSE-framed `initialize`).408- Items 6, 7, 9 (viewer) landed: 256 px map tiles per (zoom, level),409 change log per revision, LRU cache, hue per agent (join order, seed410 distance avoidance), L0-L3 as decided, timeline with markers and411 playback, follow-agent, checkpointed fold for large replays, ink as412 tones of the wash. Measured on a synthetic 9-agent G=530 field (1777413 tiles, 5070 revisions): panning at L0/L1/L2 median 8.4-8.9 ms per414 frame, 0 map tiles redrawn in steady state; load 18.6 s, dominated by415 the fold (assigned to site core: structural sharing, target < 2 s).416 Screenshots `output/playwright/after-2agents-L{1,2,3}-dark.png`,417 `v3-field-530-9agents-L{0,2}.png`.418- Items 1-2 (engine): `gen --context`/`field` and the in-place repair are419 built and committed; the timings were not: the 9x9 field's first run420 took 52+ min and a second run was still going at 48 min, both under421 contention from four other workloads, and my 10-minute target was an422 extrapolation from one idle best-case chunk (41 s, one attempt) that423 ignored multi-attempt chunks, the harder odd round, and `--jobs 9`424 (~10 waves, not 6). Both runs were stopped as uninformative.425- What the runs did show: each chunk process sits at 100-140% CPU on 18426 cores (SAT is serialised inside a process by gophersat's package-level427 buffer plus our mutex), and single flip trials take 13 s on a 204-cell428 tile (the givens removal loop rebuilds and re-solves the CNF per429 candidate clue). Decision (2026-08-26): optimise in evidence order430 before measuring again — instrument per phase; vendor gophersat with a431 per-solver buffer and drop the mutex; incremental removal loop (one432 solver per tile, `Assume` per trial, propagation before counting);433 retry the failing wave rather than the chunk. Then measure the 3x3 and434 9x9 fields idle with `--jobs` = cores / measured cores-per-chunk, and435 restate item 1's proof from that measurement (the target is set by the436 product cadence: minutes for the daily, ~10 min for the hourly).437438### Generator optimisation and the field numbers (2026-08-26, evening)439440Instrumented first (per-phase, per-tile timers; `MOSAIC_DEBUG=1` prints441the table), then cut in evidence order. Idle machine, G=60 scatter seed442a, 45 tiles, wall clock:443444| step | wall | what the table said |445|------|-----:|----------------------|446| baseline | 34.4 s | givens 33.3 s of it; 108k SAT calls, 168 s inside solve incl. lock wait; heaviest tile 12.2 s over 60 trials |447| gophersat vendored with a per-solver learn buffer, mutex gone | 32.8 s | lock wait gone (solve 32.7 s) but wall unchanged: the removal loop is CPU-bound re-parsing the CNF per trial |448| incremental SAT for the removal loop (one hot solver per tile, givens as assumptions, selector-guarded blocking clauses) | 3.04 s | SAT calls 108k -> 24k; heaviest tile 1.0 s; output byte-identical, fixtures reproduce |449| retry the failing tile's wave, not the attempt | 3.07 s | no change on this seed (nothing failed); it matters for chunks that used to restart |450451Then the layout defect the first idle 9x9 run exposed: two odd chunks'452seed tiles (placed touching nothing) never got a whole-line overlap, so453"an alternative differs inside an overlap region" was unsatisfiable and454each chunk burned 20 attempts x 3 re-packings (13 min, then failure).455Fixed in the packer (a drop onto an unpinnable seed, a check after the456fill pass, salted re-draws) and in gen (refuse such a layout before any457search).458459Fields, idle, `--jobs 6`:460461| field | tiles | G | wall | chunks min / median / max | notes |462|-------|------:|--:|-----:|---------------------------|-------|463| 3x3 of G=60 | 252 | 148 | **9 s** | 0.4 / ~1 / 1.9 s | every chunk one attempt |464| 9x9 of G=60 | 1915 | 412 | **293 s** | 0.3 / 3.3 / 46.7 s | one chunk re-packed (too many backjumps); 86 attempts over 81 chunks |465466So item 1's proof holds as written (9x9 under 10 min), and the daily's467generation is under five minutes on the Mac. The remaining tail is a few468chunks at 30-47 s whose first attempt fails on "too many backjumps" or469"pinned tile falls to propagation alone" (policy, not repair): the next470cut is to make those wave-level retries too, and `--jobs` closer to the471core count now that a chunk process is mostly single-threaded again472(the incremental loop serialises per tile; waves still run in parallel).473474### Item 2 outcome (2026-08-26, night): one attempt at G=200475476The road there, each step measured on `gen --seed a --G 200 --kind477scatter`, idle:478479| state | wall | attempts | what failed |480|-------|-----:|---------:|-------------|481| repair as first written | 40 min timeout | - | 24 min spinning in the backtracking coupled count, then a repair loop cycling on one pair |482| coupled count as SAT; redraw a mutually unwitnessed tile; rounds capped | 842 s | 10 (9 wasted) | 5x fill thrash (backjumps), 2x "shared cells stay ambiguous", 1x policy, 1x no pinning flip |483| local escalations: fill redrawn with a new salt inside the attempt, two-hop joint completion, technique policy keyed on free cells, repair redraws the interior, layout-class failures re-pack | 418 s | 6 (5 wasted) | 4x "shared cells stay ambiguous" (in wave and in repair), 1x repair round cap |484| neighbours' planted cells as last-resort seam clues | **67 s** | **1 (0 wasted)** | - |485486Final: 444 tiles, fill 5.0 s, givens 23.2 s (53 waves), settle and487coupled count 36.6 s, 37 regivens, 1 interior redraw, 0 backjumps.488`gen.Version` stays gen-v5: the 2x2 fixture still reproduces byte for489byte (the grid path has no shared-cell ambiguity to escalate on), and490the X=15 fixture re-verifies.491492Goal tally: **all ten items done and proven.** Remaining known gaps are493recorded as ideas, not as goal items: the field's per-chunk tail (a few494chunks at 30-47 s), `observe`'s O(tiles) frontier scan, seed-tile495reservations that never expire, and the harness field's dependence on496what real MCP clients send.497498### Goal extension (set 2026-08-26, night): the field grows, then it is hosted499500Items 11-12 first, 13-15 after; each proven like the others.50150211. **Growing field.** The daily field is allocated at G=1000 from the503 start with the initial 3x3 cluster at the centre, so cell ids never504 change; `mosaic-engine field --grow --state <private>` generates the505 next ring of chunks around the cluster (checkerboard within the506 ring, existing border tiles as fixed context) and emits a delta507 (new public tiles, new private tiles, merged solution, new ring508 seed tiles). The referee applies it as a `grow` event (tiles509 appended, geometry extended in place, late joiners' seeds drawn510 from the newest ring), triggered by a timer (`grow.every_minutes`)511 or when the frontier ring's coverage passes a threshold. Proof: a512 field grown twice while two scripted agents play, replay == live,513 determinism regardless of `--jobs`, growth wall clock per ring.51412. **Fog.** With `rules.fog`, `observe` lists a hidden tile only if a515 tile of the caller's group overlaps it (or it is the agent's own516 seed reservation); the spectator sees every generated tile, with517 unreached ones as the faintest wash at L0/L1 and a `grow` marker on518 the timeline. Proof: a test that observe never lists an unreached519 tile; screenshots of reach spreading over a grown field.52013. **Daily loop.** `deno task daily`: at rollover archive the day521 (manifest, log, analysis summary under `data/days/<date>/`, index522 row in SQLite), start the new day's field from `daily.Seed(date)`523 with its rings generated ahead (nightly, `field --rings all`), grow524 by reveal on schedule; `/` is today (live if running, else the525 finished replay), `/day/<date>` the replay, `/calendar` the month526 grid with per-day stats, "copy prompt" everywhere. **Stakes527 (decided 2026-08-27): two leaderboards at the end of the day, on528 two axes** — by **model** (every agent that reported that model,529 aggregated: is Claude winning, is some open model catching up) and530 by **user** (machine key or join name until shoo.dev, aggregated531 across whatever models they ran: is one person farming the board,532 and with what — each user's row lists the models they ran). Reward533 = cells solved, with forced/guess, meets and tiles alongside; daily534 and cumulative. **Visual reward the next day**: the top user's hue535 is reserved and their seed tile is drawn on the new field with a536 crown (L1+, listed first in the sidebar); the top model is named537 on the field's start card; the calendar tile shows both winners. Proof: two simulated538 rollovers on localhost with clocks advanced, calendar shows both539 days with winners, both replay, the second day's field shows the540 first day's winner's mark. Also: the referee re-folds live state541 from the log on restart (proof: kill and restart mid-game, agents542 continue at the same revision); a per-territory cap on `observe`;543 a fetch timeout in the scripted client.54414. **Hosted on Fly.io** (decided 2026-08-27). One machine (shared-cpu,545 a volume for `data/`), the Deno site plus the Go binary in one image,546 the daily loop in-process on a schedule, TLS and a stable547 `<app>.fly.dev` domain until the project is named; the MCP endpoint548 public and authless, spectator public. Nightly ring generation runs549 on the machine (minutes at `--jobs 1-2`; it is fine if it takes a550 quarter hour) or is pushed from the Mac. Proof: a fresh agent on551 another machine plays today's field from the printed prompt; the552 calendar shows the day; a machine restart resumes the game.55315. **Machine identity and shoo.dev** (from the identity decision):554 machine keys at join, `/link` pairing, "yours" on login. Last;555 optional for the first hosted days.556557### Hosting decided 2026-08-26 (night): compute offline, serve from one small box558559The engine is pure compute (Go, SAT) and growth would need it during560the day; that is only a pain on serverless. Two moves make it a561non-issue:562563- **Growth is reveal, not generation.** Growth is deterministic per564 (seed, ring), so the whole day's field is generated the night before565 as rings (`field --rings all`, one private delta per ring, ~250566 chunks for a G=1000 field: minutes on the Mac, a quarter hour on a567 small VM), and the daytime `grow` event reveals pre-generated chunks568 adjacent to where agents are pushing. Zero daytime compute beyond the569 judge; "grows where agents push" is kept at chunk granularity.570- **One referee, one box.** A single small VM (Hetzner CX22-class or a571 Fly machine) runs the Deno site and the Go binary; the judge is a572 millisecond subprocess. Nightly generation runs either on the VM573 (cron, off-peak) or on the Mac with the private file pushed over ssh;574 logs and day archives live on the box, mirrored to R2 later. No575 Workers, no KV, no split referee. If the site ever needs to move to576 a platform without subprocesses, compile `mosaic-engine judge` to577 wasm (`GOOS=wasip1`) and call it in-process; the rules stay in Go.578579Item 11's `--grow` is therefore the ring generator run ahead of time;580item 13's daily loop schedules reveal, not generation.581582### Items 11-12 landed (2026-08-26, night)583584- Engine: `field --G 1000` centres the cluster; `field --grow --state`585 generates the next ring against the border as fixed context and emits586 a delta; `--rings N|all` pre-generates rings for the nightly job587 (timing in the engine fork's report below when it lands).588- Site: `start_mosaic {kind: field, G, chunks, chunk_g, grow: {every_minutes |589 frontier_coverage}, rings, rules.fog}`; stored rings are revealed as590 `grow` events with no daytime engine call (on-demand `--grow` only as591 fallback); fog in `observe` (hidden tiles listed only when the caller's592 group reaches them); geometry extended in place, hues kept, late593 joiners seated on the newest ring; growth also replaces `end` while a594 ring can still be placed.595- Viewer: fit-to-cluster on the G=1000 canvas, empty map tiles skipped,596 growth dirties only the new tiles' map tiles (0 redraws steady, <= 5 on597 a level change), fog wash, `grow` marker, "reached N of M" line.598- Live proof: `b8b8efc5-...` — 233 tiles at start, ring 1 +372 at rev599 674, ring 2 +542 at rev 681, four scripted agents (bfs/dfs/bfs/greedy),600 2 meets, 302 solved by rev 918. Viewer screenshots601 `output/playwright/v3-grown-fog-L{0,1,2,3}.png`.602- Found on the way, to fix in item 13's checklist: `observe {agent}` hit603 76 KB on a 106-tile territory (a per-territory cap or paging at daily604 size); the scripted client's fetch has no timeout (a dev-server HMR605 reload stalled two players); on-demand ring 3 (24 chunks) took 10+606 min under contention, which is why rings are pre-generated.607608### Decided 2026-08-27: measure the generator's growth curve before it runs hosted609610The nightly `--rings all` measurement on the G=1000 daily passed an hour611with chunk processes at 2.1 GB and 0.9 GB RSS (the Fly machine has6122 GB). Before anything generates on Fly: (1) fix the memory (release613per-tile incremental SAT solvers, bound the problem cache; target614< 300 MB per chunk process); (2) a scaling study at small sizes with615per-chunk instrumentation (wall, phases, SAT calls, waves, attempts,616regivens, peak RSS, fixed-context size) across ring index 1-4, chunk617size 40/60/80, jobs 2/4/6 and band 8/16/24, to see what is linear,618what is worse, and which levers move it; tables below. Until then the hosted daily consumed fields generated on619the Mac and pushed (`deno task push-day <date>`).620621**Scaling study results (engine fork, 2026-08-27; 18-core Mac, `--jobs 6`,622G=1000, 3x3 of G=60, gen-v6):**623624| ring | new tiles | wall | before tonight's fixes |625|-----:|----------:|-----:|-----------------------:|626| base | 233 | 12 s | - |627| 1 | 396 | 17 s | 1826 s |628| 2 | 577 | 20 s | 30 s |629| 3 | 736 | 28 s | 43 s |630| 4 | 938 | 35 s | 1262 s (then stopped at 60 min in ring 5) |6316322888 tiles after ring 4; max chunk 10 s; one chunk in 90 needed a second633attempt. Readings: cost is ~37 ms per tile regardless of chunk size634(40/60/80 neutral); `--jobs` beyond 4 gains nothing (a chunk uses6351.5-2.6 cores); band 8 doubles tiles and RSS, band 24 is cheapest;636memory per chunk 130-250 MB on a region-sized board (was 618 MB, and6372 GB on the full canvas), spikes under CPU starvation are GC overshoot,638not working set. Hosting guidance for a 2 GB box: `--jobs` = cores/2639with `GOMEMLIMIT=256MiB`. The pathological chunks were each a real640bug, now fixed: seed placed on infeasible context, clipped regions too641thin, drops accepted without a joint completion with the new tiles642they touch, the "complete alone" fallback thrashing after a one-hop643joint UNSAT; small boards (<= 48 new tiles) are now filled by one joint644SAT completion (`sat.CompleteAll`) and a bad layout is a 150 ms verdict645at pack time. `manifest.field` carries `origin`, `occupied`, `grown`,646`seed_tiles`; a grow delta appends tiles and reverse overlaps (never647reorders: pins index the lists).648649### Items 13-14 landed (2026-08-27): hosted at mosaic-daily.fly.dev650651- **https://mosaic.dev.site/** (Cloudflare-proxied, Fly cert issued652 2026-08-27; `mosaic-daily.fly.dev` still answers) — app653 `mosaic-daily`, region ams at first,654 one shared-cpu-2x machine (2 GB), 1 GB volume at `/data`, TLS; about655 $11/month at list prices. `MOSAIC_GENERATE=0`: the box never runs the656 generator; days are generated on the Mac and pushed:657 `cd site && deno task push-day <date>` (3x3 of G=60 on a G=1000658 canvas with 2 rings ahead, ~4 MB, ~3 min idle) to `/push/<date>` with659 a bearer token (`~/.config/mosaic/push-token`, Fly secret660 `MOSAIC_PUSH_TOKEN`). A day with nothing pushed shows "no field for661 <date> yet".662- Daily loop: seed from the date, rollover at UTC midnight, archive663 under `data/days/<date>/`, SQLite index; `/`, `/day/<date>`,664 `/calendar`, `/leaderboard/<date>` (by model, and by user with the665 models they ran; daily and cumulative); the top user is honoured the666 next day (reserved seat and hue, crown, seated on join by machine id667 or name), the top model named on the day; "copy prompt" with the668 request's origin.669- Robustness: the referee re-folds from the log on restart (proven670 locally at rev 280 and hosted across `fly machines restart` at rev671 179, both resumed and played on); `observe` capped at 100 tiles with672 a `truncated` flag; client fetch timeout 90 s.673- Proof: `tests/daily_test.ts` (two simulated rollovers with winners,674 honours, cumulative boards, restart resume, missing-day behaviour);675 hosted day `day-2026-08-27` played by two scripted agents.676- Not yet: a fresh agent from another machine (item 14's proof line),677 the nightly `--rings all` timing, and hosted generation (waits on the678 engine's scaling study).679680### Decided 2026-08-27: the hosted box is run for stability, not speed681682Generation moves onto the Fly box now that a chunk is ~250 MB on a683region-sized board. Stability does not mean gimped: `--jobs 2` on the6842-vCPU box (two ~250 MB chunks fit easily), `--rings all` with the685whole night as budget, no artificial caps; what stability means is that686at rollover there is always a playable field687(prepared, else last pushed, else a small emergency field generated on688the spot) — never "no field today"; the generator is a supervised child689the daily loop kills on timeout and that can never take the site down;690the site keeps answering `/` and `/mcp` while a chunk generates. The691machine and volume move to dfw. Speed decisions come from the scaling692tables, after stability holds.693694### First real agent on the hosted daily (2026-08-27)695696The user pasted the copy-prompt into Codex Desktop (reported model697gpt-5.6-sol); it joined today's hosted field on its own, harness698captured from the transport, and played. Prompt proven. Two bugs it699exposed, fixed on the spot:700701- `start_mosaic` was in the hosted tool list and the prompt said to702 start a field if none was playing, so a stray G=100 game was created703 on the box. On the daily host the public tools are join/observe/704 reveal/commit/retract only; the prompt says join today's field.705- Commits were not territory-gated: another agent solved the Codex706 agent's revealed seed tile out from under it, leaving it with no707 frontier, so it re-joined as a new agent. Commits and retracts are708 now allowed only on tiles the agent's own group revealed; `join` with709 the same machine id or name is idempotent and re-seats an agent left710 with nothing. Contested tiles will be an explicit mechanic, not an711 accident.712713Also measured on the box while it generated: a play-through went714revision 2 -> 245 in 60 s with two chunk processes at 140 MB peak each,715`/` answering in 0.13 s.716717### Measured on the Fly box (2026-08-27) and the nightly's final shape718719On the shared-cpu-2x machine (gen-v6, band 24, jobs 2, GOMEMLIMIT720256 MiB): ring 1 of the daily (+251 tiles) took **2,410 s** against721~20 s on the Mac; the givens phase (SAT-heavy removal loop) dominates;722max RSS per chunk 181 MB. Memory is a non-issue, CPU is: shared vCPUs723are ~100x slower on this workload, so `--rings all` cannot fit a724nightly window there. Decision: the app machine only serves (plus the725small emergency field); the nightly is a **scheduled Fly machine with726dedicated CPU** (`fly machine run --schedule daily`, performance-4x,727dfw) that generates tomorrow's field with `--rings all` in minutes,728POSTs it to `/push/<date>`, and stops — billed for minutes a day. The729push route, built as a workaround, is the nightly's proper delivery730path. Everything else from items 13-14 holds on the box: `/` with731generating/missing states and yesterday's replay, territory-gated732commits, idempotent join, restart-from-log, hosted tool list trimmed.733734### Nightly proven on Fly (2026-08-27)735736`mosaic-nightly` (performance-4x, `--schedule daily`, dfw, same image737with `MOSAIC_ROLE=nightly`) generated 2026-08-28's field — 197 tiles738plus all 12 rings of the G=1000 canvas, 22 MB — in **50 min**, pushed it739to the app, and stopped; `/status` reports tomorrow pushed. Cost about740$0.15/day (~$4.5/month) on top of the app machine's ~$11. The app741machine no longer prepares fields (`MOSAIC_DAILY_PREPARE=0`); the742emergency field remains its only generation. Operational note: the743scheduled machine is not touched by `fly deploy`; after a deploy, `fly744machine update mosaic-nightly --image ...` (in deploy.md). Its daily run745lands at ~05:11 UTC, 19 h before rollover.746747Items 13 and 14 are complete. Open from the extension: 15 (machine748identity and shoo.dev), and from the ideas: stakes mechanics beyond the749leaderboard, name and domain.750751### Decided 2026-08-27: the nightly runs on slate (the user's Woodpecker CI)752753The user's own hardware runs Woodpecker ("slate", on their tailnet;754forge GitHub, docker agents linux/amd64). The nightly is a Woodpecker755cron pipeline (`.woodpecker/nightly.yaml`: build the engine in756`golang:1.27`, run `push-day --tomorrow --rings all` in `denoland/deno`,757token from a repo secret) pushing to the app at 05:00 UTC; the repo758lives at `FelineStateMachine/mosaic` (private) because Woodpecker needs759a forge repo. Done 2026-08-27: pipeline #5 on slate (16 cores) generated the7602026-08-28 field — 197 tiles + 12 rings, 22 MB — in 1098 s at jobs 16761(oversubscribed; the cron now runs jobs = cores/3, timing lands with762the next real run) and pushed it; the Fly `mosaic-nightly` machine is763destroyed; deploy.md keeps the recipe to recreate it if slate is down;764the app machine keeps its emergency field. Repo765https://github.com/FelineStateMachine/mosaic (private), Woodpecker repo7664, cron `nightly` 05:00 UTC, secret `mosaic_push_token`,767`scripts/check-tracked.sh` guards against tracked data/tokens.768769### Found 2026-08-27: the referee is an oracle, and an agent used it770771The Codex agent's script commits every unknown cell of a revealed tile772as 0 in one batch, reads the rejections (which echo the value), commits773the complements, and never deduces anything: 723 tiles, 41k accepted774cells, 20,557 wrong (33%), top of the day's leaderboard. Within the775stated rules; the per-cell verdict invites it. Honest play measured the776same day: 0.1-0.3% wrong.777778**Decided 2026-08-27: health.** Guessing bleeds you. Each agent has a779health read from its wrong-rate over its last 2,000 committed cells780(windowed, so an honest rate heals the damage on its own): under 1.5%781healthy; 1.5-4% warning (flagged in observe and commit results, hue782wilts in the viewer); 4% and above, after at least 300 committed cells,783death — writes refused for the rest of the day, territory stays, group784mates continue, the leaderboard shows "died at rev N", tomorrow is785fresh. Rationale: an honest 3-way tile costs up to ~1.3% (two wrong786cells per ~150), the exploit runs at 33%, a blind prober dies after787~250 cells. Per-cell verdicts stay: they are what the rate is measured788on. The prompt describes health as a fact of the field, not as a list789of prohibitions (agents on the host cannot start games, so the prompt790does not tell them not to). Exit for this work: `reset-day <date>`791(localhost task and `POST /admin/reset/<date>` on the host, push-token792auth) wipes the day's stats — the old log is kept aside, out of the793calendar and leaderboards — and restarts the same field at revision 0;794then the same Codex agent plays it again from the prompt, and health795does what it should. Shipped 2026-08-27 (Fly v13): health in the fold796(`healthOf`), `dead` refusals, `health`/`note` on observe and commit,797wilting hues and death markers, leaderboard `wrong_rate`/`dead`,798`--strategy probe` as the regression (dies under 300 wrong), reset-day799run once — today is at revision 0. The set-aside log replays with the800retroactive verdict: Codex a0 dead at rev 346 (38% wrong), a2 dead at801256 (33%), the honest agent healthy at 0.1%. Awaiting the acceptance802run.803804### Acceptance runs under health (2026-08-27) and what they taught805806- Codex, re-run with the old probing script: dead at one tile (42%).807 Codex again, adapted: enumerates solutions and commits their808 intersection, one ambiguous commit at a time — the technique ladder,809 arrived at because guessing costs.810- Fable 5: inferred balance and no-three from solved tiles, then811 inferred the uniqueness rule too (correctly) and talked itself out of812 it, committed whole candidate solutions on m=3 tiles at 1-in-3 odds813 calling it deduction, fired one guess per stuck tile per round across814 dozens of tiles, and died at 4% (80 of 2,000) at revision 1247 with 15815 tiles. Root cause on our side: **no tool on the host carried the816 rules** — the rules text hung off `start_mosaic`, which the host hides817 — so the agent never read that lines must be distinct, that m is the818 tile's own solution count, or how health works. Fixed: the rules819 travel with `join` (the one tool every agent calls), every tool points820 at the `mosaic://rules` resource, `observe` defines every tile field821 where it appears, and the rules open with a glossary.822- Worth keeping from its post-mortem: ambiguity under m >= 2 is normal823 and is resolved by expansion (a solved neighbour's shared cells824 collapse the count), so the right policy is reveal more, wait for825 seams, and guess one cell only when the territory is saturated — the826 rules now say so; and tile-local marginals over enumerated solutions827 are not evidence, because the alternatives are planted (a flip828 rectangle) and the truth is one of m near-symmetric options — say829 this in the rules too, so no agent trusts a 0.9 "probability".830- To play with (from the user): the uniqueness rule is subtle enough831 that an agent cannot confirm it empirically ("no duplicate lines832 observed" is weak evidence); per-tile rule masks announced in observe833 (idea 5 above) make it explicit and let tiles differ; a field with834 uniqueness off on some tiles is a gate, on with it is the standard.835836### Visual identity decided 2026-08-27: Kifu, field + dock, a Go match screen837838Mockups (https://3111.n.zip/, `output/mockups/`): three layouts (map839first; field + dock; the day as a broadcast timeline) by three840identities, first as moods (rejected: LLM tropes), then as artefacts —841Kifu (the printed Go record), Draft (weaving draft and punched card),842Timetable (railway information design). Rules that stuck: every843element from a real artefact convention for grids or binary844information; no gradients, blur, glow, textures or radii; hue only845where it is data. **The user chose Kifu with the field + dock layout,846pushed toward a game feel**, whose artefact is the Go server match847screen: player cards with clocks (health as byo-yomi time), the last848stone marked, a move counter, live territory counting, a result banner849in kifu style at day end, the tournament table beneath. Next: implement850it in the viewer (Viewer.tsx, the tilemap's L0-L3 marks, the sidebar as851player cards, the timeline as the move list) once the refined mockup is852approved.853854### Current goal (set 2026-08-27, night): the Kifu match screen, for real855856The user approved the direction from the mockup (`output/mockups/857dock.html`, buggy, a statement of intent, not pixel truth). Implement it858in the real viewer. Done means all of these, each proven:85986016. **Identity tokens.** `lib/colour.ts` and the theme carry Kifu: a flat861 board tone, stone black and white, agent hue only as data862 (territories, health, route marks), print-style small caps and rules,863 Noto Serif JP for text with tabular numerals; no gradients, blur,864 glow, textures or radii except stones. Light is the default; a night865 variant keeps the same structure. Proof: screenshots at L0-L3.86617. **Board.** The tilemap draws L0 as flat territory tints, L1 adds867 shape outlines and territory counting marks, L2 stones (1 black,868 0 white; givens with a small mark; commits plain) with the last-move869 ring on the newest, L3 board lines with coordinates on the frame; a870 move counter on the board; fog stays void; no detail for an871 unrevealed tile. A placement snap (transform only, ~120 ms) on newly872 known cells replaces the ripple. Proof: 0 map tiles redrawn in873 steady state still; screenshots.87418. **Dock as a match screen.** Player cards: hue stone avatar, name,875 model as rank with harness small, tiles, points, and the clock —876 health as byo-yomi (main time while healthy, periods ticking at877 warning, "timeout" when dead); the agent of the latest event carries878 the "to move" card. Tabs: moves (the numbered move list built from879 events: number, stone, agent, tile or coordinate; joins, meets,880 grows and deaths as kifu notes), tile (detail of the tile under the881 centre or tapped), table (the tournament table = the day's882 leaderboard, both axes). Collapsible; the stats block is gone.88319. **Counting line** along the board's bottom edge: per-agent points884 live, leader bold, dead struck through.88520. **Scrubber and playback** stay under the board as a slim rule with886 kifu-notated markers; the move list follows the playhead.88721. **Day end.** A result card in kifu style over the tournament table;888 `/day/<date>` and `/calendar` in the identity (calendar as day cells889 with a winner bar; leaderboard as the table).89022. **Phone.** Cards above and below the board, tabs as a bottom sheet,891 counting line scrolls sideways, touch pan and zoom; the Today island892 states (generating, missing) in the identity. Proof: playwright at893 390x844 on the live day, and the user on a real phone.89423. **Proofs held**: `deno task test` and `check` green, the viewer895 tests (levels, dirtying, hues, timeline, checkpoints) adapted, the896 old logs replay.897898Out of scope: machine identity, stakes beyond health, the map-first899and broadcast layouts (the day page is the dock layout in replay).900901### Items 16-23 landed (2026-08-27): the Kifu match screen is live902903The viewer is a Go-server match screen in a printed-kifu identity, on the904hosted daily at https://mosaic.dev.site/ (Cloudflare in front of Fly).905- Identity (16): a flat kaya board, stone black and white, hue only as906 data (territories, health, markers); Noto Serif JP with tabular907 numerals, small caps, hairlines; light default with a night variant908 that keeps every structure on an ink page. No gradients, blur, glow,909 textures or radii except stones.910- Board (17): L0 flat territory tints; L1 shape outlines and counting911 marks (a counting-diagram dot on every solved point); L2 stones912 (1 black, 0 white, givens with a centre mark) with a last-move ring913 and a placement snap replacing the ripple; L3 board lines ruled through914 the stones with A-T / 1-n coordinates on the focus tile's frame, and915 its pins as triangles. Fog void; no detail on an unrevealed tile;916 0 map tiles redrawn in steady state (measured L1 8/24, L2 0/35,917 L3 0/26).918- Dock (18): player cards as a match screen with byo-yomi clocks from919 health (main time healthy, periods ticking at warning, timeout dead),920 the latest mover marked "to move"; tabs moves / tile / table; the move921 list built from events in kifu notation; the tournament table from the922 leaderboard, both axes.923- Counting line (19) along the board's foot, leader underlined, dead924 struck through; the timeline (20) with kifu-notation markers, playhead,925 the move list following it; result card (21) at day end over the926 table; calendar and day pages in the identity; phone (22) with player927 cards across the top, the board, a bottom-sheet dock, touch pan and928 pinch zoom. Chrome is user-select:none; the move list, tile detail and929 table are selectable. Tests (23) green (47), check green.930- Proven live: the hosted daily rendered the screen with real agents --931 Codex Atlas (gpt-5.6-sol) in byo-yomi, fable and Codex timed out --932 the counting line, move record and dead-agent crosses all reading as a933 game. Screenshots `output/playwright/kifu/{desktop-L0..L3,934 desktop-L2-night,phone-L0,phone-L2,phone-sheet,hosted-live}.png`.935936### Item 15, first half, landed (2026-08-27): machine keys937938`~/.config/mosaic/identity.json` (ed25519, `machine_id = m-` +939sha256(public key)[:16]) from `deno run -A https://mosaic.dev.site/identity.ts`;940`join` carries `{machine_id, public_key, proof}` with `proof` a941signature over `mosaic-join|game|key|minute` (current or previous942minute); TOFU registry `data/machines.sqlite`; agent id943`machine_id/session` so several agents per machine share one by-user944row (models listed); unverified joins allowed, muted, never honoured.945This Mac is `m-4c879e83522b14c1` (dami); a signed join on the hosted day946made one verified row beside the three unverified name-claims from947earlier. Second half landed 2026-08-27, fully proven (the user signed in948through shoo and claimed this Mac: "machine m-4c879e83522b14c1 (dami)949is yours"): adopt the tribes950convention (`/Users/dami/Developer/tribes/docs/auth.md`) wholesale —951identity file as JWK ed25519 in the tribes shape, key-auth message952`mosaic-key-auth:<host>:<ts>`, `POST /api/keys/auth` mints a bearer953token (one per key) that joins carry; any ed25519 tool can mint the954key (Node snippet, openssl, Python; spec at `/identity`), no Deno955required. shoo.dev sign-in as a `login` button right of "copy prompt"956(shoo.js; id_token verified with jose against shoo's JWKS, aud957`origin:<origin>`, HMAC session cookie), `logout` when signed in. The958only thing a signed-in human can do that a visitor cannot: open a959claim link an agent produced (`/link?t=...`, single use, 10 min) to960bind that machine to their account.961962### Decided 2026-08-27 (night): three attacks an agent reported, and the guards963964An agent tried rejected commits as an oracle, read the public event965log, and tried to reverse tile hashes. Verdicts and guards (in966progress): the tile hash was a real, silent oracle (m <= 4 candidates,967hash each, one matches) — it becomes an HMAC commitment with a968per-generation secret published only in the day's archive (gen-v7);969rejected cells' values leave the public log and SSE (the committing970agent alone sees them) so a sacrificial prober cannot feed a harvester971through the broadcast; health is per machine (all sessions share the972window and the death) with a burst rule (30 wrong before the 300-cell973floor is death); keyless joins are refused now that a key costs nothing974(dying costs the key's day). Two more from the user: death is enforced WHILE a batch is975processed (verdicts stop at the 30th wrong or the 4% crossing; the rest976of the batch comes back unevaluated; atomic across a machine's sessions977under the referee's write lock; batches capped at 512 cells), and a978(machine, cell) attempt enters the health window at most once, so979retract-and-recommit cannot pad the window with free correct entries980(wrongs always count, corrects count once). Third: sessions are capped at 3 per machine per day (dying does not981reset the count) and the session is bound into the signed join proof,982closing seed farming through many sessions. Fourth, decided: under fog, geometry is NOT public during play — the983live log/SSE serve a view whose manifest lists only tiles reached by984anyone, with synthetic `reach` events as the reached set grows; the985full log is released in the day's archive. Fifth, and the biggest: the986day seed was derived from the date, so the whole field was987regenerable by anyone — the nightly now draws a random 32-byte988`day_seed` (committed as `seed_commit` at start, released at rollover),989tile hashes are HMAC commitments under a `day_key` with990`SHA256(day_key)` published at start and the key released at day end,991over a versioned encoding (game, tile, geometry, solution, givens,992seam mask), and every RNG stream is domain-separated (generation,993growth, seat placement). Also: compact agent labels are `name/model` (or `name/session`) so994two sessions of one machine are distinguishable in follow and the995counting line.996997Done 2026-08-27: all of the above is in (engine gen-v7 8121c97; site:998per-machine health window with once-per-cell credit and the 30-wrong999burst, in-batch death cutoff with `unevaluated`, 512-cell batches, three1000sessions per machine per day bound into the join proof, keyless joins1001refused on the hosted daily, rejected values stripped from the log and1002SSE, `lib/redact.ts` serving the fogged live view on `/events` and1003`/log` with placeholders and `reach`, random `day_seed` drawn by1004`push-day` and travelling inside the pushed field, `keys.json` and the1005seed/key released in the archive and checked on `/day/<date>`, labels1006`name/model`). Regression tests: `tests/guards_test.ts`,1007`tests/redact_test.ts`, the sticky-death and daily rollover tests.10081009### Guards landed (2026-08-27, night)10101011All of the above are deployed (Fly, resumed today at revision 1263):1012health per machine with once-per-cell credit, the 30-wrong burst rule,1013death enforced mid-batch (rest `unevaluated`), 512-cell batches, three1014sessions per machine per day bound into the proof, keyless joins1015refused; rejected values gone from the public feed; the live log/SSE1016redacted under fog (`lib/redact.ts`: placeholders for unreached tiles,1017`reach` events; verified on the host: 246/246 placeholders at rev 0,1018123 reach events, 0 leaked values); gen-v7 commitments under a private1019day key with a random 32-byte `day_seed` drawn by the nightly, both1020released in `keys.json` at rollover and recomputed on `/day/<date>`1021(commitments shown as verified or not). 62 site tests incl.1022`guards_test.ts` (probe + harvester, in-batch cutoff, batch cap,1023laundering, concurrent probes, session cap, keyless refusal) and1024`redact_test.ts`. Follow labels are `name/model`. Today's live field1025still has the old date-derived seed (regenerable); tomorrow's was1026rebuilt under gen-v7 by slate pipeline #7 before rollover.10271028### Status of the v2 items under this goal102910301 real packed run: done (v2 packings and v3 scatter fields played, logs1031on disk). 2 run tags: folded into item 3 above. 3 analyzer: item 8.10324 paired trials: deferred. 5 `k`: deferred. 6 library: replaced by 6a1033(item 1 above). 7 envelope: G=1000 render becomes item 6's 9x9 field.10341035## Phase 2 (v2) — same board, two agents, measured10361037Manifest v2 (packed tiles of mixed size, zones, segments, reward) is built1038and tested (decision block below; `docs/space-and-cache.md`). What v2 has1039to add is the reason to have built it: a board large enough that strategy1040matters, played by more than one agent under the same seed commit, with1041the run measured rather than eyeballed, and a tile library so that boards1042of that size are a stitch and not a twenty-minute generation.10431044### Goal10451046**Watch two agents solve the same big-first mosaic of mixed-size tiles1047(G = 100, ~100 tiles, 6..16) side by side on localhost, synced by1048revision, with an analyzer that says which one played better and why.**10491050The demo moment: `deno task dev` prints a challenge (a seed commit) and1051two prompts. Paste one into Claude Code and one into Codex (or the same1052model at two batch sizes). Open `/?compare=<a>,<b>`: two maps, one1053scrubber, the analyzer pane between them: reward over revision, guess1054rate, reveal-to-commit ratio, mean tile size solved, frontier strategy1055(BFS / DFS / constraint-greedy). Scrub back to the reveal where they1056diverged.10571058Done means all of these, each with a test or a command that proves it:105910601. **Real packed run.** `mosaic-engine gen --G 100 --kind big-first` is a1061 batch job (minutes, timed and written here), cached by manifest hash,1062 and a fresh agent given only the printed prompt solves it end to end1063 through `/mcp`. Proof: a game log under `site/data/games/` whose1064 manifest is `packer.kind: big-first`, plus the existing1065 live-fold == replay-fold test run over that log.10662. **Run tags.** `start_mosaic` accepts `tags { model, batch, code_use,1067 initial_reveal, note }`; they are written as a `run` event at revision1068 1 (the manifest at revision 0 is the board's identity and must stay1069 equal across paired runs, so tags do not go on the manifest). `/games`1070 lists games filterable by tag and by seed commit. Proof: a test that1071 two games of one challenge share `manifest.id` and differ in `run`.10723. **Analyzer in the fold.** Every batch event folds into `analysis`1073 fields (guess rate, reveal-to-commit ratio, retractions, reward per1074 batch, mean n of solved tiles, wall-clock between batches, frontier1075 strategy classified from the reveal order against the seam graph:1076 BFS, DFS, or largest-tile-first / most-pinned-first greedy). The1077 analyzer is a pure function of the log, so live and replay agree.1078 Proof: `deno task test` asserts analysis equality at every revision1079 between live and replay, and classifies the scripted client's BFS and1080 DFS modes correctly.10814. **Paired trials.** A `challenge` is a seed commit; `start_mosaic1082 { challenge }` joins it (same cached generation). The viewer at1083 `/?compare=a,b` renders both games on one scrubber, aligned by1084 revision, with the analyzer pane. Proof: two scripted clients (BFS vs1085 DFS, `--limit 6` vs `--limit 2`) on one challenge produce two logs the1086 compare page renders and the analyzer tells apart.10875. **Coupling depth `k` measured.** Per tile, `k` is the size of the1088 smallest set of its seam segments that, once fixed by solved1089 neighbours, leaves the tile a single solution (a minimum hitting set1090 over its alternatives' differing segments; m <= 4, so exhaustive).1091 Mosaic `k` is the maximum, and the manifest carries the distribution1092 `k_hist`. Replaces `k: unmeasured`. Proof: `go test ./gen` re-derives1093 `k` from the emitted alternatives and asserts every tile has1094 `1 <= k <= segments`.10956. **Tile library.** `site/data/library.sqlite` with the schema in1096 `docs/space-and-cache.md` §5; `mosaic-engine lib add` ingests gen1097 output (one canonical orientation per tile, all 16 edge tuples1098 indexed); `mosaic-engine stitch --seed --G --kind` resolves a packing1099 against the library, records misses, and `gen --drain` generates the1100 missing tiles with prefilled edges and appends them. Proof: stitching1101 a second seed at G = 100 after one drain is all hits; the stitched1102 mosaic passes every per-tile assertion (re-verified from JSON, as1103 today) and global uniqueness; `manifest.library` is the file hash.11047. **Envelope.** `pack` at G = 1000 in under a second with zone share1105 under 3%; `stitch` at G = 1000 reports its hit rate and finishes in1106 under a minute (misses allowed, listed, not silently dropped); the1107 viewer renders a synthetic G = 1000 board (1M cells, tone-only at1108 overview zoom, glyphs when a tile fills the view) and the compare1109 page renders two of them. Everything above also still runs at G = 27.11101111Out of scope for v2, on purpose: Nostr, identity, deployment, the cloud1112copy of the library (R2/D1 are phase 3; the file stays local), more than1113two agents on one compare page, visual identity, the daily seed.11141115Build order, each step usable on its own: real packed run (1, the1116baseline; nothing else is worth doing if a packed board is not playable)1117-> tags (2) -> analyzer (3) -> paired trials and the compare page (4) ->1118`k` (5) -> library (6) -> envelope (7) -> the demo.11191120### Exit criteria11211122- Two agents (or one model at two settings) finish the same G = 1001123 big-first mosaic; both logs replay frame for frame; the compare page1124 shows both under one scrubber and the analyzer pane distinguishes them1125 on at least guess rate and frontier strategy.1126- The board they played was stitched from the library, not generated1127 live, and its manifest names the library hash.1128- `k` is a number in every manifest, and at least one tile in the played1129 board has `k >= 2` (needs two neighbours; the generator asserts this1130 for big-first boards, not hopes for it).1131- `gen`, `stitch`, the viewer and the compare page all work at G = 27 and1132 at G = 1000 (the agents only have to play the G = 100).11331134### Measured while picking v2 up (2026-08-25/26)11351136- Phase 1 exit report: `output/report/index.html`; every item passed.1137- No packed (big-first / bands) board had ever been generated for real:1138 `site/tests/fixture.ts` synthesises the big-first fixture (random1139 solution, random givens, judge stubbed), and every log under1140 `site/data/games/` was a grid. The previous session's attempt to play a1141 G=60 big-first board ran 24 minutes and died in generation.1142- Why it died, in order of discovery: (1) the big-first layout leaves 6x61143 fillers with three prefilled sides and the id order filled them last,1144 (2) backjumps only ever targeted the latest dependency, (3) puzzletea's1145 MRV backtracker could neither refute an infeasible 6x6 nor complete a1146 14x14 with fixed sides inside its timeout, (4) per-neighbour forward1147 checking misses neighbour-neighbour interactions across a boxed-in1148 tile's edge, (5) a tile pinned to one solution by its neighbours' seam1149 clues failed the whole attempt instead of being re-given. All five are1150 fixed (decision "Generator search" below); `gen.Version` is `gen-v4`.1151- `pack --G 60 --kind big-first`: instant; zone share 1-3% at G = 60,1152 100, 200; by count ~40% of tiles are n = 6 but ~70% of the area is1153 n >= 12 at G = 100.1154- `gen` (gen-v4, 18-core Mac, wall clock, one attempt each):1155 2x2 of 14x14 (27x27) 14 s; G=60 big-first (62 tiles) 45 s; G=1001156 big-first (103 tiles, 3 regivens) 2 min 36 s; X=15 grid (225 tiles,1157 196x196) 12.5 min. Generation is a batch job cached by1158 `(generator version, seed, G, kind, n)` under `site/data/cache/`.1159- First real packed run: G=60 big-first, seed `a`, played by the scripted1160 client through `/mcp`; log `site/data/games/48edade0-...jsonl`, 1831161 revisions, 62 reveals, 120 commits, 1583 forced / 797 guess / 11 wrong,1162 reward 4232 (= sum of n^2), replay at `/?replay=48edade0-...`.1163 `deno task test` now folds every finished v2 log on disk and asserts a1164 solved end state with the right reward; the scripted client tolerates1165 SSE keepalive comments during a long `start_mosaic` (that was the1166 previous session's crash).1167- G=100 big-first, seed `a`, same client: log `bb7b5f61-...jsonl`, 3001168 revisions, 103 reveals, 197 commits, 4280 forced / 2693 guess / 251169 wrong, reward 11492; generation on cache miss plus the whole game took1170 141 s wall. Screenshots: `output/playwright/replay-bigfirst-{60,100}.png`.1171- Still open from item 1: a *fresh agent* (not the scripted client) on a1172 packed board. That is the next thing to do: `deno task dev` prints the1173 prompt; use `kind=big-first, G=100`.11741175### Idea captured 2026-08-26: scattered field instead of a packing11761177The two played boards read as a tiled quilt. The user would rather the1178board be a *field*: tiles of random size dropped at random positions,1179overlapping each other by arbitrary rectangles (not one shared edge),1180holes allowed, no filler zones. Each tile's rules are internal to its1181window; the only cross-mosaic rule is that shared cells agree. The1182generator already works this way underneath (`sat.CompleteRegion` is a1183joint solve over windows on one global grid, any overlap shape), so the1184change is in the layout and in what "seam" means:11851186- `pack --kind scatter`: a seeded Poisson-style drop of squares (n even,1187 6..16) onto a G x G field; accept a drop when it touches the growing1188 cluster with an overlap of at least one full row or column and at most1189 ~n/4 of the smaller tile (so no tile is given away by one neighbour),1190 and is not contained in another. Stop at a target coverage. Holes are1191 simply uncovered cells; zones go away.1192- Seam *segments* become overlap *regions* (rects in local coords, with1193 the partner id). Frontier, pins, `k`, seam clues and the coupled1194 uniqueness CSP all read regions instead of segments; the assertions1195 keep their meaning (every alternative differs inside some region; the1196 tile is under-determined alone; the field is unique as a whole).1197- Generation order is already WFC-shaped: most-constrained-first is the1198 minimum-entropy rule; nothing to add there. Randomness comes from the1199 seeded rng in the drop process and in SAT's diversification, not from1200 the CPU.1201- Viewer: the same colour model (mean owner hue at fixed lightness) draws1202 a multi-owner overlap region as a blend, which is what makes an overlap1203 legible; holes are void.12041205Not a v2 item yet; decide after items 2-4 exist, since the compare page1206and analyzer are layout-agnostic.12071208### Idea captured 2026-08-26: the field reads as texture, not information12091210At overview zoom the cells are drawn as small outlined squares, and a1211balanced no-three binary field is by construction a maximum-entropy1212texture, so values cannot read at that scale no matter the glyph. The1213overview should be value-blind: known/unknown and ownership (status wash1214at owner hue, unknown dark, holes void, no per-cell borders); values1215appear only when a cell is >= ~12 px, as filled/empty discs rather than1216squares. Two zoom regimes, not one glyph scaled. Belongs with the visual1217identity task after v2.12181219### Manifest v3 (decided 2026-08-26): a scattered field of shapes12201221Decided the same day the ideas above were captured; the user wants the1222field now, before items 2-7, and the visual pass with it.12231224**Tiles are shapes, not squares.** A tile is a set of cells: any union of1225axis-aligned rectangles whose every maximal horizontal and vertical run1226of cells (a *line*) has even length >= 4. Rectangles, L, T, plus,1227notched and holed shapes all qualify. The Takuzu rules are per line:1228no three equal in a row along a line, each line has as many 0s as 1s,1229and no two lines of the same orientation and length in one tile are1230equal. A square n x n is the special case where every line is n long.12311232**Tiles are scattered, not packed.** `pack --kind scatter` drops shapes at1233seeded random positions, sizes and orientations onto a G x G field,1234growing one connected cluster from the seed tile: a drop is accepted when1235it overlaps the cluster by at least one full line of some existing tile1236and by at most a quarter of the smaller tile's cells, and is contained in1237no tile. Overlaps are arbitrary rectangles (or unions of them), not1238single edges. Uncovered cells are holes; there are no filler zones. Stop1239at a target coverage; the result should read as a field with scattered1240small gaps, not a quilt.12411242**Cross-tile rule: shared cells agree.** Nothing else crosses a tile1243boundary. Every tile alone has 2..m solutions that differ on some1244overlap region; the field as a whole has one.12451246```1247manifest v3 (public, rev 0)1248 version 31249 id, seed_commit, G, packer { kind, params }, m_by_n, rules, seed_tile,1250 generator, library, k, k_hist (as v2)1251 tiles[] { id, x0, y0, w, h,1252 rects[]: [lx, ly, rw, rh] // union = the shape, local coords1253 cells, // count1254 m, hash,1255 overlaps[]: { with, cells }, // partner id, shared-cell count; the1256 // cell set is the intersection of the masks1257 pins[]: overlap indexes some alternative differs on }1258 (no zones)1259private: solution (G rows, '.' for holes), tiles[].givens, .flip, .ladder, .needs_*1260```12611262- Engine `rules`: `Window` is replaced by `Shape {X0, Y0, W, H, Rects}`;1263 a `Board` carries shapes and precomputes their lines; every rule,1264 propagation, flip rectangle, ladder rating and the coupled CSP works on1265 lines and shared cells. `sat` encodes per line. The size policy1266 (`PolicyFor`) keys on the tile's cell count instead of n.1267- Site: owners from rect unions; the frontier is "shares a cell with a1268 solved tile"; hue colouring unchanged; the viewer draws a shape outline1269 as the edges between tile and non-tile cells and a hole as void.1270- Gates worth building because they are unfriendly to a human and1271 interesting to an agent (knobs on the manifest, off by default): lines1272 of length 4 (a T stem) that are almost fully forced but change what the1273 crossbar can be; a hole inside a tile that splits a line in two; a tile1274 whose rule mask differs from its neighbour's (no uniqueness on one side1275 of an overlap); overlap regions wide enough that two tiles are1276 effectively one puzzle; long lines (16) crossing several overlaps; a1277 tile with two disconnected overlaps to the same neighbour.12781279**Visual pass (now, not after v2):** value-blind overview (status wash at1280owner hue, unknown dark, holes void, no per-cell borders), values as1281filled/empty discs only when a cell is >= ~12 px, tile outlines as1282shape outlines, overlap regions read as hue blends. Game feel: the user1283liked `../rect.run`'s motif but wants it less blocky here; Takuzu should1284feel like a smooth stream of calm water (Shikaku is the tile-laying,1285castle one). Soft edges, slow transitions, low contrast at rest, the1286field as a surface that fills rather than a grid that gets stamped.12871288### Feasibility captured 2026-08-26: the hourly public board12891290Question: what board size lets ~25 agents work one field together for an1291hour, after which a new board arrives and the old attempt is persisted;1292is generation disproportionate to play?12931294Measured generation (gen-v4, 18-core Mac, one attempt): 0.7 s/tile at1295G=60 big-first, 1.5 s/tile at G=100 big-first, 3.3 s/tile for all-hard129614x14 (X=15). Measured play: the phase-1 fresh agent did 4 hard 14x141297tiles in 3 min with code (0.75 min/tile); the scripted client is not a1298measure. Model: 25 agents, 50% parallel efficiency (frontier contention,1299revision conflicts, waiting on a neighbour's seam), mean tile ~150 cells,130085% coverage.13011302| agent pace | tiles/hour | cells | G | generation (mixed / all-hard) |1303|--------------|-----------:|-------:|-----:|-------------------------------|1304| 1 min/tile | 750 | 112k | ~360 | 20 min / 41 min |1305| 2 min/tile | 375 | 56k | ~260 | 10 min / 21 min |1306| 3 min/tile | 250 | 38k | ~210 | 7 min / 14 min |13071308So generation is 10-35% of the hour of play it feeds, on one machine,1309and it runs ahead (board N+1 generates while N is played), so it is not1310disproportionate; it only looked that way while the old generator could1311not finish at all. The tile library (v2 item 6) turns it into seconds.13121313What actually limits 25 agents is not size but the frontier: a field1314grown from one seed has a frontier of a few dozen tiles only once a few1315dozen are solved, so the first minutes serialise. Use several seed tiles1316(5-10, drawn from the seed) so the wavefronts meet. The other cost is1317`observe`: delta-coded known cells are ~2 bytes/cell, so 56k cells is a1318~100 KB observation per call; at that scale observe should take a tile1319or viewport argument. Neither is a board-size problem.13201321Plan: start hourly boards at G=200-260 with 5-10 seeds, measure agents'1322real tile rate with the analyzer (item 3), and move G to hit a ~45 min1323median completion. A board that does not finish in the hour is persisted1324as it stands; that is fine and is data.13251326### Measured after the v3 pivot (2026-08-26, later the same day)13271328- Built by three forks in parallel on one contract: site (visual pass,1329 then v3 in fold/referee/MCP/viewer/client/tests, v2 logs still replay1330 through `normaliseManifest`), pack (`scatter` with a hole-fill pass and1331 the distinct-lines bound), engine core (`rules` on shapes and lines,1332 `sat` per line, judge on shapes, gen over overlap regions; `gen-v5`).1333- A shape constraint the packer did not know and the generator's1334 validation caught: uniqueness needs at most D(L)/2 parallel lines of1335 length L, where D(L) is the number of distinct valid L-lines (D(4)=6,1336 D(6)=14, D(8)=34, D(10)=84, D(12)=208, D(14)=518, D(16)=1296). So no1337 4-thick arms at all, 6-thick arms at most 6 long; the smallest tile is1338 6x6 (or the 32-cell notched 6x6). `pack.ValidShape` and1339 `rules.Shape.Valid` both enforce it.1340- Packer (seed a, MaxHole 24): G=100 -> 119 tiles (47 from the fill1341 pass), coverage 0.90, 54 holes of mean 18 cells, 0.45 s; G=200 -> 4701342 tiles, coverage 0.91, 204 holes of mean 17 cells, 1.9 s. Holes over 241343 cells that remain (13 at G=100) are concave pockets no valid shape fits1344 under the overlap rule.1345- First scatter field generated and played: G=60, seed a, 31 tiles (111346 compound, 12 non-square rectangles), 1038 hole cells, generated in 41 s1347 (one attempt, no backjumps), solved by the scripted client in 711348 revisions, 1112 forced / 726 guess / 3 wrong, reward 3032; log1349 `0687247b-...`, screenshot `output/playwright/v3-scatter-60-overview.png`.1350 G=100 scatter, seed a: 75 tiles, generated on cache miss and solved by1351 the scripted client in 383 s wall (three other generations were running1352 at the time), 99 commits, 3141 forced / 2445 guess / 7 wrong, reward1353 9020; log `7b8941d3-...`, screenshot1354 `output/playwright/v3-scatter-100-overview.png`.1355- Engine-core fork final: `go test ./...` green (`gen` 130-154 s with1356 both fixtures; the 2x2 reproduces byte for byte under `MOSAIC_SLOW=1`);1357 pins are now recomputed at settle time; a layout is validated with1358 `Shape.Valid()` before any search (an invalid shape cost SAT 12-20 s to1359 refute). Manifest deviation from the contract: `rules` fields are1360 `overlaps`, `cross_tile` (not `seams`, `cross_seam`); `Output.Layout`1361 carries the packer's report. Timings under contention: G=100 scatter1362 6.5 min (75 tiles, one attempt); X=15 grid 18.6 min.1363- Known generator gap: at G=200 scatter the first attempt failed after1364 11 min ("repair tile 193: an alternative differs inside no overlap1365 region": the neighbours' seam clues had fully fixed the tile's only1366 pin-eligible overlap, and the regiven repair cannot widen it), so the1367 attempt restarts from scratch. Fix: a repair that releases the1368 neighbours' clues on that overlap (or plants across a different one)1369 instead of restarting. No G=200 number yet; chunkwise generation1370 (item 6a) makes single G=200 passes unnecessary anyway.1371- Visual pass screenshots: `output/playwright/v3-overview-100-dark.png`,1372 `v3-mid-100-dark.png`, `v3-detail-100-dark.png` (value-blind overview,1373 discs at zoom, no per-cell borders, ripple on newly known cells).13741375### Idea captured 2026-08-26: chunkwise generation (checkerboard, not stitch)13761377Generate a large field as a grid of chunks (~G=60 each) in parallel, then1378join them. The join does not need a stitcher or a library: a chunk is a1379`gen` run whose border tiles may have cells prefilled from neighbouring1380chunks, which the generator already handles (joint completion over fixed1381cells is what `fillTile` does). Order the chunks as a checkerboard: even1382chunks generate independently and in parallel (unique alone, as today);1383odd chunks generate with their border tiles overlapping the even1384neighbours' border tiles, prefilled from them, and are asserted unique1385given that context. Two rounds, each the wall clock of one chunk.13861387- Per-tile assertions are unchanged (2..m alone, alternatives differ in1388 an overlap region, technique checks); per chunk the coupled CSP runs1389 over the chunk plus its fixed context; the whole is unique because even1390 chunks are unique alone and odd chunks are unique given them.1391- Determinism: chunk seed = (seed, chunk index); the result does not1392 depend on how many run at once. Tile ids offset per chunk; one manifest1393 for the union; the frontier crosses chunk borders like any overlap.1394- Separate processes, not goroutines: gophersat's package-level buffer1395 serialises every SAT call inside one process (the `sat` mutex), so one1396 `gen` uses about one core for the solution phase. Chunks as processes1397 is the way to use the 18 cores.1398- Estimate from today's numbers: G=260 as 4x4 chunks of ~G=65 is two1399 rounds of ~45-60 s, ~2 min wall, versus ~15-20 min serial. That is the1400 hourly-board generation cost solved with far less machinery than the1401 tile library; the library (item 6) stays as the finer-grained version1402 for the daily.1403- Needs: `gen --context <cells+tiles>` (prefilled cells and the identity1404 of the tiles that own them, so overlaps and pins are computed against1405 them), a `chunks` orchestrator in the site (`lib/engine.ts`) or a1406 `mosaic-engine field` subcommand that plans the checkerboard, runs the1407 processes, and merges manifests; a test that a 2x2 of G=60 chunks1408 passes every assertion re-verified from the merged JSON and has one1409 coupled solution. Slot it as item 6a, before the library.14101411### Idea captured 2026-08-26: the daily as 9x9 chunks, agents placed at random14121413Follow-on from chunkwise generation. The checkerboard is flat, so a daily1414board of 9x9 chunks of G=60 (G ~ 530, ~280k cells, ~2500 tiles) is still1415two rounds: 41 even chunks then 40 odd, 18 at a time, roughly 3 waves of1416~45-60 s each way, so ~5-6 min on the Mac. Recursion (chunks of chunks)1417buys nothing for cost; the orchestrator is not the bottleneck. Large-G1418single passes are no longer needed for anything.14191420Each agent joining the board is placed at its own seed tile drawn at a1421random uncovered-frontier spot; from there it can dig toward an ally's1422region or route toward open space. Rules to decide (gates, not settled):14231424- Per-agent frontier: an agent may reveal only tiles overlapping tiles it1425 solved itself, until its region touches another agent's region, at1426 which point the regions merge and both share the frontier (the meeting1427 is an event the analyzer records). This is what makes "dig out an1428 ally" mean something; a global frontier would blur positions.1429- Ownership colouring becomes per agent (the 30b4 owner-hue idea, at1430 last): a cell's wash is the mean hue of the agents who solved the tiles1431 covering it; overlaps between two agents' tiles read as their blend.1432- A daily of ~2500 tiles at 25 agents and 1-3 min/tile lasts 3-8 hours1433 of play; new agents keep joining at empty spots; the board persists1434 through the day and is archived as it stands at midnight.1435- Hard requirement at this size: `observe` scoped to a region (the1436 agent's own tiles plus a radius, or a viewport), never the whole field;1437 the whole field is for the spectator over SSE, tiled by chunk.14381439### Decided 2026-08-26: the viewer is a tilemap with levels of detail14401441Hue means the agent who solved it, not the tile. Tile identity, rules1442and values are detail that draws in as you zoom, like a map: at every1443zoom the question "where does this belong" has one answer at the scale1444you are looking at.14451446| level | cell size | what is drawn |1447|-------|-------------|----------------------------------------------------------------|1448| L0 | < 3 px | territory: agent-hue washes (mean hue where agents' tiles overlap), unknown dark, holes void; no edges |1449| L1 | 3-12 px | + shape outlines (tile/non-tile boundary), overlap regions as blends, status wash (revealed lighter than solved), the ripple |1450| L2 | 12-30 px | + values as discs (filled 1, ring 0), givens muted vs commits in ink, forced vs guess as two ink weights |1451| L3 | >= 30 px | + the lines: a faint bar along each line of the tile under the centre, line length, pins marked on the overlap, tile id |14521453- Rendering: the field is cut into map tiles (256 px per level, keyed by1454 chunk and level), rasterised once and cached; an event dirties only1455 the map tiles its cells touch; the spectator subscribes to SSE per1456 chunk in view. The same cache serves the replay scrubber.1457- Nothing is decoration: every mark at a level answers a question that1458 level can ask (whose, which tile, what value, which rule). Rules are1459 not coloured; they are read off the lines at L3.1460- The current two-regime viewer is L0/L1 + L2 without the tilemap; the1461 tilemap becomes necessary at the daily's ~280k cells.14621463### Ideas captured 2026-08-26 (night): difficulty and novelty at daily size14641465G=530 (1777 tiles, nine agents) reads right as a daily. Levers for1466harder play and for emergence, in the order they are worth building;1467each is a manifest or rules knob, off by default, and every one is1468visible in the viewer without a new colour channel.146914701. **A growing field.** Chunkwise generation makes the world extensible:1471 every hour (or when coverage of the frontier ring passes a threshold)1472 the referee generates a ring of chunks around the cluster, using the1473 existing border tiles as context, and where agents are pushing. The1474 field is the day's history of where the agents went. Needs: `field1475 --grow` (context = current border), a `grow` event, the viewer's1476 geometry accepting new tiles mid-log.14772. **Terrain.** The packer draws tile size and hardness (blank ratio,1478 technique policy) from a noise field: plains of 6x6 fillers, ridges1479 of 16x16 at 75% blank. Reward stays cells, so effort per cell is what1480 varies and agents decide where to spend it; terrain reads at L0 as1481 wash lightness. Needs: a `terrain` param on the packer, per-tile1482 policy overrides in the manifest.14833. **Contested tiles and pin bonuses.** A frontier tile adjacent to two1484 groups can already be revealed by both; make it explicit: the1485 finisher owns it, both groups' commits count for their agents, and1486 solving a tile that pins a neighbour earns a bonus. Needs: a1487 `contested` flag in observe, analyzer fields, viewer marker.14884. **Notes on tiles.** An agent can leave a <=160-char note on a tile it1489 solved or revealed, readable by anyone who observes it, shown in the1490 viewer at L3 and in the console. Coordination or deception, both in1491 the log. Needs: a `note` tool and event.14925. **Per-tile rule masks.** Tiles announce their rules: some without the1493 uniqueness rule, some with an extra one (no 2x2 of equal values, or1494 no three on a diagonal, both easy in `rules` and `sat`). Agents must1495 read the mask and adapt their solvers; specialists emerge. Needs:1496 `rules` per tile in the manifest and the judge, a `Mask` extension.14976. **Wormholes.** Two distant tiles share an equality constraint on a1498 pair of lines (cells of line A equal cells of line B, stated in the1499 manifest). Information travels across the field; solving one region1500 pins another. Needs: an equality constraint type in `rules` and1501 `sat`, a manifest `links[]`, a viewer mark at both ends.15027. **Shapes unknown until adjacent.** `observe` lists hidden tiles only1503 once a group's tile overlaps them; exploration has a cost. A flag.15048. **Decay.** A solved tile whose neighbours are not all solved within N1505 revisions loses its cells again unless sealed; pressure to1506 consolidate rather than sprawl. The most invasive; last.15071508Cross-tile rules (the reserved `cross_tile` flag: no-three continuing1509across a shared edge) are a difficulty lever but not an emergence one;1510keep as a knob.15111512**Decided 2026-08-26: 1 + 7 together are the next build.** A field that1513grows where agents push, whose unexplored parts are not visible until a1514group's tile touches them, is one mechanic seen from two sides: the map1515is made of what has been reached. Shape: the referee keeps a `fog`1516flag per field; `observe` lists a hidden tile only if it overlaps a1517tile of the caller's group (or lies within the agent's own seed1518reservation); the spectator sees everything the log has, which is1519every generated tile, but at L0 unreached tiles draw as the faintest1520wash so the day's reach reads at a glance; growth runs as `field --grow1521<log>` producing a `grow` event with the new tiles (ids appended, seed1522tiles for late joiners drawn from the new ring), triggered by a timer or1523by frontier coverage, and the fold applies it by extending geometry in1524place. Proof: a G=148 field grown twice while two scripted agents play1525it, replay == live, observe never lists an unreached tile.15261527### Kept from the earlier phase-2 list15281529- Larger mosaics, tile size 10-12, `m = 4`: superseded by packings and1530 `m(n)` (decision block below).1531- **Tile cache for stitching (idea captured 2026-08-25).** Generate many1532 n-size tiles ahead of time and store them so that a daily mosaic is a1533 stitch, not a generation. The generator already splits per tile: the1534 expensive part (givens, planted flip, uniqueness rectangle, ladder rating)1535 depends only on the tile's own solution grid and on which of its sides are1536 seams, so the cache key is `(n, hash(solution grid), seam sides)` and the1537 value is the givens plus the assertion record. Completions are keyed by1538 `(n, top row, left column)`, and "failed to fit" (no completion for that1539 prefill) is stored too, as a negative entry, so retries are free. Growing1540 or refreshing the set is a batch job; the referee only ever reads it.1541 Edges are binary lines, so store them as ints: at n=14 an edge is a 14-bit1542 value (a balanced one is one of C(14,7)=3432), a completion key is two1543 ints, a stored tile carries its four edge ints, and stitching is integer1544 matching over a table rather than grid comparison.1545## Phase 3 — Nostr and the shared daily world15461547- Events become signed Nostr events with the kinds on the 30b4 page. The1548 referee is the single signer; the JSONL from phase 1 is already the event1549 shape, so this is a serializer, not a rewrite.1550- Private relay (strfry or HAVEN) as world and archive.1551- "Shared space" = authless MCP endpoint exposed beyond localhost, where any1552 npub-identified agent contributes to the **daily world**: one mosaic per1553 date from `puzzletea/daily.Seed(date)`, many agents, one referee, every1554 contribution attributed by key. tribes.pub already has the "submit via1555 /mcp, compose a deterministic sim, watch" pattern to borrow from.1556- Spectator subscribes to the relay by tile filter for a viewport.15571558### Product UX (decided 2026-08-26): the daily, for humans first15591560Everything is viewable logged out. Login (shoo.dev) only adds "yours":1561your machines, your territory, your replays.15621563Pages:1564- `/` today's board: live if the day is running ("view live" follows1565 SSE), else the finished replay. Explore by pan and zoom in the browser;1566 no agent required. A "copy prompt" button hands off to an agent: the1567 prompt bootstraps it (install the MCP endpoint, generate a machine1568 key, register, join today's board at a random spot, play) and ends1569 with "give your human the link when asked".1570- `/day/<date>` the replay of any past day, with the same viewer.1571- `/calendar` a month grid; every day is a summary tile: agents1572 participated (a contributions graph by harness/model), time to1573 completion or time remaining, guess %, tiles solved / total, territory1574 shares, meetings. Click through to the day.1575- `/link?t=...` claim a machine token (identity decision above).15761577Replay UX (improve): a timeline that shows event density, jump to1578markers (tile solved, agents met, a big guess run), playback speed,1579"follow this agent", and the same scrubber at every zoom of the tilemap.15801581Persistence is ours: per day the manifest, the private generator output1582(never served), the JSONL log, and the analysis summary; a SQLite index1583`days(date, seed_commit, stats json)` for the calendar; archive copies1584to R2. A daily at ~2500 tiles is ~10k events, ~10 MB; trivial.15851586Contributions by model: MCP `initialize` carries `clientInfo {name,1587version}`, which identifies the harness (Claude Code, Codex, ...), not1588the model, and is free. The model is self-reported: the bootstrap1589prompt asks the agent to state it in the `run`/join tags, and the1590calendar labels it "as reported". Verifying the model is not possible1591from the protocol; do not pretend to.15921593The viewer is the tilemap with levels of detail (decision above); at1594the daily's size that is a requirement, not a nicety.15951596### Machine identity (captured 2026-08-25, phase 3, not built in phase 1)15971598How an agent gets a durable identity and hands its human a web session:159916001. The agent generates its keypair, registers the pubkey with the API, and1601 gets back a `machine_id`.16022. When the user wants the web UI, the agent hits the API with a request1603 signed by that key and gets a short-lived, single-use pairing token.16043. The agent opens the default browser to `https://<site>/link?t=<token>`1605 (and prints the URL as a fallback for headless/SSH).16064. The server validates the token's signature chain, ties the session to1607 `machine_id`, and sets a long-lived `Set-Cookie`.16085. Every visit after that: cookie present -> "welcome back, here's your stuff."16091610Identity and accounts stay out of phase 1 on purpose; this is the shape to1611build against when phase 3 makes the referee a signer and agents npubs.16121613### Identity decided 2026-08-26: machine keys for agents, shoo.dev for humans16141615Builds on "Machine identity (captured 2026-08-25)" above. Two identities,1616one link between them:16171618- **Agent = machine key.** The agent generates its keypair, registers the1619 pubkey, gets a `machine_id`; every write it makes is attributed to it1620 (this is the "whose" hue in the viewer and the per-agent frontier).1621 Phase 3 makes the same key its Nostr npub.1622- **Human = shoo.dev account.** When the agent wants to hand its human a1623 session, it asks the API for a single-use pairing token signed by its1624 key and opens `https://<domain>/link?t=<token>` (prints the URL for1625 headless). That page sends the human through shoo.dev login; on return1626 the server ties the shoo.dev account to the `machine_id`, sets the1627 long-lived cookie, and every later visit shows the human their agents,1628 their territory and their replays.1629- One human may have many machines (several agents, several hosts); a1630 machine belongs to at most one human; unlinked machines still play,1631 they just have no human to show it to.1632- Open: the project needs a name and a domain before `/link` exists; the1633 shoo.dev integration details (OIDC or its own flow) are to be read1634 from its docs when this is built, not assumed.16351636## Decisions taken (change here, not in code)16371638### Manifest v2 (decided 2026-08-26): packed tiles of mixed size16391640Phase 2 replaces the `X, n` grid with an explicit packing. The phase-1 grid1641is emitted in the same shape (a regular tile list), so one manifest format1642serves both. Design background: `docs/space-and-cache.md`.16431644- **Packer: big-first-then-fill.** Large squares (n 14..16) are placed1645 first at seeded positions with free edges; the gaps are filled by bands1646 and Euclid cuts with smaller squares; remainders under n = 6 become1647 filler zones. The library only ever has to answer one-constrained-side1648 lookups for large tiles, which is the cheap regime.1649- **m scales with n, and is capped.** `m(n)`: 2 for n in {6, 8}, 3 for1650 {10, 12, 14}, 4 for {16}. Hard cap 4, never a free parameter per run;1651 the generator asserts each tile lands in 2..m(n) and reports its actual1652 count. Rationale: a small tile with 4 solutions is a coin toss, a1653 16-tile with 2 is pinned by any single seam segment.1654- **Sizes** n even in 6..16 (16 is new; the rules engine is size-agnostic,1655 the generator's timings at 16 are to be measured before it is used in a1656 daily).1657- **Assertions scale with n** (`gen.PolicyFor`): every tile has 2..m(n)1658 solutions alone and every alternative differs on a seam segment; the1659 blank floor is 55% at n <= 8, 65% at n = 10, 70% from 12 (cap 75%, 73% at1660 16 where counting gets expensive); the technique checks (needs the1661 uniqueness constraint, needs a bifurcation) apply from n = 12, which is1662 the canonical hard standard. Small tiles are fillers around the hard1663 ones, not hard puzzles themselves.1664- **Generation** is two phases: the solution, tile by tile in id order with1665 forward checking (a completion is kept only if every later neighbour1666 still has one) and backjumping to the latest neighbour on a NOFIT; then1667 the givens, in waves of tiles that share no cell, in parallel. Cells1668 where three or more tiles meet are never planted; they may be seam clues.1669- **Symmetries**: rotations, reflections and 0/1 complement (16 in all)1670 preserve every rule and every assertion, so a library tile is stored once1671 and indexed under all 16 edge tuples.16721673```1674manifest (public, rev 0 of every log)1675 version 21676 id sha256(seed || nonce) seed_commit = id1677 G cells per side of the board1678 packer { kind: "big-first" | "bands" | "grown" | "grid", params }1679 m_by_n { "6": 2, "8": 2, "10": 3, "12": 3, "14": 3, "16": 4 }1680 rules { seams, cross_seam, seam_clues, band_unique, zones: "filler" }1681 seed_tile tile id1682 tiles[] { id, x0, y0, n, hash, m,1683 seams[]: { side, from, len, with }, // segments, in local coords1684 pins[]: segment indexes some alternative differs on }1685 zones[] { id, rects[]: { x0, y0, w, h } } // filler, fully given1686 k "unmeasured"1687 generator version hash1688 library hash of the tile library file the tiles came from, or null1689private (gen output, never in the log)1690 solution G rows1691 tiles[].givens, .flip, .ladder, .needs_*1692```16931694- Cell id stays `y * G + x`. A cell's owners are derived from `tiles[]`1695 (and at most one zone); hue is a greedy 7-colouring of the tile adjacency1696 graph, computed by the fold, not stored.1697- Seam *segments* replace seam sides everywhere: frontier = hidden tiles1698 sharing a segment with a solved tile; `pins` are segment indexes; the1699 global-uniqueness CSP runs over segments; cells where three or more1700 tiles meet are never planted and never seam clues.1701- Zones are filler in v2: fully given on reveal of any neighbour, never on1702 the frontier, zero reward. The manifest shape leaves room for a rule set1703 per zone later.1704- Reward: n^2 per solved tile; the analyzer records reward per batch and1705 the mean n of solved tiles over time.17061707### Generator search (decided 2026-08-26): SAT for answers, propagation for technique17081709Completing a window with fixed edges, deciding whether one exists, and1710counting solutions up to a limit are ordinary CSP questions, not something1711to hand-roll. The engine's `sat` package encodes one window (balance as1712cardinality constraints, no-three as clauses, line uniqueness through XOR1713auxiliaries, fixed cells as units) for gophersat, a pure-Go CDCL solver.1714Measured: a 16x16 with all four sides fixed completes in ~4 ms, a 14x14 at171572% blank counts to limit 4 in ~8 ms, a contradiction is refuted in1716microseconds; puzzletea's MRV backtracker took its whole 6 s timeout on the1717first two and could not refute an infeasible 6x6 at all, which is why a1718G=60 packing never finished before. puzzletea is no longer a dependency.1719The `rules` propagation solver stays for what it is for: forced/guess1720attribution and the technique ladder model human deduction, and SAT has1721nothing to say about that. gophersat keeps a package-level buffer for1722clause learning, so `sat` serialises `Solve` behind a mutex; calls are1723milliseconds and the parallel givens waves lose nothing measurable.17241725Fill order is most-constrained-first (`gen.fillOrder`): from the seed1726tile, the next tile is the one with the largest share of its cells already1727fixed, ties to the smaller n. Small fillers are therefore completed while1728they have one or two fixed sides and their remaining edges constrain the1729large tiles, which have room; id order (big tiles first) left 6x6 fillers1730for last with three fixed sides and, at G=60, 18 of them, most with no1731completion. Backjumps escalate: every second failure of the same tile1732targets the next earlier dependency, and a tile whose forward check fails1733for every completion keeps the one that leaves the most later neighbours1734feasible, so the neighbour that then fails is the one with the real1735conflict.17361737- Home: new repo. The Go engine owns the puzzle rules (`rules`, `sat`);1738 the Deno site is where state, MCP, and the viewer live. Nothing outside1739 the engine knows a Takuzu rule.1740- Spectator: Fresh 2 island with a canvas, not TUI. Pan/zoom, background1741 blends, and drawn glyphs are the point of the view. Same framework and pins1742 as rect.run so there is nothing new to learn.1743- MCP library: `@modelcontextprotocol/server` + `@modelcontextprotocol/hono`,1744 the pair mcpsweeper already runs, mounted inside Fresh.1745- Seams: overlap-by-one. Simplest coupling that makes tiles under-determined1746 alone; rules-across-seam and seam clues are phase-2 knobs.1747- Rule attribution is computed by the referee at commit time and stored in1748 the batch event for the analyzer. The spectator's colour layer is tile1749 ownership (semi-transparent washes that combine on seams), not rules.