Skip to content

Web showcase: project plan

Everything in this project runs in a terminal today. This plan turns it into a fast, static, embeddable website where anyone can play every game, watch every agent decide, see why the gate acted or held back, and explore the statistics behind it. It also produces an API that other sites and services can call. The plan is for contributors. It says what will be built, in what order, what "done" means for each phase, and which decisions are already made and why.

Each phase ships something usable on its own, and nothing in a later phase is needed to keep an earlier one working.

Goals

  1. A showcase site with every example: games (play, watch, compare, audit), the decision runtime, parameter learning and the risk monitor. Interactive and animated. Good on phones as well as desktops.
  2. Teaching and science. Every claim on the site links to the proof, test or paper behind it. Every run can be replayed exactly from a URL.
  3. Embeddable. Drop a game, a replay or an ambient "background screen" into any existing website with one script tag and one element, served from our own site.
  4. One implementation. The browser runs the same Python package as the CLI. Nothing is reimplemented in JavaScript, so the tests and proofs cover what visitors see.
  5. Self-managing. CI enforces correctness, protocol compatibility, performance budgets, accessibility and dependency updates. Every release is a local build of plain files that we copy to our own nginx server.

Non-goals for now: user accounts, persistent multiplayer across the internet, server-side storage of visitors' data, and anything published to or hosted on third-party services. Everything runs on our own nginx server and our own service (phase 9).

The architecture in one picture

          ┌──────────────── grounded_decision (Python, unchanged) ───────────────┐
          │ rules · agents · gates · solvers · DecisionRecord · learn · monitor │
          │ session layer (new, pure, no I/O, no threads, no signals)           │
          │   open(spec) → Session ; session.step() / submit(move) → Frame      │
          └───────┬──────────────────────────┬──────────────────────────┬──────┘
             CLI views                  Pyodide Web Worker        our own service
           (terminal, JSON)             (in the browser)         (phase 9, behind nginx)
                  └────────── one versioned protocol (JSON Schema) ─────────┘
                                             │
               generated TypeScript types · replay format (.gdr) · design tokens
                                             │
   ┌───────────────┬────────────────────────┼──────────────────────┬────────────────┐
 board renderer   decision inspector     charts and explorables   input (keys,   web components
 (Canvas 2D,      (proposal, checks,      (compare, audit,        touch, mouse)  <gd-game> <gd-replay>
  square + hex)    verdict, evidence)      learning, monitor)                    <gd-ambient> <gd-demo>
                                             │
                            SvelteKit static site → our own nginx server

Why this shape

  • The model–view split already exists. Games describe boards by meaning (cells, cell_details, move_cells, stats), never as characters. The terminal styles are one projection of that. A browser renderer is another, and adding a game needs no front-end code.
  • Pyodide runs the package unchanged. Its only dependencies are numpy (shipped with Pyodide) and lark (pure Python). The browser is about 1.5–3 times slower than native Python. That is still far more than 60 frames a second needs, because the engine runs in a Web Worker and never blocks the page.
  • Replays don't need Python. A recorded game is a compact stream of frame differences. A small JavaScript player animates it instantly, with no 10 MB download. Ambient screens, embeds and the first paint of every page use replays. Live Python loads only when the visitor interacts.
  • Static first. The whole site is files we copy to our own server, with no servers to run, scale or secure. The optional server reuses the same protocol, so the front end does not know or care where Python runs.

Decisions already made

Question Decision Why
Where the rules run The Python package in Pyodide; optionally the same package on a server One implementation, so the proofs and tests cover the site
Front-end framework SvelteKit with the static adapter, TypeScript in strict mode Small runtime, compiles to static files, good for animation-heavy pages
Board drawing Canvas 2D with layered canvases and dirty rectangles; a WebGL path only if a measured budget fails Grid boards need no scene graph; avoids about 450 KB of PixiJS. A 320 × 240 snake is 76,800 cells, which is one ImageData write.
Embedding Standard web components (custom elements, Shadow DOM), plus an iframe fallback Works in any site and framework; styles cannot leak either way
Protocol JSON Schema generated from Python dataclasses; TypeScript types generated from the schema One source of truth; drift fails CI
Colours and glyphs One token file generated from views.py, used by both the terminal and the CSS The CLI and the site look like the same product
Hosting A local, content-hashed static build that we copy to our own site and service; no third-party host, vendor CDN, account or secret Our infrastructure, no lock-in: the build output is plain files any web server can serve
Cross-origin isolation Not required Avoids SharedArrayBuffer, so the site works on any host and inside other sites. Stopping a computation means terminating the worker.

Repository layout

src/grounded_decision/session/   protocol dataclasses, Session, replay writer (Python)
web/                             pnpm workspace
  packages/protocol/             generated types, runtime validators, schema tests
  packages/engine/               Pyodide worker, transport, wheel loader
  packages/replay/               .gdr reader and player (no Python)
  packages/board/                renderer: square and hex grids, animation, themes
  packages/inspector/            DecisionRecord views, check timelines
  packages/models/               DOM-free models shared by the site and the elements
  packages/elements/             <gd-game> <gd-replay> <gd-ambient> <gd-demo>
  apps/site/                     the showcase site (SvelteKit)
  tests/e2e/                     Playwright

make stays the single entry point. New targets: web-setup, web-dev, web-build, web-test, web-e2e, web-lighthouse, web-check, web-elements, dist, replays, protocol. Once web/ exists, make check also runs the web checks.

Phases

Each phase lists what is built and its exit criteria, all automated in CI. Work inside a phase can run in parallel. The phases are sequential.

Phase 0: Foundations

  • Workspace: a pnpm workspace with Vite and SvelteKit, TypeScript strict, ESLint and Prettier for web/ only (Python keeps ruff), Vitest and Playwright.
  • CI: a web job beside the Python job. Exact pins, updated by hand.
  • Records: the decisions above written into Design decisions as D17 onwards.
  • Exit: an empty site builds locally and passes lint, type check and one Playwright test in CI.

Phase 1: The session layer and the protocol (Python only)

Status: built. grounded_decision.session (SessionSpec, Session, Frame, the Endpoint and protocol 1.0.0), the CLI's play and --view json on top of it, make protocol (JSON Schema, design tokens, golden fixtures, TypeScript types) and web/packages/protocol (generated types, runtime validators, fixture tests). Reference: Session protocol; decisions D28 to D33. Not yet: Session.undo(), and compare and audit share the session's match loop (engine.Match) but not yet its frames.

  • The session API: grounded_decision.session with SessionSpec(game, agents, world, seed, options), Session.step(), Session.submit(move), Session.legal_moves(), Session.undo() where the game allows it, and Frame. A frame holds the cells, details and move cells, the panel stats, the latest Decision (proposal, checks, verdict, notes) and the outcome. It is pure, with no I/O, threads, signals or wall clock, so it runs anywhere. A frame also carries the semantic overlays the terminal views already use (views.Overlay: CONVERTED, CONVERTING, RING, TARGET_NEAR for a one-step/clone target, TARGET_FAR for a two-step/jump target, SELECTED, LAST_FROM/LAST_TO, REJECTED/CHOSEN), the move's transition frames (overlays on intermediate positions, for the player to animate), the move_event line, the board's layout (square, pointy, flat) and the fixed PanelLayout slots, so the web player draws the same meaning in its own colours and keeps the same fixed panel.
  • The CLI on top of it: the terminal views, --view json and compare/audit consume the session layer, so there is one code path.
  • The protocol: messages are open, step, submit, frames, error and describe. describe returns the game list, agents, worlds and checks from the module contract constants. The protocol is versioned with semver, and a schema-diff test fails a breaking change without a major bump.
  • Tokens: a new protocol make target writes the JSON Schema and a token file (colours and glyphs for every CellKind).
  • Exit:
  • Golden frames: --view json output equals the session layer's frames byte for byte for every game and seed in the snapshot set.
  • The schema is committed and checked.
  • make check is green.

Phase 2: Replays and the board renderer (no Python in the browser)

Status of the replay half: built. grounded-decision record and replay-info (--verify re-records the spec and compares content hashes), the .gdr format version 2 (grounded_decision.session.replay), make replays (a committed gallery of every game in web/apps/site/static/replays/ with an index.json manifest, and golden files for the decoder's tests), and web/packages/replay (decoder with random access and a DOM-free ReplayPlayer, 11.5 KB gzipped). Reference: Replay format; decisions D34 to D37.

Status of the renderer half: built. web/packages/board draws any frame on three layered canvases with dirty rectangles (square grids, pointy hex rows, flat hex columns; every CellKind and Overlay; one pixel per cell above 16,384 cells), animates moves from the frame's transitions and move cells (slides along jump paths, Hexxagon flips, Tetris drops and bursts, 2048 pops and merges), cuts under reduced motion, and describes each frame in words; 23 KB gzipped. The site has /gallery and a prerendered /replay/NAME page per replay (player controls, keyboard shortcuts, ?frame= links, the fixed decision panel, a colour-blind palette and glyph mode). Measured in headless Chromium: 0.8 ms median and 1.0 ms 95th percentile per frame on the 320 × 240 snake. Playwright checks every replay, screenshots of each in light and dark, keyboard-only use, WCAG 2.2 AA with axe, and the frame budget. Details: web/README.md; decisions D38 to D42. Not yet: Linux screenshot baselines (CI writes and uploads them on its first run; they need committing), and runs in WebKit and Firefox.

  • Recording: grounded-decision record GAME --agents … --world … --seed … writes a .gdr replay: a header (game, agents, world, seed, package version, protocol version, content hash), the keyframes, one line of changes per part of the frame, then gzip.
  • Playing: packages/replay decodes it. packages/board draws square and hex boards for every CellKind, with the snake gradient, jump paths and last-move highlights. It animates moves from move_cells: pieces slide, Hexxagon pieces flip, lines clear and 2048 tiles merge.
  • Accessibility: colours follow the shared tokens, with a colour-blind-safe palette and glyph fallbacks. prefers-reduced-motion turns animation into cuts.
  • Exit:
  • Every game's snapshot replay plays in the browser.
  • Visual-regression screenshots are stable.
  • The renderer holds 60 fps on a 320 × 240 snake on a mid-range laptop, with 4 ms or less per frame on the main thread, measured in CI with a headless trace budget.
  • The replay player is 40 KB or less, gzipped.

This phase alone can ship: a gallery of animated, real, reproducible games.

Phase 3: The live engine in the browser

Status: built. web/packages/engine runs Pyodide 314.0.6 in a Web Worker from the site's own files (make web-engine: the runtime from the pinned npm package, lark from uv.lock, the wheel from make build; numpy hosted for later, since the session layer no longer imports it), behind a Transport interface and a validating Engine client with lazy start, progress, cancellation and protocol errors. Protocol 1.1.0 adds select (a person's piece and its targets). Each game has a prerendered /play/GAME page: the replay first, then the live game; agents, world, seed, size and rule options in the URL; watching at a speed, playing by click, tap or keys, --guarded with the gate's explanation, and Copy CLI command. make web-pyodide-golden found and fixed a determinism bug (seeds hashed differently on 32-bit WebAssembly) and now shows all ten games byte-identical to CPython; make web-pyodide-test runs the session, game and learning tests in Pyodide. Measured: the first live move in 2.5 s on a cold cache over 40 Mbit/s and 1.0 s on a warm one; about 4,100 snake moves a second in the worker with every frame on the wire (2,700 before protocol 1.2 sent the snake's body as one path, D51), above the plan's 3,000. Reference: The engine in the browser; decisions D43 to D50. Not yet: WebKit and Firefox runs. (Loading numpy on demand came with phase 6, D65.)

  • The worker: packages/engine runs Pyodide in a Web Worker, loads the project's wheel (built by make build) and speaks the protocol over postMessage. One transport interface is shared with the WebSocket transport to our own service (phase 9). Pyodide and the wheel are lazy-loaded on first interaction and cached.
  • Determinism: the same SessionSpec produces identical frames in CPython and in Pyodide. A CI job runs the golden-frame set in both.
  • The test suite in Pyodide: the pure-library tests run under Pyodide in CI (pytest via Pyodide's test runner). Tests that need the terminal or the file system are marked and skipped.
  • Exit:
  • Tic-tac-toe and Snake are playable live.
  • Pyodide golden frames match.
  • Time to first live move is 4 s or less on a cold cache and 1 s or less on a warm cache.
  • Snake engine throughput in the worker is at least 3,000 moves a second.

Phase 4: The showcase site, version 1

Status: built. A design system from tokens (apps/site/src/app.css: type scale, spacing, radii, light and dark colours, shared buttons, fields, cards, badges and callouts), a header with Play, Gallery and Learn on every page, Display settings (scheme, colour-blind-safe colours, glyphs, motion) kept in the browser and applied before the first paint. The home page explains propose, check, then act, abstain or degrade beside a replay-first example that pauses on each override. Every game is offered three ways (watch, play against agents, play guarded) on its play page and on every game card. The decision inspector (web/packages/inspector) shows each move's proposal against the move played (both marked on the board), every check with what it tests, its evidence and result, the verdict, fallback and reason, and the regime against the world ("Assumption not met" when it is not); a scrubbable timeline marks overrides on replays and live games alike. Explain panes use the catalogue's text, which protocol 1.2.0 extends with the regimes, so they say what play --explain says. Play pages keep the person's moves and the position on show in the URL, so a copied link reopens the very game; every game is playable from the keyboard alone, and moves are announced in a live log, summarised at speed. Measured with Lighthouse CI (make web-lighthouse, mobile emulation, median of three runs): 99 to 100 for performance and 100 for accessibility, best practices and SEO on the home page, the gallery, a replay page and a play page; CI fails below 95. Playwright covers watch, play (a keyboard move in every game; whole games of tic-tac-toe and Connect Four), the guarded override, share-link round trips, the timeline, axe on every route in both schemes, and screenshots of the new pages. Details: web/README.md; decisions D52 to D58. Not yet: Linux screenshot baselines for the new pages (CI uploads them), and WebKit and Firefox runs.

  • Play pages: every game, with agents, world, seed, size, speed, pause, step, a timeline you can scrub, and human play by keyboard, touch and mouse. A guarded toggle lets the gate catch your blunders and explains why.
  • URL state: every setting is in the URL. Sharing a link shares the exact game, and a "copy CLI command" button gives the identical terminal run.
  • Decision inspector: each move shows the proposal, every check with its evidence and result, the verdict (act, abstain or degrade), the fallback, and the reason. Overrides are marked in the timeline. It also gives the regime, the world and whether the agent's assumptions hold.
  • Explain panes: they reuse AGENT_DETAILS, CHECK_DESCRIPTIONS and WORLDS, so the CLI's --explain and the site say the same thing.
  • Exit:
  • Every game is playable, all three ways.
  • Lighthouse scores are at least 95 for performance, accessibility, best practices and SEO on replay-first pages.
  • Playwright covers play, watch, override and share-link round-trips for every game.
  • WCAG 2.2 AA is met for controls and the inspector: keyboard-only play and screen-reader move announcements.

Phase 5: Compare, audit and teaching pages

Status: built. Protocol 1.3 makes compare, audit and analyse session operations on the command line's own code (Tournament, AuditSetup and AuditRun, refactored so the CLI and the session share one path): a comparison's games are numbered and played one request at a time on any engine, Python tallies the table from the set of finished games (so it equals compare --json whatever the order), and an audit returns its judged decisions a chunk of moves at a time, each disagreement with its position. Golden tests compare every number with the command line for several games. /compare/GAME runs the games on a pool of Pyodide workers (one per core but one, at most four by default) with a live table of Clopper–Pearson intervals, a Cancel button and the identical command; /audit/GAME lists each disagreement as a button that opens its position (before and after, both moves marked) and the decision inspector. Both are prerendered for every game and linked from the play pages. /learn/GAME is a teaching page per game, replay-first: make lessons records real positions and the package's own analyses (the snake's cycle and certificate, Minesweeper's probabilities, Connect Four's column values, the Tetris adversary and Burgiel's S and Z pieces, Go's superko and area score, Hexxagon's ring trap and two-ply exchange, the gate on a checkers, Chinese checkers, tic-tac-toe and 2048 position); Minesweeper and Connect Four also analyse live as you play. Every claim names its test (<Claim test=…>), linked to its source line, and tests/test_site_claims.py fails a claim whose test is missing, skipped or slow. Decisions D59 to D63. Not yet: Linux screenshot baselines for the new pages (CI uploads them).

  • Compare and audit in the browser: live tournament tables with Clopper–Pearson intervals, filling in as games finish (a worker pool, one Pyodide per core, with a configurable limit). Audits against oracles show each disagreement as a clickable position.
  • Explorable explanations, one per game:
  • Snake: the Hamiltonian cycle and the certificate shown live.
  • Minesweeper: a probability heat map, with proved-safe cells marked.
  • Connect Four: the solver's value of each column.
  • Tetris: the adversary's choice and why it hurts.
  • Go: superko and area scoring, stepped through.
  • Sources: every theorem on a page links to its test, its source line and, where there is one, its paper.
  • Exit: every claim on a teaching page has a link to a test that checks it, enforced by a link checker that maps claims to test IDs.

Phase 6: Beyond games

Status: built, except the embedding. Protocol 1.4.0 adds learn, monitor and runtime (grounded_decision.session.demos): stateless requests whose replies hold everything a page draws, computed by the command line's own code (learning_run, the parameter_learning example's LiveTraffic, and the reference service's pure core, now examples/reference_core.py). The engine loads numpy on demand (WorkerTransport's packages), only on the three new pages: /parameters (the how-to's 3,000 decisions, the example's authorisations, or a CSV parsed in the browser and never uploaded; risk target, confidence, measure, procedure, seed, decision line and grid; the certificate, explain(), the fingerprint, params.json, and every combination's exact p-value charted), /monitor (the example's drift or the how-to's stream, played as it arrives, with the bounds, the target and the alarm) and /runtime (the build-a-service tutorial's requests stepped through, routing to human_review (the live examples' hold_for_human) and await_next_event shown, the DecisionRecord in the inspector's styles, and more events sent live). Each page shows the engine's recorded reply first (make protocol writes it), gives the identical command, and follows the docs standard; the docs pages link to them and back. Golden tests (tests/test_session_demos.py) compare every demo with grounded-decision learn, grounded-decision examples parameter_learning, DecisionService on SQLite and the docs' numbers; Vitest covers the CSV reader, the chart geometry and component, and the page models; Playwright covers each page (preview numbers, live interaction, axe in both schemes, a phone's width) and Lighthouse runs on /runtime. Decisions D64 to D68, and an open question on platform-exact floating point (fingerprints computed in the browser can differ in the last bits; the page says so). The widgets came to the docs pages with phase 7's <gd-demo> (D76).

  • Parameter learning: load a sample dataset, or your own CSV (which stays in the browser). Set a risk target and watch Learn-then-Test choose thresholds, with the certificate, provenance fingerprint and explain() text.
  • Risk monitor: a streaming chart of the monitor on a synthetic drift scenario, showing when it raises the alarm and why.
  • The decision runtime: the reference service's loop as a stepper. Events come in, the statechart proposes, the six checks run, and a DecisionRecord comes out. Routing to hold_for_human is shown.
  • Exit: each demo matches its documented example's numbers exactly (golden tests), and every docs page has its widget embedded.

Phase 7: Embedding and integration

Status: built. web/packages/elements defines <gd-replay>, <gd-game>, <gd-demo> and <gd-ambient> (a stable API; its playlist plays now, phase 8 adds the rest). They are standard custom elements with an open Shadow DOM, written over the site's own packages, with the models shared through the new web/packages/models. Attributes are the command line's flags, mirrored as properties. They send ready, frame, move, verdict and error events, expose ::part()s and --gd-* custom properties, and follow the host's color-scheme, then prefers-color-scheme, or an explicit theme. They are keyboard driven, with live regions summarised at speed. Each element loads when it nears the viewport. Python loads on the first interaction, from engine-base or the bundle's own engine/, through a blob: module when the page is on another origin (D73). make web-elements writes one self-contained, versioned directory, elements/0.1.0/, holding the chunks, the worker, the phase 3 engine files, the replays, the catalogue and the recorded demo replies. It writes integrity.json with SHA-384 for every file, and a scripts-only elements/0/ alias. make dist gathers the site, the elements and the docs into dist/web/ for our own nginx server.

The site serves /elements/, an iframe page per replay and per game (/embed/replay/NAME/, /embed/play/GAME/; only these may be framed) and a static oEmbed file per replay. make docs embeds the elements through a MkDocs hook: the local bundle, its script tag with its integrity hash only on pages that use one, and fallback text without JavaScript or without a build. The games pages and the learning, provenance, monitoring and runtime pages run them in place. The guide is Embed the games and demos.

Measured, gzipped: gd.js 9.7 KB; <gd-replay> and <gd-ambient> 53.4 KB before their first frame (the build fails above 64 KB); <gd-game> and <gd-demo> 9.7 KB before their preview. Playwright embeds every element in plain HTML and in a React 19 app on another origin, under a policy with only the guide's entries, and in the MkDocs site. It checks drawing, the keyboard, light and dark schemes, axe and an empty console. It also checks a tampered gd.js refused by its hash, a live game and a live demo running across origins, the iframe and oEmbed files, and the served headers. Vitest covers attribute and property reflection, events, the keyboard, failure and the playlist. Decisions D70 to D76. Not yet: WebKit and Firefox runs, and import-map integrity for the chunks (documented, not tested).

  • Web components:
  • <gd-replay src=… autoplay loop> (no Python).
  • <gd-game game=… agents=… world=…> (live).
  • <gd-demo name=…> (the phase 6 widgets).
  • <gd-ambient> (phase 8).
  • Delivery: versioned, immutable URLs on our own server (/elements/1.4.2/gd.js) with Subresource Integrity hashes, and a moving /elements/1/ alias.
  • Theming: CSS custom properties and ::part() styling. Light and dark themes follow the host page or prefers-color-scheme.
  • Fallbacks: an iframe embed for hosts that forbid scripts, and an oEmbed endpoint (a static JSON file) for CMSs.
  • MkDocs: make docs embeds the elements, so every "Try it" block on the docs site runs in place.
  • Integration guide: a "How to embed" page for our other websites. It covers script tags, Content Security Policy entries, a self-hosting option and a size budget per element.
  • Exit:
  • A test page embeds every element in three host setups: plain HTML, a React app and the MkDocs site.
  • SRI, CSP and dark mode are verified in Playwright.
  • Initial load per embed is 64 KB or less, gzipped, for replay and ambient elements.

Phase 8: Ambient and background screens

Status: built. <gd-ambient> (web/packages/elements) plays recorded games one after another with no Python: a manifest's replays (library, else the bundle's gallery), kept by name or game (games), in an order shuffled by seed and reshuffled by it at every pass, paced by pace (slow, normal, fast or seconds a game: 1 to 8 moves a second, and a longer game cuts to its end) or speed, with a cross-fade between games (a cut under reduced motion) and optional captions: the verdict and the deciding check from the decision record, at most one every 6 seconds, overrides first, in large type with captions="large". kiosk fills the screen, asks for fullscreen on the first click or key, hides the cursor after 3 seconds at rest and keeps the screen awake (Screen Wake Lock, falling back silently). The low-power rules are a state machine over what the browser reports: hidden pauses, reduced motion shows still positions every 8 seconds or more, a discharging battery (Battery API) or reduced data draws at 15 frames a second, and otherwise at 30. The site's /ambient takes the settings in its URL (/ambient?kiosk=1&games=snake,go&pace=fast&captions=large). make ambient-replays records a library of the eight backdrops, three of each from the day's seed, into build/ambient/ (never committed), each verified as replay-info --verify does and listed with its content hash and outcome; make dist copies it to dist/web/ambient/, and a nightly job records one as a test only. The guide is Embed the games and demos.

Measured on an Apple-silicon laptop in Chromium's headless mode (which draws with the GPU), a 1920 × 1080 kiosk screen with large captions, 20 seconds each: 2.9% of one core on the page's main thread (3.5% for the renderer process) at full power, 2.6% on battery, 0.1% under reduced motion. The soak test, accelerated to 200 moves a second, played 20,017 positions over 74 games with no console errors: the heap after a forced garbage collection stayed between 4.1 and 5.7 MB from the second game to the last, the DOM between 521 and 527 nodes, and the event listeners at 67 throughout. Five minutes at the normal pace on a recorded library (make web-soak SOAK_MINUTES=5) kept the heap between 2.5 and 5.4 MB, the DOM at 520 to 522 nodes and the listeners at 41. gd-ambient's first load is 60.0 KB gzipped (budget 64 KB, raised from 60 KB with protocol 1.6). Tested: Vitest for the playlist, pacing, the power state machine, the captions and their throttle, the capped clock and the element; Playwright for the playlist moving across games, kiosk mode (no controls, fullscreen, the cursor hidden), the pause when hidden and 15 fps on battery, reduced motion, axe in both schemes and in kiosk mode, a phone's width, the CPU budget, and the soak (2,000 positions in CI; make web-soak runs it for 30 minutes, and make web-soak SOAK_MINUTES=1440 is the manual 24-hour check). Decisions D81 to D83. Not yet: a 24-hour run on the television itself, which needs the owner's screen.

  • /ambient and <gd-ambient>: a playlist of recorded games shuffled by seed. Snake at large sizes, six-player Chinese checkers, Go and Tetris against the adversary all make good backdrops. Optional captions from the decision records show what the agent is "thinking".
  • Kiosk and screensaver mode: fullscreen, no controls, cursor hidden, and a URL parameter to choose the games and pace.
  • Low power:
  • It pauses when hidden (Page Visibility API).
  • It drops to 15 fps on battery when the browser reports it.
  • With prefers-reduced-motion, it renders still frames that change slowly.
  • It uses replays only, with no Pyodide, so it stays well under 5% CPU on a laptop.
  • Fresh content: a nightly CI job regenerates a rotating replay library from new seeds. Each replay carries its content hash and verified outcome.
  • Exit:
  • A 24-hour soak test in a headless browser shows flat memory and no errors.
  • The CPU budget is met.
  • The playlist plays on a TV-sized screen with readable captions.

Phase 9: Our own service

Status: built. grounded_decision.service (grounded-decision serve, make serve, the service extra: uvicorn and websockets, pinned exactly) runs the package's own Endpoint, one session per WebSocket on /ws/ and one message per POST /api/, the reference decision service under /api/decisions/ (the same routes as examples/reference_service.py, now DecisionRoutes, with its SQLite tables in a file you choose) and GET /healthz. Limits on message size, sessions per process, each connection's rate, request time, idle time and the work in one request (moves, games, seeds) are answered with the protocol's own error replies, so the protocol stayed 1.4.0. Its JSON log lines name no visitor. The front end's WebSocketTransport is the second Transport; EngineChoice picks it by one setting (VITE_ENGINE, overridden by ?engine=), with the worker as fallback when the service is off or full. make dist writes dist/service: the wheel and its dependencies for the server's platform (offline install), the hashed requirements from uv.lock, the nginx snippets, a systemd unit per port, run.sh and release.sh (versioned release directories, an atomic current link, health-checked switch, automatic switch back, rollback, prune). The guide is Run our own service.

Tested: every golden conversation through the Endpoint, the WebSocket and HTTP, byte for byte; each limit; the logs; the decision API against the reference service, request for request; release.sh for real against a scratch prefix (tests/test_service.py, tests/test_service_ops.py). Playwright runs the golden conversations through WebSocketTransport against a local service behind an nginx-like proxy, plays snake and /runtime live with no Python downloaded, and falls back to the worker with the socket refused (web/tests/e2e/service.spec.ts). Measured on an Apple-silicon laptop, one process, snake 20 × 20 (make service-load): 64 sessions at once, 3,493 requests a second, 95th percentile 21.5 ms, against a target of 1,000 and 100 ms. Decisions D77 to D80. Not yet: running it on our servers, which needs the owner.

  • What it is: our own server process, part of the product, run on our own machines behind our nginx (as a reverse proxy for /api/ and /ws/). No third-party platform, account or hosted service is involved.
  • What it does: runs the same package and speaks the same protocol over WebSocket and HTTP, for work the browser can't do well: large datasets for parameter learning, long tournaments and audits, and the reference decision service as a real HTTP API.
  • Operations: stateless, rate-limited, with request size limits and structured logs that store no visitor data. Shipped with an nginx snippet and a service unit file; make serve runs it locally.
  • The front end switches transports (worker or service) with one setting; the static site still works with the service switched off.
  • Exit:
  • The protocol conformance suite passes against both transports.
  • A local load test reaches its target.
  • Building, copying to our servers and rolling back are scripted.

Phase 10: Launch hardening and release

  • Security: a strict Content Security Policy that allows only the site's own origin (Pyodide is served from it too). wasm-unsafe-eval is the only relaxation, because Pyodide needs it. Plus security headers, shipped as an nginx config snippet, and a dependency audit in CI.
  • Privacy: no cookies and no tracking by default. If analytics are wanted, a privacy-preserving, cookieless tool, recorded as a design decision.
  • Release: semver for the package, the protocol and the elements (independently versioned). A changelog. One local command builds every artifact into one directory (the site, the versioned element bundle, the docs, the wheel, our service) that we copy to our own servers. Nothing is published to a package index or deployed to a host.
  • Reproducibility: each replay embeds the package version and hash it was recorded with, so any shared game can be reproduced exactly.
  • Exit:
  • The security header scan scores A.
  • Accessibility and performance budgets are green.
  • A local release build, served by nginx with the shipped config, passes the end-to-end tests.

Cross-cutting rules (every phase)

Area Rule Enforced by
Correctness Browser and CLI produce identical frames for the same spec Golden-frame tests in CPython and Pyodide
Compatibility Protocol changes follow semver Schema-diff test
Performance Budgets per page and element (bundle size, LCP ≤ 1.5 s on replay pages, 60 fps, main thread ≤ 4 ms per frame) Bundle-size check, Lighthouse CI, trace budgets
Accessibility WCAG 2.2 AA; keyboard play; reduced motion; colour-blind palette with glyph fallback axe in Playwright, manual audit before each phase exit
Honesty Every guarantee shown links to the test or proof; measured numbers say so Claim-link checker
Unification One source for names, descriptions, colours and glyphs: the Python modules Generated files checked to be current, as docs/reference/agents.md is today
Self-management Exact pins updated by hand, generated files checked to be current, one local release build make check, CI tests

Risks and how we handle them

  • Pyodide download size (about 10 MB). Pages use replays first and load live Python only on interaction. The download is cached long-term, and a progress indicator shows while the replay keeps playing.
  • Modules Pyodide lacks or limits (signal, threading, subprocesses). The session layer must not import them, and a test imports it with those modules blocked.
  • Safari and mobile performance. The board renderer has budgets per browser in Playwright's WebKit runs, and large boards fall back to a lower frame rate before they drop frames.
  • Two languages. All game logic stays in Python. TypeScript only draws, animates and handles input, and its types are generated, so it cannot drift from the protocol.
  • Scope creep. Each phase ships alone, and nothing depends on a third-party host or service: only our nginx server and our own service.