Files
karti-ai 5bbf913664
ci / web (push) Successful in 3m10s
ci / python (push) Successful in 2m32s
Landing picks an environment; each environment is a tabbed page opening on Play
The environment page was a linear scroll of eight narrative beats. That is an
essay, and it is the wrong shape for somebody who has just chosen an
environment and wants to use it. It is now four tabs — Play, Watch, Reward,
Evidence — opening on Play, with the board above the fold at 390x844 and the
anatomy strip directly beneath it. The landing page leads with the picker
instead of burying it under the thesis.

The contract changed rather than layering tabs over beats. `Narrative.beats` is
gone; `claims: Record<DemoTabId, string>` replaces it, one required sentence per
tab. Writing the claim is how an author discovers whether a tab has anything to
say — a tab whose claim is hard to write is usually a tab with nothing in it.
Doing this now costs one migration; doing it after eleven more environments
costs twelve.

Tabs are derived, never declared: Play iff the demo ships an `interactive` mode,
Watch iff it has recorded runs. A demo that could name its own tabs would mean
environment seven inventing a fifth one and the site ceasing to be one product.

One thing the browser caught that no gate would have. The header stat strip
describes the RECORDED RUN, and on Play it sat above the visitor's own empty
board reading "Outcome: failed" — which parses as your game having already
failed before you touch a key. It now renders only on the tabs whose subject is
that run, which also moved the board 54px up the page.

The picker is honest about the shape of the lineup by construction: one built
environment gets its own block and the demo's real board as its thumbnail,
twelve written specifications render dimmed with a Spec badge, and every count
on the page is derived from the data rather than typed.

206 contract checks pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
2026-08-28 18:07:34 -07:00

3.2 KiB

Working in this repository

Adding an environment

Read .claude/skills/new-environment/SKILL.md. It is the recipe and, more importantly, the judgement: which gate means "iterate" and which means "this environment does not work". The pipeline that runs it is .claude/workflows/new-environment.js.

The short version: the specification comes from src/content/verticals.ts (rank: 1 is the one to build next), it gets written up and attacked before any code, the probe ladder is what proves the reward measures something, and the conformance digest is what proves the browser and the environment are playing the same game.

The three commands

uv sync --all-packages && uv run pytest envs/wordle_five/tests && uv run python envs/probe.py
pnpm install && pnpm check && pnpm build
uv run python envs/verify_fixtures.py

Directories an agent adding a demo may touch

Exactly three, plus one line of content:

  • src/demos/<slug>/
  • envs/<pkg>/
  • public/traces/<slug>/
  • one entry in src/content/verticals.ts

Anything else is a shared file and a merge conflict waiting to happen. If you believe you need to edit the shell, the header or the router, you have found a contract bug — read CONTRACT.md, rule 6.

Traps that have already cost time here

pnpm 11 does not read the pnpm field in package.json. Settings live in pnpm-workspace.yaml, and an unapproved build script makes pnpm install exit 1 rather than warn.

A language's built-in RNG is not portable. random.Random(seed) and any JavaScript PRNG will disagree, so the same seed picks different words on the two sides and every permalink silently shows a different puzzle than the run it claims to replay. Both sides use FNV-1a over the decimal seed. Math.imul on the JS side is load-bearing — a plain multiply overflows into a double.

Scoring must be two passes. Every green resolves before any yellow. One pass gets SASSY/BASIS wrong, and 21.2 million pairs are hashed in CI precisely so that cannot ship.

consistency is scored over turns SPENT, not guesses accepted. Counting only legal guesses hands a free 1.0 to a policy that plays one word and jams the parser five times: one guess, no contradictions, perfect score.

economy's denominator is the shipped solver's depth, not a depth-optimal search. Entropy-greedy is not depth-optimal, so grading it against a true optimum makes the oracle rung fail its own probe assertion on some seeds.

The production CSP has no worker-src, so it falls back to default-src 'self'. A blob-backed worker (Vite's ?worker&inline) is blocked in production only, with no console error — the solver simply never boots. CI greps the bundle for blob: for this reason.

Caddy's bind 10.0.0.2 on cloud-2 is mandatory and its absence is silent. Without it the site answers an empty 200 behind a valid certificate, and a --resolve check from cloud-2 itself still passes. Verify from a third machine against the real hostname.

spark-1 serves one model and is single-stream. Capture is a serialised queue, not a parallel one. Book it before starting a long sweep.

Style

Comments explain why, and only where the reason is not evident. Never narrate what the code says. Prefer one honest number over three adjectives.