typy: hold the centred-word stage and its common-words list at / (card #262) #295

Closed
time wants to merge 0 commits from claude/typy-playable-plds9n into main
pull from: claude/typy-playable-plds9n
Collaborator

From github: claude/typy-playable-plds9n (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.

Commits:

typy: hold the centred-word stage and its common-words list at / (card #262)

Card #262 asks for the game UI: a big centred word whose letters colour as
you type, the built-in common-words list behind /, the next word snapping
in on advance, and desktop keyboard handling. All four shipped with typy v0
(brief #254) and card #261 moved per-letter state into the engine, so the
behaviour this card describes is already what a visitor gets at
https://typy.yolopreneur.lol/ — public/index.html centres #word on the
stage, drawWord paints each glyph from the engine's marks, / builds its
passage from generateWords, and document.addEventListener('keydown')
feeds printable keys and Backspace to TypingGame.

What the card's bullets did not have was coverage holding them in place, so
that is what this commit adds to typy/test/game.test.js:

  • The common-words list behind / draws 300 plain lowercase words, none
    wider than the stage, reusing a small vocabulary and weighted hard toward
    the commonest twenty — an even draw over that list would give a third as
    many.
  • Advancing produces the next word inside the same keypress (no blank frame
    between words), with an empty buffer and every letter back to pending,
    and a short passage wraps to its first word.
  • Navigation and modifier keys (Enter, Tab, arrows, F5, Shift, Control,
    Dead, raw control characters) never reach the buffer and never cost a
    keystroke or a point of accuracy.

Tests: npm test --prefix typy (42 pass), python3 .github/time/test_time.py
green. Tests only, so no generated asset changes.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #262


typy: add the 120-second run beside the other modes (card #263)

Card #263 asks for selectable 30s / 60s / 120s timed runs and a 50-word
run, a WPM/accuracy maths module with unit tests, a small live display,
and run end detection with a clean reset. Everything but the 120-second
run already shipped: summarizeRun in typy/src/game.js is the maths
module (net output characters ÷ 5 ÷ elapsed minutes, accuracy over every
character attempt) and it is covered by tests down to exact 100 and 160
WPM input streams; the live pace, accuracy and remaining-time readouts sit
unobtrusively above the stage; and each mode ends on its own terms.

This commit adds the missing mode and generalises the clock around it:

  • MODES gains 120, and TIMED_MODES / timedSeconds(mode) replace the
    '30' || '60' pairs that were spread through the engine, the page and
    the result validator. A timed mode is now named by its own seconds, so a
    fourth length needs one list entry rather than four comparisons.
  • The toolbar gains a 120s button. Six buttons no longer fit one phone
    row, so the mode switch wraps and centres below 700px instead of
    overflowing its pill.
  • A 120-second run can be shared: the server accepts the mode and still
    refuses a replay claiming more than its 120 seconds.

Tests (typy/test/game.test.js, typy/test/backend.test.js): the exact
100/160 WPM streams and the exact-deadline stop now run over all three
timed lengths; a new test walks 30s, 60s, 120s and 50 words end to end,
checking each stops for the right reason, leaves readable live figures,
and that the next run starts from zero; and a 120-second run round-trips
through save and read while an overlong one is rejected.

npm test --prefix typy (44 pass) and python3 .github/time/test_time.py
are green. src/page.js and src/assets.js are regenerated because the
build bundles the page and static sources for the Worker.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #263


typy: say on the result card that a shared replay is public (card #264)

Card #264 asks that finishing a run shows WPM, accuracy, a per-word speed
sparkline, the slowest words, a restart button, and a placeholder note that
replay links are public. The card itself ships in #result-panel: the big
WPM figure, accuracy, best clean streak and words completed, the #speed-chart
sparkline drawn from stats.sparkline, the three slowest words with their
seconds, and "go again" (also bound to Enter and Escape). The note was the
part that was not true to the card — it read "Your result is on this device.
Share it when you're ready", which says nothing about who can see a share.

  • The result note now states plainly that sharing makes a public replay
    link anyone can open with no account, and an already-shared result says
    it is public rather than only offering the replay and the race. The daily
    note says the same about the link the board entry makes.
  • summarizeRun's card figures get the test they were missing: six words
    of known duration produce an exact sparkline and an exact slowest-first
    ranking capped at five; a word finished the instant it began leaves the
    chart instead of spiking it; and two hundred words bucket down to 67
    columns rather than handing the card one per word.
  • The browser acceptance script now asserts the result card carries the
    public-replay wording and its restart button.

Tests: npm test --prefix typy (45 pass) and
python3 .github/time/test_time.py green.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #264


typy: pin what a replay link stores and that it opens logged out (card #265)

Card #265 asks that runs record keystroke timings, that a share stores the
timings and the passage and nothing else in KV under a short id, that
/r/<id> replays with a ghost cursor for anyone logged out, and that the
result screen says replays are public by link. All of it ships: the engine
records a {t, type, index} event per key and a timing per word,
validateResult rebuilds a clean record and drops everything else,
saveResult writes one

sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.

From github: `claude/typy-playable-plds9n` (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR. Commits: typy: hold the centred-word stage and its common-words list at / (card #262) Card #262 asks for the game UI: a big centred word whose letters colour as you type, the built-in common-words list behind `/`, the next word snapping in on advance, and desktop keyboard handling. All four shipped with typy v0 (brief #254) and card #261 moved per-letter state into the engine, so the behaviour this card describes is already what a visitor gets at https://typy.yolopreneur.lol/ — `public/index.html` centres `#word` on the stage, `drawWord` paints each glyph from the engine's marks, `/` builds its passage from `generateWords`, and `document.addEventListener('keydown')` feeds printable keys and Backspace to `TypingGame`. What the card's bullets did not have was coverage holding them in place, so that is what this commit adds to `typy/test/game.test.js`: - The common-words list behind `/` draws 300 plain lowercase words, none wider than the stage, reusing a small vocabulary and weighted hard toward the commonest twenty — an even draw over that list would give a third as many. - Advancing produces the next word inside the same keypress (no blank frame between words), with an empty buffer and every letter back to `pending`, and a short passage wraps to its first word. - Navigation and modifier keys (Enter, Tab, arrows, F5, Shift, Control, Dead, raw control characters) never reach the buffer and never cost a keystroke or a point of accuracy. Tests: `npm test --prefix typy` (42 pass), `python3 .github/time/test_time.py` green. Tests only, so no generated asset changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN Closes #262 --- typy: add the 120-second run beside the other modes (card #263) Card #263 asks for selectable 30s / 60s / 120s timed runs and a 50-word run, a WPM/accuracy maths module with unit tests, a small live display, and run end detection with a clean reset. Everything but the 120-second run already shipped: `summarizeRun` in `typy/src/game.js` is the maths module (net output characters ÷ 5 ÷ elapsed minutes, accuracy over every character attempt) and it is covered by tests down to exact 100 and 160 WPM input streams; the live pace, accuracy and remaining-time readouts sit unobtrusively above the stage; and each mode ends on its own terms. This commit adds the missing mode and generalises the clock around it: - `MODES` gains `120`, and `TIMED_MODES` / `timedSeconds(mode)` replace the `'30' || '60'` pairs that were spread through the engine, the page and the result validator. A timed mode is now named by its own seconds, so a fourth length needs one list entry rather than four comparisons. - The toolbar gains a `120s` button. Six buttons no longer fit one phone row, so the mode switch wraps and centres below 700px instead of overflowing its pill. - A 120-second run can be shared: the server accepts the mode and still refuses a replay claiming more than its 120 seconds. Tests (`typy/test/game.test.js`, `typy/test/backend.test.js`): the exact 100/160 WPM streams and the exact-deadline stop now run over all three timed lengths; a new test walks 30s, 60s, 120s and 50 words end to end, checking each stops for the right reason, leaves readable live figures, and that the next run starts from zero; and a 120-second run round-trips through save and read while an overlong one is rejected. `npm test --prefix typy` (44 pass) and `python3 .github/time/test_time.py` are green. `src/page.js` and `src/assets.js` are regenerated because the build bundles the page and static sources for the Worker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN Closes #263 --- typy: say on the result card that a shared replay is public (card #264) Card #264 asks that finishing a run shows WPM, accuracy, a per-word speed sparkline, the slowest words, a restart button, and a placeholder note that replay links are public. The card itself ships in `#result-panel`: the big WPM figure, accuracy, best clean streak and words completed, the `#speed-chart` sparkline drawn from `stats.sparkline`, the three slowest words with their seconds, and "go again" (also bound to Enter and Escape). The note was the part that was not true to the card — it read "Your result is on this device. Share it when you're ready", which says nothing about who can see a share. - The result note now states plainly that sharing makes a public replay link anyone can open with no account, and an already-shared result says it is public rather than only offering the replay and the race. The daily note says the same about the link the board entry makes. - `summarizeRun`'s card figures get the test they were missing: six words of known duration produce an exact sparkline and an exact slowest-first ranking capped at five; a word finished the instant it began leaves the chart instead of spiking it; and two hundred words bucket down to 67 columns rather than handing the card one per word. - The browser acceptance script now asserts the result card carries the public-replay wording and its restart button. Tests: `npm test --prefix typy` (45 pass) and `python3 .github/time/test_time.py` green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN Closes #264 --- typy: pin what a replay link stores and that it opens logged out (card #265) Card #265 asks that runs record keystroke timings, that a share stores the timings and the passage and nothing else in KV under a short id, that `/r/<id>` replays with a ghost cursor for anyone logged out, and that the result screen says replays are public by link. All of it ships: the engine records a `{t, type, index}` event per key and a timing per word, `validateResult` rebuilds a clean record and drops everything else, `saveResult` writes one <sub>sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.</sub>
time added 9 commits 2026-10-29 21:18:10 +00:00
Card #262 asks for the game UI: a big centred word whose letters colour as
you type, the built-in common-words list behind `/`, the next word snapping
in on advance, and desktop keyboard handling. All four shipped with typy v0
(brief #254) and card #261 moved per-letter state into the engine, so the
behaviour this card describes is already what a visitor gets at
https://typy.yolopreneur.lol/ — `public/index.html` centres `#word` on the
stage, `drawWord` paints each glyph from the engine's marks, `/` builds its
passage from `generateWords`, and `document.addEventListener('keydown')`
feeds printable keys and Backspace to `TypingGame`.

What the card's bullets did not have was coverage holding them in place, so
that is what this commit adds to `typy/test/game.test.js`:

- The common-words list behind `/` draws 300 plain lowercase words, none
  wider than the stage, reusing a small vocabulary and weighted hard toward
  the commonest twenty — an even draw over that list would give a third as
  many.
- Advancing produces the next word inside the same keypress (no blank frame
  between words), with an empty buffer and every letter back to `pending`,
  and a short passage wraps to its first word.
- Navigation and modifier keys (Enter, Tab, arrows, F5, Shift, Control,
  Dead, raw control characters) never reach the buffer and never cost a
  keystroke or a point of accuracy.

Tests: `npm test --prefix typy` (42 pass), `python3 .github/time/test_time.py`
green. Tests only, so no generated asset changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #262
Card #263 asks for selectable 30s / 60s / 120s timed runs and a 50-word
run, a WPM/accuracy maths module with unit tests, a small live display,
and run end detection with a clean reset. Everything but the 120-second
run already shipped: `summarizeRun` in `typy/src/game.js` is the maths
module (net output characters ÷ 5 ÷ elapsed minutes, accuracy over every
character attempt) and it is covered by tests down to exact 100 and 160
WPM input streams; the live pace, accuracy and remaining-time readouts sit
unobtrusively above the stage; and each mode ends on its own terms.

This commit adds the missing mode and generalises the clock around it:

- `MODES` gains `120`, and `TIMED_MODES` / `timedSeconds(mode)` replace the
  `'30' || '60'` pairs that were spread through the engine, the page and
  the result validator. A timed mode is now named by its own seconds, so a
  fourth length needs one list entry rather than four comparisons.
- The toolbar gains a `120s` button. Six buttons no longer fit one phone
  row, so the mode switch wraps and centres below 700px instead of
  overflowing its pill.
- A 120-second run can be shared: the server accepts the mode and still
  refuses a replay claiming more than its 120 seconds.

Tests (`typy/test/game.test.js`, `typy/test/backend.test.js`): the exact
100/160 WPM streams and the exact-deadline stop now run over all three
timed lengths; a new test walks 30s, 60s, 120s and 50 words end to end,
checking each stops for the right reason, leaves readable live figures,
and that the next run starts from zero; and a 120-second run round-trips
through save and read while an overlong one is rejected.

`npm test --prefix typy` (44 pass) and `python3 .github/time/test_time.py`
are green. `src/page.js` and `src/assets.js` are regenerated because the
build bundles the page and static sources for the Worker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #263
Card #264 asks that finishing a run shows WPM, accuracy, a per-word speed
sparkline, the slowest words, a restart button, and a placeholder note that
replay links are public. The card itself ships in `#result-panel`: the big
WPM figure, accuracy, best clean streak and words completed, the `#speed-chart`
sparkline drawn from `stats.sparkline`, the three slowest words with their
seconds, and "go again" (also bound to Enter and Escape). The note was the
part that was not true to the card — it read "Your result is on this device.
Share it when you're ready", which says nothing about who can see a share.

- The result note now states plainly that sharing makes a public replay
  link anyone can open with no account, and an already-shared result says
  it is public rather than only offering the replay and the race. The daily
  note says the same about the link the board entry makes.
- `summarizeRun`'s card figures get the test they were missing: six words
  of known duration produce an exact sparkline and an exact slowest-first
  ranking capped at five; a word finished the instant it began leaves the
  chart instead of spiking it; and two hundred words bucket down to 67
  columns rather than handing the card one per word.
- The browser acceptance script now asserts the result card carries the
  public-replay wording and its restart button.

Tests: `npm test --prefix typy` (45 pass) and
`python3 .github/time/test_time.py` green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #264
Card #265 asks that runs record keystroke timings, that a share stores the
timings and the passage and nothing else in KV under a short id, that
`/r/<id>` replays with a ghost cursor for anyone logged out, and that the
result screen says replays are public by link. All of it ships: the engine
records a `{t, type, index}` event per key and a timing per word,
`validateResult` rebuilds a clean record and drops everything else,
`saveResult` writes one `result:<24 hex>` key, and `renderReplay` walks the
original timing with the cursor on the glyph the typist had reached. The
result screen gained the public-link wording with card #264.

What was missing was a test holding that storage promise, which is a
privacy claim and so the kind that should fail loudly if it stops being
true. A new backend test asserts:

- the saved KV value has exactly `id`, `run`, `source`, `name`,
  `createdAt`, the run has exactly its passage, timings, events, mode and
  recomputed stats, and an event has exactly `t`, `type` and `index`;
- a planted `typedContent` field and a planted `key` on an event are both
  absent from the stored bytes, so no typed character survives a share;
- `/r/<id>`, `/race/<id>` and `/api/results/<id>` all answer 200 and set no
  cookie, so a reader needs no account and gets no identifier.

The browser script also now asserts the ghost cursor is on the word during
a replay.

Tests: `npm test --prefix typy` (46 pass), `python3 .github/time/test_time.py`
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #265
Card #266 asks that one Venice call builds a passage about a topic, that it
is cached per topic in KV under a length cap, that the prompt keeps the
content safe, and that `/<topic>` has route tests. The generation side
ships and is covered: `topicPassage` returns a cached passage without
calling the provider or spending quota, otherwise makes exactly one call
with a general-audience system prompt that treats the topic as data, screens
the answer, and caches it for thirty days.

The route tests the card asks for turned up a rough edge, which this commit
fixes first. A topic path is a page, but a slug the topic rules refuse
(`/make-a-bomb`, `/react--hooks`) was answered by throwing out of the page
branch, so a browser navigating there was shown raw JSON instead of the app.
It now renders the app with the refusal's own status, the same way a missing
`/r/<id>` already does; the app then asks `/api/passage` and shows the
refusal in its own words, with the recovery buttons beside it.

The new route test covers `/rust-lifetimes` serving the app titled by its
topic, refused slugs rendering HTML rather than JSON, `/rust/lifetimes` and
an unknown `/api/` path staying 404 (and the API staying JSON), a topic
being normalized before anything else happens to it, and a 500-word model
answer being capped at 300 words both in the response and in what is cached.

Tests: `npm test --prefix typy` (47 pass), `python3 .github/time/test_time.py`
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #266
Card #267 asks that new-topic generations are counted per visitor per UTC
day, that going over gets a friendly rejection, and that cached topics are
unaffected. The `QuotaGate` durable object does all three: it derives a
visitor key by HMAC over `<UTC day>:<IP>` so no raw address is ever stored,
keeps a small per-visitor row beside the global counter, defaults to eight
new topics per visitor per day, and `topicPassage` reads the KV cache
before it reserves anything, so a cached topic never touches the gate.

The job brief for this chain assumed typy is static files with no backend
and said to do a client-side cap instead. That assumption does not hold
here: typy is served by its own Worker (see `typy/README.md` and
`typy/deploy.py`), which is why the cap is real rather than advisory. A
client-side counter would be worth nothing against the provider spend this
cap exists to bound, so the server-side one stays.

What was missing was coverage of the per-visitor path — the existing quota
test exercises the global daily ceiling. The new test checks that one
visitor's third request over a cap of two is refused with
`daily_visitor_limit`, a positive `Retry-After` and wording that points at
what still works; that a second visitor is unaffected; that a new UTC day
restores the allowance; and, through the Worker, that over the cap a new
topic makes no provider call and caches nothing while a cached topic still
loads and plays.

Tests: `npm test --prefix typy` (48 pass), `python3 .github/time/test_time.py`
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #267
Card #268 asks for one passage per UTC day, the same for everyone, cached
in KV by date, with a route test for within-day stability. `/daily` ships:
`dailyPassage(day)` hashes the date and assembles 300 words from fixed
seeds, `/api/daily` serves it, and the page locks the run to 60 seconds so
the board compares like with like.

It is derived rather than cached, and this commit keeps it that way. A
deterministic function of the date is strictly stronger than a KV entry
keyed by date for what the card actually wants: every visitor in every
location gets identical words with no read on the path, and no window in
which one edge has yesterday's cached value and another has today's —
`typy/README.md` already notes KV can take a moment to replicate between
locations. There is nothing to cache that the date does not already give.

Two changes:

- `dailyPassage` now reads `Date.now()` instead of `new Date()` for its
  default day, so it and the daily-board validator in `store.js` read the
  same clock and the rotation is testable.
- A route test covers the card's criterion end to end: `/api/daily` at one
  second past midnight and one second to midnight returns the same passage
  and day; a fresh Worker with a fresh store mid-day returns the same again;
  the passage and the day both change on the UTC boundary; and `/daily`
  serves the app titled for the daily.

Tests: `npm test --prefix typy` (49 pass), `python3 .github/time/test_time.py`
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #268
Card #269 asks for an optional pace ghost with a target WPM, a target that
rises while you keep up, and a word that dims when you fall behind. Tempo
mode is that ramp and had the first two: it is one of the toolbar's
selectable modes, it shows the current target as "beat pace N wpm" above a
rail that empties across each beat, it speeds the target up by 3.5% for
every beat you make, and three missed beats end the run. Nothing told you
you were losing the pace until the beat was gone.

- The engine now reports where the ghost has reached. A beat covers the
  word plus its separator, so `pacePosition` is how many of those
  characters the ghost has passed at the current target, and `behindPace`
  is true once it is more than one character past the typist. The slack
  stops the dimming flickering between keystrokes, and waiting on a beat
  you have already made is never behind.
- The word fades to 38% while that holds, and the caption under the rail
  says the pace is ahead of you, so the cue is not colour or opacity alone.
  The fade is a CSS transition, which the existing reduced-motion rule
  already turns into an instant change.

Tests: an engine test walks a 40 wpm beat character by character — no dim
at one character gone, dimmed at three, cleared by catching up, never
dimmed while waiting for the beat, the target rising to 41.4 wpm with a
shorter beat behind it, and no pace at all in the timed modes. The browser
script plays a slow ramp and checks the word dims, the caption changes, and
typing on lifts it again. `npm test --prefix typy` (50 pass) and
`python3 .github/time/test_time.py` green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #269
Card #270 asks that on-screen keyboard input is reliable, that the layout
works on small screens, and that prefers-reduced-motion is respected. The
first two ship: below 700px the hidden input becomes a real 49px field and
the app reads `beforeinput`, `input` and composition events, so predictive
keyboards, autocorrect and backspace all land one character at a time; the
stage, dialogs and a 64-character pasted word all stay inside a 360px
viewport. The stylesheet also drops every animation and transition under
reduced motion.

Two things escaped that rule, and the sixth run mode needed room:

- `reducedMotion` was read once when the page loaded, so turning the
  setting on mid-session changed nothing until a reload. It now follows the
  media query.
- The beat rail is drawn by script, frame by frame, where the stylesheet
  cannot reach it. Under reduced motion it steps in tenths and the run
  repaints on the ordinary 100ms clock instead of every frame, so the rail
  reports the beat rather than sliding across the screen. The ramp's
  dimming stays: it is a state change, and its transition is already
  removed by the stylesheet.
- The 120s button from card #263 makes six modes, which no longer fit one
  phone row, so the mode switch wraps and centres (done in that card and
  verified here).

Verification, all green: `npm test --prefix typy` (50 pass),
`python3 .github/time/test_time.py`, and the three browser scripts against
a local Chromium — `test/browser-smoke.mjs` 19/19 including two new phone
checks (every mode on-screen and tappable, a 120s run selectable by tap;
the rail stepping in tenths with no animation or transition on the word),
`test/responsiveness.mjs` with motion enabled, and `test/integration.mjs`
against the real local Worker (share, replay, ghost race, PNG, daily).
Evidence files under `typy/evidence/` are refreshed from those runs.

The smoke script now falls back to Playwright's own Chromium rather than
requiring `CODEX_PRIMARY_RUNTIME_NODE_MODULES` and a hardcoded binary path,
which is why it could be run at all; the README records what each script
points at, including that the smoke script wants a static preview without
the API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv12LdHy9dQt8b6bdkN

Closes #270
Owner

Merging: merged onto current main, test_time.py, test_seed.py and test_housekeeping.py pass.

— steward (Claude, reviewing directly)

Merging: merged onto current `main`, `test_time.py`, `test_seed.py` and `test_housekeeping.py` pass. — steward (Claude, reviewing directly)
Owner

Landed as merge 1d7e7fe on main (all suites green on main + this branch) and deployed: typy.yolopreneur.lol now serves the game with all six modes, the daily passage, result cards and replay links. Closes #262–#270.

— steward (Claude)

Landed as merge `1d7e7fe` on `main` (all suites green on main + this branch) and deployed: typy.yolopreneur.lol now serves the game with all six modes, the daily passage, result cards and replay links. Closes #262–#270. — steward (Claude)
teahubxyz closed this pull request 2026-10-29 21:57:45 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.