When the sea goes wrong — a round wedged, a field poisoned by a tuning mistake, a gate nobody can
reach — an admin ends it and starts the next one through a door, not by pushing a commit and waiting
for a deploy.
The world is one long-lived process and #11 tunes it against real play, so recovery has to be an
operation. who.ts already separates admin from user, and nothing in the app uses the
distinction yet. #30 makes a dead loop say so; this is how a person answers it.
Done looks like
[ ] One action in src/actions.ts — resetRound(db, who, input) — and the rule lives only there:
a who.role that is not admin gets a named failure, at every door, including the terminal.
[ ] It ends the round in progress as abandoned rather than scoring it as a win, clears the current
field, and starts a fresh round with a new gate. The tick never stops and nobody reloads.
[ ] Every door calls it and none of them decides anything: POST /api/round/reset (same-origin,
403 for a non-admin, rationed like any write), an MCP tool with one sentence and a schema, a
key in src/tty.ts, and a control on the page rendered only for an admin.
[ ] round.ended then round.started emit on the bus exactly as a natural round boundary does, so
an open page and an open socket both follow it without a special case (#40 decides which bus
carries which).
[ ] An abandoned round is distinguishable in the data from one that ended on the clock or on the
gate, so the ladder can decline to count it.
[ ] Tests: each door refuses a guest and a signed-in non-admin; an admin's reset ends the old round
and the new one has a different gate; the abandoned round is marked; the loop is driven by hand,
no sleeping test.
Risks This is a button that ends twenty-three other people's round: say in the pull what stands
between a stray tap and a reset, and make the page's control the one that is hardest to hit by
accident. Do not add a second way to mutate the world — if the terminal or MCP needs anything the
action does not expose, the action grows, not the door. An admin door is also the first thing an
attacker reaches for: the refusal is tested at each door, not assumed from one.
Waits on #8.
Fits Vesper's brief: a game has a loop, a state and a way to save.
When the sea goes wrong — a round wedged, a field poisoned by a tuning mistake, a gate nobody can reach — an admin ends it and starts the next one through a door, not by pushing a commit and waiting for a deploy.
The world is one long-lived process and #11 tunes it against real play, so recovery has to be an operation.
who.tsalready separatesadminfromuser, and nothing in the app uses the distinction yet. #30 makes a dead loop say so; this is how a person answers it.Done looks like
src/actions.ts—resetRound(db, who, input)— and the rule lives only there:a
who.rolethat is notadmingets a named failure, at every door, including the terminal.field, and starts a fresh round with a new gate. The tick never stops and nobody reloads.
POST /api/round/reset(same-origin,403 for a non-admin, rationed like any write), an MCP tool with one sentence and a schema, a key in
src/tty.ts, and a control on the page rendered only for an admin.round.endedthenround.startedemit on the bus exactly as a natural round boundary does, soan open page and an open socket both follow it without a special case (#40 decides which bus carries which).
gate, so the ladder can decline to count it.
and the new one has a different gate; the abandoned round is marked; the loop is driven by hand, no sleeping test.
Files
src/actions.ts,src/actions.test.ts,src/round.ts,src/routes.ts,src/mcp.ts,src/tty.ts,src/page.ts,src/server.test.ts.Risks This is a button that ends twenty-three other people's round: say in the pull what stands between a stray tap and a reset, and make the page's control the one that is hardest to hit by accident. Do not add a second way to mutate the world — if the terminal or MCP needs anything the action does not expose, the action grows, not the door. An admin door is also the first thing an attacker reaches for: the refusal is tested at each door, not assumed from one.
Waits on #8.
Fits Vesper's brief: a game has a loop, a state and a way to save.