Public: anyone can read this; the crew can change it.

Direction: A negotiation game played over hours, not minutes: four to six players, secret goals, public offers, and a deal that bin #1

the repository's standing direction — always open, read by the planner directly, never picked up like an ordinary tell opened this
opened this

A negotiation game played over hours, not minutes: four to six players, secret goals, public offers, and a deal that binds when it is struck. Turns are asynchronous: you play from your phone whenever you like, and the page and the terminal both tell you when it is your move. The crew's own agents can sit in as players over MCP (the game is an MCP server: join, state, offer, accept, pass). Identity comes from the forge (the template's src/who.ts is the check); guests can watch a public table. The planner proposes three rule-sets as issues, picks the one with the sharpest tension between honesty and advantage, closes the others with a word, and maps it. Novelty is the point: no known board game re-skinned.

This issue carries the repository's direction. The planner is given it when the map has run dry; it opens and links the issues that follow from it, and this one stays open.

commented

Three rule-sets were proposed and judged: The Manifest (#3, closed — thin: unverifiable cheap talk over a known bargaining problem) and The Covenant (#4, closed — slow: commitment, not honesty). The one adopted is Sworn & Said (#5): an accord binds when a quorum signs, and every signature costs the signer one true, public aim — you cannot accept a deal that pays you nothing, and every deal you accept tells the room what you were paid. Lying survives in the cheap said register and is audited in front of everyone at settlement.

The map that follows it: #5 rules of record → #6 the deal (seeded charter and charges) and #7 tables and seats → #8 the turn → #9 sworn and said → #10 the binding and the reveal → #11 the clock, #12 the table page, #13 the MCP tools, #14 the terminal → #15 the lobby and the public table, #16 "it is your move". Ready now: #5, #6, #7.

commented

Read the whole template (it is still the notes demo: actions.ts as the one implementation with page, API, MCP and tty as doors), booted it to confirm / answers with the security floor intact, and turned the direction into a map. Nothing was pushed and the branch is untouched.

Three rule-sets, judged

  • #3 The Manifest — secret valuations over a split hold. Closed: Thin. (unverifiable cheap talk over a textbook bargaining problem).
  • #4 The Covenant — promises the engine enforces on your future turns. Closed: Slow. (its tension is commitment, not honesty, and it needs more rounds than a day allows).
  • #5 Sworn & Said — chosen. An accord binds when a quorum signs, and accepting costs a truth: you must cite one real aim of your secret charge that the accord satisfies, it becomes public forever, and an accord that pays you nothing cannot be accepted at all. Anything else you assert is recorded as said, unverified, and marked true or false in front of everyone at the reveal. That is the sharpest honesty-versus-advantage trade of the three, and it is only possible because a machine holds the hidden state.

Opened, in order

  • #5 Chosen rule-set: Sworn & Said — write the rules of record as RULES.md → @scout
  • #6 The deal: a charter and four secret charges from a seed, in src/board.ts → @torque
  • #7 A table you can open and sit at: schema, seats and state, replacing the notes demo → @ledger
  • #8 The turn: offer, accept, pass — and whose move it is (also carries startTable, per the amendment comment) → @ledger
  • #9 Sworn and said: an acceptance costs a true aim, and a claim is a claim → @vesper
  • #10 The accord binds: quorum, scoring, and the reveal → @vesper
  • #11 The clock: turn windows, the deadline, and the impound, derived from timestamps → @hearth
  • #12 The table, on a phone: the whole game server-rendered, live, playable with a thumb → @quill
  • #13 An agent can play: the MCP tools for join, state, offer, accept and pass → @scout
  • #14 Play from the terminal: a table over ssh, and it tells you when it is your move → @hearth
  • #15 The lobby: open tables, a public table to watch, and a seat you can take → @quill
  • #16 "It is your move": one honest signal on the page, in the API and in the terminal → @scout

Links: #8 waits on #6 and #7; #9 on #8; #10 on #9; #11, #12, #13, #14 on #10; #15 on #12; #16 on #11, #12 and #14. The direction #1 stays open and carries a comment with the verdict and the map.

Ready now: #5, #6, #7. A stray issue #2 was created while probing the API and is closed. I assigned nobody — the suggestions above are suggestions.

commented

The rules of record landed (#5, merged as #17), so the first stone of the map is in place and the chain below it moved. Two things I did with that merge: the two rules the pull left open are now settled by RULES.md itself, and I said so on the issues that implement them — a charge's three positive weights are distinct (#6), and a positive aim can be sworn once in a game, ever (#9).

The problem with the map as it stood: #6 and #7 are in flight and everything else waited behind them in one long line — zero issues a free builder could pick up. So this round is four issues that do not wait on the engine, three of them ready right now.

Opened, in order

  • #19 No private view is ever cached: private, no-store and Vary on every answer that depends on who is asking → @scout. Measured on the branch today: / and /api/notes carry the CSP, nosniff, referrer and permissions headers and no cache-control and no vary at all. For a game whose whole point is a redacted view per viewer, that is the hole to close before the redaction exists.
  • #20 Time in words: src/when.ts, and a countdown that does not move the page → @hearth. #11, #12, #14 and #16 all need "4h 12m left" in the same words; one pure module, now injected, and one interval for the whole page.
  • #21 The look of a table: the layout, the charter grid, and a /style page that proves it at 360px → @quill. The components #12 and #15 will call, rendered from fixtures at a route you can open in the preview, so the look is reviewable before the game is playable.
  • #22 A word with your act: one public line of table talk, on your turn only → @vesper. The next increment the game itself needs: hours of negotiation with no way to persuade is austere, and talk that rides an act (280 characters, public, unaudited, never scored) buys persuasion without touching the said register or the turn rhythm.

Links added: #22 waits on #8; #11, #12 and #14 wait on #20; #12 also waits on #21.

Ready now: #19, #20, #21 — none of them touches src/actions.ts, src/db.ts or src/page.ts, so all three can run beside #6 and #7 without a conflict worth the name. I assigned nobody.

commented

One more, because #19 was picked up within the minute and the map needs three a free builder can start on:

  • #23 The live channel carries no secret and no waste: events name their table, and a phone that slept catches up → @hearth. src/live.ts already replays, heartbeats and backs off; what it does not do is keep a payload off the wire. For a game that redacts per viewer, the rule has to be written down and tested — an event's data is an identifier, never content — and a page left open on a phone for six hours has to come back correct rather than stale. #16 waits on it; emit keeps its current signature so #6 and #7 are untouched.

Ready now: #20, #21, #23.

commented

The wordless feedback was the merge: #5's pull (#17) landed, so RULES.md is the rules of record and the two rules its author flagged are now settled — I wrote those settlements onto the issues that implement them (#6: the three positive weights are distinct; #9: a positive aim can be sworn once in a game, ever, and §6's "a refusal never leaks" is a test). The map's real problem was that #6 and #7 were in flight and every other issue queued behind them, so nothing was startable; this round is work that runs beside the engine.

Opened, in order:

  • #19 — No private view is ever cached: private, no-store and Vary on every answer that depends on who is asking. Measured on the branch: / and /api/notes carry the CSP, nosniff, referrer and permissions headers and no cache-control and no vary at all. Suggested: @scout. (claimed by @mason while I was still filing)
  • #20 — Time in words: src/when.ts, and a countdown that does not move the page. One pure module and one interval, so #11, #12, #14 and #16 say "4h 12m left" in the same words. Suggested: @hearth.
  • #21 — The look of a table: the layout, the charter grid, and a /style page that proves it at 360px. The components #12 and #15 will call, rendered from fixtures at a route you can open in the preview. Suggested: @quill.
  • #22 — A word with your act: one public line of table talk, on your turn only. 280 characters, public, unaudited, never scored — persuasion without touching the said register. Waits on #8. Suggested: @vesper.
  • #23 — The live channel carries no secret and no waste. An event's data is an identifier, never content; events name their table; a phone that slept comes back correct. emit keeps its signature so #6 and #7 are untouched. Suggested: @hearth.

Links added: #22 ← #8; #11, #12, #14 ← #20; #12 ← #21; #16 ← #23. Nothing is parked, I assigned nobody, and #1 stays open with the round written on it.

Ready now: #20, #21, #23 — none of them touches src/actions.ts, src/db.ts or src/page.ts, so all three run beside #6, #7 and #19 without a conflict worth the name. Nothing was pushed; the branch is clean, which is what a planning run should leave behind. I filed five rather than my usual four because #19 was taken within a minute of being opened and the map would otherwise have had two startable issues.

commented

#6 landed (merged as #24): src/board.ts deals a charter and four secret charges from a seed, scores an accord, and brute-forces 2187 accords in its own test to prove a dealt board is worth arguing over. Result/ok/fail moved to src/result.ts on the way, which is the right shape — a pure rules module should not import bun:sqlite to refuse an input. #7 (pull #18) and #19 (pull #25) are open and waiting on a person, and #23 is being written, so the engine chain below #8 still waits on #7's merge.

This round is four issues, two of them startable today and both in files no open pull touches.

Opened, in order

  • #26 What an accord pays you: satisfied, citable and a validated accord, the pure half of the sworn rule in src/board.ts → @vesper. §6 is arithmetic over a charge and an accord and needs no database; putting it beside score() now leaves #9 as wiring and a boundary instead of a rule invented inside an action, and gives #12 and #14 the number a player needs before they sign. #9 now waits on it.
  • #27 A wreck worth arguing over: the clause pool grows to 18, proved over a thousand seeds → @torque. Seven of twelve makes the second table feel like the first. This is cheap today and expensive the moment #8 stores a seed on a real table, because widening the pool re-deals every unfinished game — so it is filed ahead of the engine deliberately.
  • #28 A record that follows you: what you swore and what you said, across every table → @ledger. Waits on #10. The direction's next increment: honesty costs nothing if the table forgets, so a player carries counts — signatures made, weight sworn, said claims proved false — built only from finished tables, and never read by the rules.
  • #29 Learn it in two minutes: a /rules page that teaches the game rather than specifying it → @quill. Waits on #21. RULES.md is 400 lines of record; a player who has just been dealt a charge needs 400 words and a charter they can look at, with every number imported from src/board.ts so the two cannot drift.

Ready now: #20, #21, #26, #27. None of the four touches src/db.ts, src/actions.ts, src/page.ts, src/http.ts or src/live.ts, so all of them run beside the three pulls in flight. I assigned nobody.

commented

The wordless feedback was #6 merging (pull #24): src/board.ts now deals and scores a board from a seed, with Result/ok/fail split out into src/result.ts. #7 (pull #18) and #19 (pull #25) are open awaiting a person, #23 is being written, so everything below #8 still waits on #7 — this round is work that runs beside it, in files no open pull touches.

Opened, in order:

  • #26 — What an accord pays you: satisfied, citable and a validated accord, the pure half of the sworn rule in src/board.ts. §6 is arithmetic over a charge and an accord; putting it beside score() leaves #9 as wiring and a boundary instead of a rule invented in an action, and gives #12 and #14 the number a player needs before they sign. #9 now waits on it. Suggested: @vesper.
  • #27 — A wreck worth arguing over: the clause pool grows to 18, proved over a thousand seeds. Seven clauses from twelve makes the second table feel like the first. Cheap today, expensive the moment #8 stores a seed on a real table, since widening the pool re-deals every unfinished game — filed ahead of the engine deliberately. Suggested: @torque.
  • #28 — A record that follows you: what you swore and what you said, across every table. Waits on #10. The direction's next increment: counts built only from finished tables — signatures made, weight sworn, said claims proved false — public, factual, and never read by the rules. Suggested: @ledger.
  • #29 — Learn it in two minutes: a /rules page that teaches the game rather than specifying it. Waits on #21. Every number imported from src/board.ts so the page and the record cannot drift. Suggested: @quill.

Links added: #9 ← #26; #28 ← #10; #29 ← #21.

Ready now: #20, #26, #27 (#21 was claimed while I was filing). Nothing is parked, I assigned nobody, and #1 stays open with the round written on it. Nothing was pushed — the branch is clean, which is what a planning run leaves behind.

commented

#19 landed (merged as #25): every answer that read an identity is now private, no-store with Vary naming the headers it depended on, which is the floor this game needs before it has anything secret to redact. #7 (pull #18) is still open and waiting on a person, and with it the whole engine chain from #8 down — #26 and #21 are being written, #23 is in pull #30.

So this round is three issues again chosen to run beside the pulls, in files no open pull owns.

Opened, in order

  • #31 The app stops on purpose: a deploy in the middle of a six-hour table loses nothing → @hearth. src/server.ts opens a database and listens and has no way to stop: no SIGTERM, no close, no grace. A game that lasts a day on an app that redeploys on every merge needs stopping to be deliberate — finish in flight, close the streams so the browsers reconnect themselves, checkpoint and close the database.
  • #32 The bar, enforced: one test that fails when the app gets heavy, slow or dependent → @scout. AGENTS.md states the bar and nothing checks it. One test file: no dependencies in package.json, static/ under a stated weight, every HTML route complete on the first byte and under 50 ms, no inline script — discovering routes from routes.ts so the pages #12, #15, #21 and #29 add are covered the day they land. Cheap now, while the numbers are small.
  • #33 A table fills, or it dies: an invite link, a seat an agent can take, and open tables that expire → @ledger. Waits on #8. The likeliest way this game is never played is a table stuck at two seats: a link you can send, start_when_full, an abandoned status for a table nobody joined, and the lobby sorted by how close each table is to full. Agents sit down through the MCP join_table tool like anyone else — no invite token, no new door.

Ready now: #20, #27, #31, #32. None of them touches src/actions.ts, src/page.ts, src/board.ts or src/live.ts. The one thing the map cannot route around is #18: until it merges, #8 and everything under it stays blocked, and what I can open is work beside the critical path rather than on it.

commented

The wordless feedback was #19 merging (pull #25) — the caching floor is in: every answer that read an identity is private, no-store with Vary naming the headers it depended on. #7 (pull #18) is still open awaiting a person, and with it the whole engine chain from #8 down; #21 and #26 are being written and #23 is in pull #30. So this round is again work that runs beside the critical path, in files no open pull owns.

Opened, in order:

  • #31 — The app stops on purpose: a deploy in the middle of a six-hour table loses nothing. src/server.ts opens a database and listens and has no way to stop — no SIGTERM, no close, no grace. A day-long game on an app that redeploys on every merge needs a deliberate stop: finish in flight, close the event streams so browsers reconnect themselves, checkpoint and close the database. Suggested: @hearth.
  • #32 — The bar, enforced: one test that fails when the app gets heavy, slow or dependent. AGENTS.md states the bar and nothing checks it. One test file: no dependencies, static/ under a stated weight, every HTML route complete on the first byte and under 50 ms, no inline script — routes discovered from routes.ts so pages added by #12, #15, #21 and #29 are covered the day they land. Suggested: @scout.
  • #33 — A table fills, or it dies: an invite link, a seat an agent can take, and open tables that expire. Waits on #8. The likeliest way this game is never played is a table stuck at two seats: a shareable link, start_when_full settled in the same transaction as the last join, an abandoned status caught lazily at read time, and a lobby sorted by how close each table is to full. Agents sit down through MCP join_table like anyone else — no invite token, no new door. Suggested: @ledger.

Links added: #33 ← #8.

Ready now: #20, #27, #31, #32. Nothing is parked, I assigned nobody, #1 stays open with the round written on it, and nothing was pushed — the branch is clean, which is what a planning run leaves behind. The one thing the map cannot route around is pull #18: until it merges, #8 and everything beneath it stays blocked.

commented

#26 landed (merged as #35): validAccord, satisfied and citable are in src/board.ts with §6's refusal sentences in one constant, the §10 transcript replayed in the test, and a leak test asserting no refusal names a clause, a setting, a weight or a seat. That is the second time the pure half has gone first and made the wiring easy, so this round makes it the pattern.

Opened, in order

  • #36 The turn and the binding as pure arithmetic: src/round.ts, before the ledger writes a row → @vesper. Whose turn it is, what stands, who has signed, whether it binds, and what the settlement and the reveal are — plain data in, plain data out, §10's nine acts replayed as the test. #8, #9 and #10 now wait on it, which costs nothing while they wait on #7's pull anyway, and turns three rules-heavy runs into a schema, a transaction and a boundary. It is a new file, so it is startable today beside every open pull.
  • #37 Is it a good game? A headless tournament that proves honesty is a real dilemma → @torque. Waits on #36 and #10. The direction chose this rule-set for the sharpest tension between honesty and advantage; that is a claim, and ten thousand games against four scripted players — honest, greedy, liar, quiet — will say whether deals bind, whether any strategy dominates, and whether a table is ever unwinnable. If a number says the game is broken, the pull says so rather than tuning the rules until they look good.
  • #38 An agent that has never played: MCP resources and a prompt that teach the table → @scout. Waits on #13. An agent that needs the rules pasted into a system prompt is not sitting down on the same terms as a person: parley://rules, parley://tables/{id} through the same redaction, and one sit_at_a_table prompt built from the same constants as everything else.

Amended: #27 gains the invariant that matters most in a sweep — every table must be signable. Quorum is ⌈n/2⌉+1 and a seat can only accept an accord that pays it an unsworn positive aim, so a deal where no accord pays a quorum of seats at once can only ever impound. Brute-force it over the accords that test already walks, count how many such accords a deal admits, and redraw rather than deal a table nobody can sign.

Ready now: #27, #31, #32, #36. Pull #18 is still the one thing the map cannot route around; everything under #8 waits on it.

commented

The wordless feedback was #26 merging (pull #35): validAccord, satisfied and citable are in src/board.ts, with §6's refusal sentences in one constant and a leak test asserting no refusal names a clause, setting, weight or seat. That's twice now that doing the pure half first made the wiring easy, so this round makes it the pattern.

Opened, in order:

  • #36 — The turn and the binding as pure arithmetic: src/round.ts, before the ledger writes a row. Whose turn it is, what stands, who has signed, whether it binds, the settlement and the reveal — plain data in, plain data out, with §10's nine acts replayed as the test. #8, #9 and #10 now wait on it, which costs nothing while they already wait on pull #18, and it turns three rules-heavy runs into a schema, a transaction and a boundary. New file, so it is startable today beside every open pull. Suggested: @vesper.
  • #37 — Is it a good game? A headless tournament that proves honesty is a real dilemma. Waits on #36 and #10. The direction chose this rule-set for the sharpest honesty-versus-advantage tension; ten thousand seeded games against four scripted players will say whether deals bind, whether a strategy dominates, and whether a table is ever unwinnable — and a bad number gets reported, not tuned away. Suggested: @torque.
  • #38 — An agent that has never played: MCP resources and a prompt that teach the table. Waits on #13. parley://rules, parley://tables/{id} through the same redaction as every other door, and one sit_at_a_table prompt built from the same constants. Suggested: @scout.

Amended #27 with the invariant that matters most in a sweep: every table must be signable. Quorum is ⌈n/2⌉+1 and a seat can only accept an accord paying it an unsworn positive aim, so a deal where no accord pays a quorum at once can only ever impound — brute-force it, count how many such accords a deal admits, and redraw rather than deal a table nobody can sign.

Links added: #8, #9, #10 ← #36; #37 ← #36, #10; #38 ← #13.

Ready now: #27, #31, #32, #36. Nothing is parked, I assigned nobody, #1 stays open, and nothing was pushed — the branch is clean. Pull #18 remains the one thing the map cannot route around: everything under #8 waits on it.

commented

#7 landed (merged as #18) — the notes demo is gone and the game's own state is in its place. I played the merged app through its API as two signed-in people before planning: a table opens with quorum: 3 and its clocks set, a second player takes a seat, a third join comes back 409 conflict with a sentence, an anonymous open is 401, an unknown table is 404, the guest view and the player view both carry charge: null, and / answers in about 3 ms. The redaction is where it should be. Two things a customer notices immediately, and both are now issues.

Opened, in order

  • #40 A table has a URL: GET /tables/:id, linkable and watchable before the game can be played → @quill. Waits on #21. Today GET /tables/1 is a 404: / is the whole app, and a table is not something you can send anybody. Give a table its address now — seats, host, status, clocks, quorum, join and leave, live — and let #12 grow the game into that same page instead of inventing it at the end of the chain. #12 and #33 now wait on it.
  • #41 The terminal, under test: src/tty.ts split so every screen is a pure function of state → @hearth. The terminal shipped with the tables and has no test at all — it opens the database at module scope and draws straight to stdout. Split it while it is still ninety lines, a test per screen, FORGE_APP_SSH printed verbatim or the terminal declared off, and a guest screen that offers no key that writes. #14 now waits on it, and starts from tests instead of from a refactor.

Ready now: #27, #31, #32, #36, #41. #36 is the keystone: with #7 merged, #8 waits on nothing else, so the pure round arithmetic is what stands between the map and a game you can actually play.

commented

#23 was parked — pull #30 did the payload rule, the topics and the browser client in one run, and when #7 landed underneath it the branch conflicted in src/actions.ts and the check failed. The work in mason/23-the-live-channel-carries-no-secret-and-n is sound; the scope was too wide to rebase. Split into three that each land on today's main:

  • #23, rewritten to the payload rule alone: emit validates that a payload is identifiers, the rule goes in AGENTS.md, today's four emits get a test each, and /events finally answers with the security floor. It touches src/live.ts and nothing in src/actions.ts, which is where the last attempt died. Ready now.
  • #42 Events name their table — the topic on emit, ?topic= on the stream, topic-aware replay, and one word per call site in src/actions.ts. The comment that says the filter is not authorization is part of done. Waits on #23.
  • #43 A phone that slept catches up — the browser half: visibility, bfcache, reconnect and a new gap event, each causing exactly one coalesced refetch, tested against the small hand-built DOM that branch already wrote. Waits on #42, and #16 now waits on it rather than on #23.

The branch is closed but not lost: each of the three says to read it and take what fits.

commented

The wordless feedback was the biggest merge yet — #7 landed (pull #18), so the notes demo is gone and tables, seats and acts are in production. I played it through its API as two signed-in people before planning: a table opens with quorum: 3 and its clocks set, a second player sits, a third join comes back 409 conflict with a sentence, an anonymous open is 401, the guest view and the player view both carry charge: null, and / answers in about 3 ms. The redaction is exactly where it belongs. Two things a customer hits at once, and the parked issue, are this round.

Opened, in order:

  • #40 — A table has a URL: GET /tables/:id, linkable and watchable before the game can be played. Waits on #21. Today that path is a 404 — / is the whole app and a table is not something you can send anyone. Seats, host, status, clocks, quorum, join and leave, live, with #12 growing the game into the same page rather than inventing it at the end of the chain. #12 and #33 now wait on it. Suggested: @quill.
  • #41 — The terminal, under test: src/tty.ts split so every screen is a pure function of state. Ready now. The terminal shipped with the tables and has no test at all — it opens the database at module scope and draws straight to stdout. Split it while it is ninety lines, a test per screen, FORGE_APP_SSH verbatim or the terminal declared off, and a guest screen that offers no key that writes. #14 now waits on it. Suggested: @hearth.
  • #42 — Events name their table and #43 — A phone that slept catches up, the two halves split out of the parked #23.

#23 was parked because pull #30 did the payload rule, the topics and the browser client in one run and then conflicted in src/actions.ts when #7 landed under it. The work is sound, the scope was unrebasable. The forge would not let me edit the body (invalid_state on PATCH), so the rewrite is a planner comment on the issue: #23 is now the payload rule alone — emit validates that a payload is identifiers, the rule goes in AGENTS.md, today's four emits each get a test, and /events finally answers with the security floor — touching src/live.ts and nothing in src/actions.ts. #42 takes the topics, #43 the browser, and #16 waits on #43 now. All three point at the closed branch as a draft to take from.

Links added: #40 ← #21; #12, #33 ← #40; #14 ← #41; #42 ← #23; #43 ← #42; #16 ← #43.

Ready now: #23, #27, #31, #32, #41, with #36 being written — it is the keystone, since #8 waits on nothing else. Nothing is parked any more, I assigned nobody, and nothing was pushed: the clone is clean and the temporary worktree I used to play the merged app is removed.

commented

#36 and #20 landed (merged as #44 and #39). src/round.ts is the whole pure game — whoseTurn, standing, quorum, binds, ending, settlement, sworn, trueWorth, mayAct — with §10's nine acts replayed in its test, the act shape { seat, kind, accord?, cite?, at } declared, the turn derived rather than stored, and §10's charter and charges lifted into src/example.ts so every test checks the same hand-checkable table. #8 is ready, and with it the rest of the engine.

The map is healthy — six ready — so this round is mostly making sure the three engine runs wire what exists instead of rewriting it.

Amended

  • #8 is now wiring, not a rules engine: store exactly round.ts's act shape, import whoseTurn/standing/quorum/binds/mayAct, and spend the run on the boundary and the transaction. A rule that appears both in an action and in round.ts is a bug.
  • #9 keeps one genuinely missing piece — nothing validates a said claim's shape yet, and that boundary belongs in src/board.ts beside validAccord. The refusal sentences already exist in one constant; no door invents wording.
  • #10 is the transaction and the record: settle in one transaction with the acceptance that triggered it, store what settlement() returns rather than recomputing on read, and prove a settled table reads the same after a restart.
  • #40 also takes not found is a page — today a stale table link answers four plain words of text — and renders its clocks with the merged src/when.ts.

Opened

  • #45 Every preview opens on a game worth looking at: a demo table seeded from §10 → @hearth. Waits on #10. Every pull gets a preview with an empty database, so a reviewer sees an empty lobby and cannot judge the page a settled table renders. Three seeded tables — open, playing, settled with the full reveal — written through the same code path the actions use, guarded twice (empty database and FORGE_APP_ENV=preview), never in production, and plainly labelled as dealt for the preview.

Ready now: #8, #23, #27, #31, #32, #41 — #8 first among them: it is the run that turns a table you can sit at into a game you can play.

commented

The wordless feedback was two merges: #36 (pull #44) and #20 (pull #39). src/round.ts is now the whole pure game — whoseTurn, standing, quorum, binds, ending, settlement, sworn, trueWorth, mayAct — with §10's nine acts replayed in its test, the act shape { seat, kind, accord?, cite?, at } declared for the ledger, the turn derived rather than stored, and §10's charter and charges lifted into src/example.ts so every test checks one hand-checkable table. #8 became ready and has already been claimed, so the engine is moving.

With six issues ready, this round is mostly making sure the three engine runs wire what exists instead of rewriting it:

  • #8 — amended to wiring, not a rules engine: store exactly round.ts's act shape, import whoseTurn/standing/quorum/binds/mayAct, spend the run on the boundary and the transaction. A rule living both in an action and in round.ts is a bug.
  • #9 — amended: the one genuinely missing piece is that nothing validates a said claim's shape, and that boundary belongs in src/board.ts beside validAccord. The refusal sentences are already one constant; no door invents wording.
  • #10 — amended: the transaction and the record. Settle in one transaction with the acceptance that triggered it, store what settlement() returns rather than recomputing on read, and prove a settled table reads the same after a restart.
  • #40 — amended to also carry not found is a page (a stale table link answers four plain words of text today) and to render its clocks with the merged src/when.ts.

Opened:

  • #45 — Every preview opens on a game worth looking at: a demo table seeded from RULES.md §10. Waits on #10. Every pull gets a preview with an empty database, so a reviewer sees an empty lobby and can never see what a settled table renders. Three seeded tables — open, playing, settled with the full reveal — written through the same code path the actions use, guarded twice (empty database and FORGE_APP_ENV=preview), never in production, plainly labelled as dealt for the preview. Suggested: @hearth.

Ready now: #23, #27, #31, #32, #41, with #8 and #21 being written. Nothing is parked, I assigned nobody, #1 stays open, and nothing was pushed — the clone is clean.

commented

#21 landed (pull #34), so the look exists before the game does: src/pages/layout.ts is the one document, src/pages/table.ts is every part a table is made of — charter grid, sworn and said claims, act log, clock slot, offer form — and /style proves them all from §10's fixture, public, max-age=60, in about a millisecond. src/page.ts gave up its copy of the layout in the same merge, so the duplication that pull warned about is already gone. #40 and #29 became ready with it.

I played main again before planning: / in 3 ms, a table opened and sat at through the JSON API as two signed identities, the charge redacted for both, a cross-site POST refused with 403, the MCP handshake answering initialize, tools/list and a refusal as a tool error rather than a crash. Three things a customer still meets: /tables/1 is nine bytes of not found (#40), /rules is a 404 (#29), and /events answers without the security floor (#23). All three are already ready issues, so this round adds what the map did not have.

Opened, in order

  • #46 — Two acts cannot take one turn: the ledger writes inside a transaction that re-checks, and a race proves it. Waits on #8. The turn is derived from the count of acts, so two writers that read the same ledger both believe they hold the turn, and today nothing in the schema stops the second one landing — with four doors already able to race and the clock's lapsed pass still to come. An immediate transaction that re-checks mayAct inside itself, UNIQUE(game_id, ordinal) as the guard SQLite enforces (never as a second source of truth for whose turn it is), one settlement per quorum, and a test with two connections on one file. Suggested: @ledger.
  • #47 — A settled table is public: the reveal reads without an account, and it is the one page a cache may keep. Waits on #10 and #40. By §9 a finished table has no secrets, which makes the reveal the only thing this app can serve identically to everyone: byte-identical for guest, player, host and admin, public, max-age=60 only when settled or impounded, private in every other state and a test that walks a table one act short of the binding to prove it. It is also the best artefact the game produces and today it would be unshareable. Suggested: @vesper.
  • #48 — The front door says what this is: a lobby a stranger can read in ten seconds. Waits on #29 and #40. Production's / is a form, a list, and a link to JSON; nothing says what the game is or what a signature costs. Forty words and a link to the rules, an empty state that invites, a table's title linking to the table. The lobby's shape stays #15; this is its words and its links. Suggested: @quill.
  • #49 — A whole game, through the doors: RULES.md §10 played over HTTP and MCP, and a reveal that matches the fixture. Waits on #10 and #13. round.ts is proved against §10; the app is not. Four signed identities, nine acts played through the form door, the JSON API and POST /mcp, the redaction checked after every act, /events carrying identifiers only, and the settlement asserted against src/example.ts — the fixture is the oracle, and if the app disagrees with §10, §10 wins. This is the acceptance test of the direction. Suggested: @scout.

Links added: #46 ← #8; #47 ← #10, #40; #48 ← #29, #40; #49 ← #10, #13.

Ready now: #27, #29, #31, #32, #40, #41, with #8 and #23 being written. Nothing is parked, I assigned nobody, #1 stays open, and nothing was pushed — the clone is clean.

commented

Planning round on the merge of #21 (pull #34): the look of a table now exists — one layout, src/pages/table.ts as every part a table is made of, /style proving them from RULES.md §10's fixture — and #40 and #29 came ready with it. I replayed main before planning: / in 3 ms, a table opened and sat at as two signed identities with the charge redacted for both, a cross-site POST refused, the MCP handshake answering initialize and tools/list correctly; /tables/1 is still nine bytes of not found, /rules is a 404, and /events still answers without the security floor — all three already have ready issues, so this round adds what the map lacked.

Opened, in order:

  • #46 — Two acts cannot take one turn: the ledger writes inside a transaction that re-checks, and a race proves it (waits on #8) → @ledger. The turn is derived from the act count, four doors can already race, and nothing in the schema stops the second writer.
  • #47 — A settled table is public: the reveal reads without an account, and it is the one page a cache may keep (waits on #10, #40) → @vesper. Byte-identical for guest, player, host and admin; public, max-age=60 only when settled or impounded, with a test that walks a table one act short of the binding.
  • #48 — The front door says what this is: a lobby a stranger can read in ten seconds (waits on #29, #40) → @quill. Forty words, a link to the rules, an empty state that invites, and a table's title linking to the table instead of to JSON.
  • #49 — A whole game, through the doors: RULES.md §10 played over HTTP and MCP, and a reveal that matches the fixture (waits on #10, #13) → @scout. The acceptance test of the direction: four identities, nine acts across three doors, the settlement asserted against src/example.ts.

Links added: #46 ← #8; #47 ← #10, #40; #48 ← #29, #40; #49 ← #10, #13. A round summary is on #1, which stays open.

Ready now: #27, #29, #31, #32, #40, #41, with #8 and #23 being written. Nothing is parked, I assigned nobody, and nothing was pushed — the clone is clean at main.

commented

The wordless feedback was two pulls opening rather than a merge: #50 (#8, the turn — the ledger, startTable, offer/accept/pass over the API) and #51 (#23, the payload rule and the floor on /events) are both open against today's main, so the engine chain is moving but nothing new has landed since #21. I replayed main before planning: / in 7 ms with the whole floor and private, no-store, /style in under a millisecond, /api/tables an empty list, /rules and /tables/1 still four plain words of not found — both already ready issues (#29, #40).

One correction to earlier rounds while I am here: the crew of this repository is @mason (builds), @juniper (reviews the experience) and @sentinel (reviews security). Earlier comments of mine suggested names that are not on it; read those as @mason with the matching reviewer.

The map had five unclaimed ready issues, so this round is the next increment rather than more of the chain — one thing that is true today and untested, and three that a demanding player or agent will want the moment the engine lands.

Opened, in order

  • #52 — The look holds up under a keyboard, a thumb and poor light: contrast, focus and 44px, proved by a test. static/app.css claims all three in its own opening comment and nothing checks any of them. I measured the palette: every text pair passes 4.5:1 today (muted 4.67 light / 6.57 dark, good 4.63, danger 4.78, accent 5.10), so this is a guard on something already good, plus the skip link the layout does not have. Deliberately not #32, which guards bytes and milliseconds. Ready now. @mason, reviewed by @juniper.
  • #53 — The clock ticks when nobody is looking: a silent table still passes, still impounds, still tells the next seat (waits on #11 and #46). #11 settles the clock at read time, which means an overnight table with no reader never passes and never impounds, and #16's "it is your move" never fires for the seat whose turn it became. One in-process ticker, no rule of its own, idle-cheap, stopped with the server. @mason, with @sentinel on the one writer that has no person behind it.
  • #54 — What happened while you were away: a seat's read mark, and "3 acts since you last looked" (waits on #8 and #12). The question you actually have on your sixth visit to a day-long table. A nullable column on seat, a count in tableState, one divider in the act log, and a test that a guest's page is byte-identical with or without it. @mason, with @juniper on the wording.
  • #55 — An agent waits for its turn instead of polling: one MCP call that returns when the table moves (waits on #13). The direction wants crew agents at the table on the same terms as a person; today an agent can only poll. wait_for_turn rides the bus src/live.ts already has, caps its waiters, treats a timeout as an answer, and is not authorization. @mason, with @sentinel on the held connection.

Links added: #53 ← #11, #46; #54 ← #8, #12; #55 ← #13.

Ready now: #27, #29, #31, #32, #41, #52, with #8, #23 and #40 in @mason's hands. Nothing is parked, I assigned nobody, #1 stays open, and nothing was pushed — the clone is clean at main.

commented

This was a planning run: no code changed, the working tree is clean at 51f3174, and there is nothing to commit or push — the map is the result.

Done: four issues filed against the direction (#1) and linked. #52 guards the look that static/app.css already claims — contrast (measured on main: every text pair passes 4.5:1 in both palettes), :focus-visible, 44px targets, and the skip link the layout lacks — as one dependency-free test; it is ready now. #53 closes the hole #11 leaves open, where a table with no reader never passes a lapsed turn and never impounds (waits on #11, #46). #54 adds a seat's read mark and a "3 acts since you last looked" divider (waits on #8, #12). #55 gives an agent wait_for_turn over the bus src/live.ts already has instead of a polling loop (waits on #13). A round summary is on #1, which stays open, and it corrects earlier rounds that suggested crew names this repository does not have — the crew is @mason, @juniper and @sentinel.

Not done, deliberately: no pull, no assignment, no code. Ready now for a builder: #27, #29, #31, #32, #41, #52, with #8, #23 and #40 in @mason's hands and pulls #50 and #51 awaiting review — those two both add or constrain emit, so whichever merges second will need the other's event list updated.

Budget reached at $2.53; this is where it stopped.

commented

Three pulls are open and nothing has merged since #21, so this round is the increment the map did not have. I replayed today's main before planning: / in 3 ms with the whole floor and private, no-store, a table opened and four seats taken as four signed identities with the charge redacted for every viewer, a cross-site POST refused, the rate limiter refusing 28 of 40 rapid writes, /static/..%2F..%2Fpackage.json a 404, <script> in a title escaped on the lobby. Three things a demanding caller meets that the map did not own, each measured rather than guessed.

Opened, in order

  • #60 — The agent door refuses what it did not advertise: MCP arguments checked against each tool's schema, and an id that is digits. Measured: tools/call open_table {"title": 123} creates a table named "123", an unknown argument (seats: 6) is dropped in silence so the agent gets four seats believing it asked for six, and GET /api/tables/1e3 names table 1000 because find() parses with bare Number(). A model cannot correct a mistake it is never told about, and this direction seats agents on the same terms as people. Ready now. @mason, reviewed by @sentinel.
  • #61 — The app says out loud when the forge has not told it who it is: a boot check for identity, address and data. With FORGE_APP_IDENTITY_KEY unset every request is anon, so nobody can open a table or take a seat and nothing says why; with FORGE_APP_URL unset publicOrigin is http://localhost, so every absolute link points at localhost, Secure comes off the cookie and HSTS off the floor. A pure settings(), one line per finding at boot, fatal in production, never printing a value. Ready now. @mason, reviewed by @sentinel.
  • #62 — One line of text is one line: src/words.ts, so a title cannot hide, reverse itself or lie about its length. Measured: a title with U+200B is accepted and renders as if it were not there, U+202E reverses everything printed after it on the page and in the terminal, NBSP passes the control-character check, and the 60 counts UTF-16 units so 31 emoji are refused and 60 combining marks are not. One gate, the way src/when.ts is one vocabulary — before #22 writes a fourth set of string rules by hand. Ready now. @mason, reviewed by @juniper.
  • #63 — No charge leaves by any door: one property test over many seeds, every viewer and every read (waits on #8 and #41). seesCharge redacts in one place and #49 plays §10 once; neither guards the next read — a route, a tool from #13, a screen from #41 or a column from a migration. The oracle is the row read back from the database, so a new secret fails the test until someone redacts it. @mason, reviewed by @sentinel.

Links added: #13 ← #60 (the tools it adds inherit the checked schema); #22 ← #62 (talk gets the same gate); #63 ← #8, #41.

Ready now: #27, #29, #31, #32, #52, #60, #61, #62, with #8, #23, #40 and #41 in @mason's hands and pulls #50, #51 and #56 awaiting review. Nothing is parked, I assigned nobody, #1 stays open, and nothing was pushed — the clone is clean at main. Two stray issues created while probing the API (#58, #59) are closed.

commented

This was a planning run against the direction (#1): no code changed, the working tree is clean at 51f3174, and there is nothing to commit or push — the map is the result.

I replayed today's main before planning: / in 3 ms with the whole security floor and private, no-store, a table opened and four seats taken as four signed identities with every charge redacted for every viewer, a cross-site POST refused, 28 of 40 rapid writes rate-limited, /static/..%2F..%2Fpackage.json a 404, <script> in a title escaped on the lobby. Three pulls are open (#50 the turn, #51 the live payload rule, #56 the table's URL) and nothing has merged since #21, so this round is what the map did not own rather than more of the chain.

Filed, in order:

  • #60 — The agent door refuses what it did not advertise: MCP arguments checked against each tool's schema, and an id that is digits. Measured: open_table {"title": 123} creates a table named "123", an unknown argument (seats: 6) is dropped in silence, and /api/tables/1e3 names table 1000. Ready now. Suggested @mason, reviewed by @sentinel.
  • #61 — The app says out loud when the forge has not told it who it is: a boot check for identity, address and data. Unset FORGE_APP_IDENTITY_KEY makes every visitor a guest with no log line; unset FORGE_APP_URL points every absolute link at localhost and strips Secure and HSTS. Ready now. Suggested @mason, reviewed by @sentinel.
  • #62 — One line of text is one line: src/words.ts, so a title cannot hide, reverse itself or lie about its length. U+200B accepted, U+202E reverses the page and the terminal, NBSP passes the control check, 60 counts UTF-16 units. Ready now, and #22's talk inherits the gate. Suggested @mason, reviewed by @juniper.
  • #63 — No charge leaves by any door: one property test over many seeds, every viewer and every read (waits on #8 and #41). The oracle is the row read back from the database, so a secret added by a later migration fails the test until it is redacted. Suggested @mason, reviewed by @sentinel.

Links added: #13 ← #60, #22 ← #62, #63 ← #8 and #41. A round summary is on #1, which stays open. Two stray issues created while probing the API (#58, #59) are closed.

Ready now: #27, #29, #31, #32, #52, #60, #61, #62, with #8, #23, #40 and #41 in @mason's hands. Nothing is parked and I assigned nobody.

commented

The wordless feedback was four merges since the last round — #18 the schema and seats, #39 src/when.ts, #44 src/round.ts, #34 the look and /style, #57 the terminal's screens — with pulls #50 (#8, the turn), #51 (#23, the live payload) and #56 (#40, a table's URL) still open. I replayed main (dd58653) before planning: / in 1 ms with the whole floor and private, no-store, /style in 6 ms, a table opened and joined by two signed identities with every charge redacted for every viewer, /tables/1 and /rules still 404 (both already ready issues, #40 and #29). Eight issues were already ready, so this round is four things the map did not own — two of them measured holes in merged code.

Opened, in order

  • #65 — An open table always has its host at it: close a table instead of leaving it empty. Measured: a host can leave their own table, and it stays open with seated: 0 and a host who holds no seat, in the lobby forever as 0/4 seated · nobody yet — which src/actions.ts says in a comment cannot happen. The host is refused the leave and gets closeTable instead, at all four doors, open only. Ready now. @mason, reviewed by @sentinel.
  • #66 — A wrong address is still parley: an error page a person can read, an error body an agent can parse, and 405 for the wrong method. Measured: /nope, /tables/1 and /api/nope are all four words of text/plain with no layout and no way back, POST / is 404 rather than 405, 429 carries no retry-after, and the 500 is a bare <h1> outside the layout — while every failure raised inside an action is already { error, message }. Ready now. @mason, reviewed by @juniper and @sentinel.
  • #67 — Reads are rationed too: a cap on open event streams, per person and in all (waits on #23). Measured: 120 concurrent /events from one client are all accepted, 135 descriptors, RSS 53.7 → 58.4 MB, each with its own heartbeat, and allow() rations writes only. Over the cap the person's oldest stream closes, never the newest — a reload must not blank its own page. @mason, reviewed by @sentinel.
  • #68 — A seat can be given up mid-game: the rule written into RULES.md, and the ledger act that produces it (waits on #8). src/round.ts models Seat.left in full — refusal, order kept, clock plays it out — and nothing can ever produce it: RULES.md never mentions it, there is no column, no act and no action, and leaveTable refuses once play starts. The rule is decided in the issue (the quorum does not fall with the seats) so the builder writes it rather than invents it, and the rejected alternative is named.

Links added: #67 ← #23, #68 ← #8.

Ready now: #27, #29, #31, #32, #52, #60, #61, #62, #65, #66, with #8, #23 and #40 in @mason's hands as open pulls. Nothing is parked, I assigned nobody, and #1 stays open.

commented

This was a planning run against the direction (#1): no code changed, the working tree is clean at dd58653, and there is nothing to commit or push — the map is the result.

I replayed main before planning (/ in 1 ms with the whole floor and private, no-store, /style in 6 ms, a table opened and joined by two signed identities with every charge redacted for every viewer, /tables/1 and /rules still 404 — both already ready issues), then filed four issues the map did not own, two of them measured holes in code that has merged.

  • #65 — An open table always has its host at it: close a table instead of leaving it empty. A host can leave their own table and it stays open with seated: 0 and a host who holds no seat, listed forever as 0/4 seated · nobody yet, which src/actions.ts claims in a comment cannot happen. Ready now; suggested @mason, reviewed by @sentinel.
  • #66 — A wrong address is still parley: an error page a person can read, an error body an agent can parse, and 405 for the wrong method. /nope, /tables/1 and /api/nope are four words of text/plain with no layout and no way back, POST / is 404, 429 has no retry-after, and the 500 is a bare <h1> outside the layout. Ready now; suggested @mason, reviewed by @juniper and @sentinel.
  • #67 — Reads are rationed too: a cap on open event streams, per person and in all (waits on #23). 120 concurrent /events from one client were all accepted — 135 descriptors, RSS 53.7 → 58.4 MB, each with its own heartbeat — while allow() rations writes only. Suggested @mason, reviewed by @sentinel.
  • #68 — A seat can be given up mid-game: the rule written into RULES.md, and the ledger act that produces it (waits on #8). src/round.ts models Seat.left in full and nothing can ever produce it; the issue decides the rule (the quorum does not fall with the seats) and names the alternative it rejects.

Links added: #67 ← #23, #68 ← #8, and a round summary is on #1, which stays open. Nothing is parked and I assigned nobody. Ready now: #27, #29, #31, #32, #52, #60, #61, #62, #65, #66, with #8, #23 and #40 in @mason's hands as open pulls #50, #51 and #56.

commented

The wordless feedback was two more merges: #56 (a table has a URL) and #51 (the live channel's payload rule and catch-up). I replayed main (e933965) before planning: /tables/1 answers in 6 ms with the whole floor and private, no-store, a guest reads the seats with every charge redacted, /events now carries the floor, and a stale link lands on the app's own 404 page instead of four plain words. Ten issues were already ready, so this round is one correction and three things the map did not own.

Corrected, not repeated: #56 landed the 404 half of #66 (src/pages/missing.ts). I rewrote what done looks like on that issue in a comment rather than leaving a builder to redo merged work: what is left is /api/nope answering text/plain where every other API failure is { error, message }, POST / answering 404 where the route table knows it should be 405 with allow, a 429 with no retry-after, and a 500 that is still a bare <h1> outside the layout. It stays ready.

Opened, in order

  • #70 — The page changes under your hands without losing them: focus kept across a live swap, and a word for a screen reader. Measured: static/live.js does el.replaceWith(next) on every [data-live] region on every event, and those regions hold the Sit, Leave and Take a seat buttons — so any join anywhere drops keyboard focus to <body> on every open page, and no region announces anything. The focus rule comes out as a pure function with a test, the way static/when.js already is. Ready now. @mason, reviewed by @juniper.
  • #71 — A link you send looks like an invitation: share tags on a table, and a robots policy for the views that are nobody else's. The app's own advice for filling a table is to copy the Invite field and send it; that link unfurls as a bare host today — no description, no og: tags, no canonical — and there is no robots.txt and no X-Robots-Tag, which private, no-store does not stand in for. Nothing in an unfurl that a guest cannot already see. Ready now. @mason, reviewed by @sentinel and @juniper.
  • #72 — A number the crew can trust: /status, for the person who has to know whether tables are actually being played. Games are about to run for days across deploys and nothing but console.error says anything: no tables by state, no acts in the last day, no schema version at runtime, no count of open streams — which #67 needs to pick its cap. An action like any other, admin-only, counts and versions and no names. Ready now. @mason, reviewed by @sentinel.

No new links: all three stand alone, and each says in its own risks which open work it would rebase on (#70 with #43, #72 with #67).

Ready now: #27, #29, #31, #32, #42, #52, #61, #62, #65, #66, #67, #70, #71, #72, with #8 in @mason's hands as pull #50 and #29 and #60 being written. Nothing is parked, I assigned nobody, #1 stays open, and nothing was pushed — the clone is clean at e933965.

commented

This was a planning run against the direction (#1): no code changed, the working tree is clean at e933965, and there is nothing to commit or push — the map is the result.

I replayed main before planning: /tables/1 answers in 6 ms with the whole floor and private, no-store, a guest reads the seats with every charge redacted, /events now carries the floor, and a stale link lands on the app's own 404 page. #56 and #51 merged since the last round, which made part of an open issue stale, so this round is one correction and three new issues.

  • #66 rewritten in a comment (the forge's API only takes a state change on PATCH): #56 landed its 404 half in src/pages/missing.ts, so what remains is /api/nope answering text/plain where every other API failure is { error, message }, POST / answering 404 rather than 405 with allow, a 429 without retry-after, and a 500 that is still a bare <h1> outside the layout. It stays ready.
  • #70 — focus kept across a live swap, and a word for a screen reader. static/live.js replaces every [data-live] region on every event, and those regions hold the Sit, Leave and Take a seat buttons: any join anywhere drops keyboard focus to <body> on every open page, and nothing announces. Ready now; suggested @mason, reviewed by @juniper.
  • #71 — share tags on a table, and a robots policy. The app tells you to copy the Invite field and send it; that link unfurls as a bare host, and there is no robots.txt or X-Robots-Tag behind the private, no-store views. Ready now; suggested @mason, reviewed by @sentinel and @juniper.
  • #72 — /status for the crew. Games are about to run for days across deploys with nothing but console.error to show for it; an admin-only action returning counts, the schema version and the number of open streams (#67 needs that number to pick its cap). Ready now; suggested @mason, reviewed by @sentinel.

No new dependency links — each of the three stands alone and names in its own risks the open work it would rebase on (#70 with #43, #72 with #67). Ready now: #27, #29, #31, #32, #42, #48, #52, #61, #62, #65, #66, #67, #70, #71, #72, with #8 in @mason's hands as pull #50. Nothing is parked, I assigned nobody, and #1 stays open.

commented

The wordless feedback was the engine landing: #50 merged the turn (offer, accept, pass, startTable) and #69 the /rules page. I played a whole table on main (948abc0) before planning — four signed identities open, sit, start, offer, accept and pass entirely over HTTP; the charter is dealt, my charge is mine alone, and §6 already refuses an acceptance that pays me nothing with "This accord satisfies none of your aims". The app can be played. It cannot be played by a person on a page, or by an agent over MCP, and that is the whole finding of this round.

The map's shape was the problem. Nine issues funnelled through #10, the binding and the reveal, including the three doors — the page (#12), the tools (#13) and the terminal (#14) — none of which needs a reveal to show a turn. So this round is a split, not more chain.

Opened, in order

  • #74 — What you may do, and what it would pay you: may, canStart and the standing offer's worth, in the state itself. taking() computes mayAct on every act and throws it away; the view carries turn, your_turn and standing and nothing else, so every door would have to re-derive §6, and canStart does not exist — a host who opened a table on the page cannot start it without curl. Ready now. @mason, reviewed by @sentinel.
  • #75 — The table plays on the page: the charter, the standing offer, the log and the three acts — before the reveal exists (waits on #74). The pre-reveal half of #12, which is narrowed by a comment to the settlement and the reveal. /tables/1 today shows a started table as its title, its seats and "The seats are fixed", and its data-live does not even list act.made. @mason, reviewed by @juniper.
  • #76 — An agent can take a turn: the MCP tools for start, offer, accept and pass (waits on #74 and #60). The pre-reveal half of #13, likewise narrowed: tools/list answers five tools today and not one of them takes a turn, while the HTTP door plays a whole cycle. This is the direction's headline — the crew's agents at the table on the same terms as a person. @mason, reviewed by @sentinel.
  • #77 — A turn window that passes unplayed is a pass: the lapse, derived at read time. The window half of #11 (narrowed by comment to the deadline and the impound, which are an ending and belong with #10). RULES.md §8's automatic pass needs only the ledger and src/when.ts, both merged, and without it a table stops forever on the first seat that goes quiet. Ready now. @mason, reviewed by @sentinel.

Also done to keep the map true: #9 is narrowed by a comment — the sworn half landed inside #8's acceptOffer (citation, first-aim default, the refusal that leaks nothing), so what is left of #9 is the said register of §7, the half that makes lying a move. And a measurement is recorded on #75: at 300 acts GET /api/tables/1 is 30 KB and 14 ms against 2 KB and 6 ms empty, and a fortnight of 15-minute windows is thousands — bound what the view hands out, never what the arithmetic reads, because the turn is derived from the act count.

Ready now: #9, #27, #31, #32, #33, #42, #46, #48, #52, #61, #62, #63, #65, #66, #67, #68, #70, #71, #72, #74, #77, with #60 in @mason's hands as pull #73. The forge's API has no way to remove a dependency, so #12, #13 and #11 keep their edge on #10 and their comments say what they now mean. Nothing is parked, I assigned nobody, #1 stays open, and nothing was pushed.

commented

This was a planning run against the direction (#1): no code changed, the working tree is clean, and nothing was pushed — the map is the result. (I fast-forwarded the local branch to 948abc0 only to read and run the merged engine; there is nothing of mine to commit.)

I played a whole table on main before planning — four signed identities open, sit, start, offer, accept and pass entirely over HTTP, the charter dealt, each charge private, and §6 already refusing an acceptance that pays the caller nothing. The app can be played; it cannot be played by a person on a page or by an agent over MCP, and nine issues were funnelling through #10 (the binding and the reveal) that do not need it. So this round is a split rather than more chain:

  • #74 — may, canStart and the standing offer's worth, in the state itself. taking() computes mayAct on every act and discards it, and canStart does not exist, so a host who opened a table on the page cannot start it without curl. Ready now.
  • #75 — The table plays on the page (waits on #74): the pre-reveal half of #12, which I narrowed by comment to the settlement and the reveal.
  • #76 — An agent can take a turn: the MCP tools for start, offer, accept and pass (waits on #74 and #60): the pre-reveal half of #13, likewise narrowed — five tools exist today and none takes a turn.
  • #77 — A turn window that passes unplayed is a pass (ready now): the window half of #11, narrowed by comment to the deadline and the impound; without it a table stops forever on the first seat that goes quiet.

I also narrowed #9 — the sworn half shipped inside #8's acceptOffer, so what is left is §7's said register — and recorded on #75 that at 300 acts GET /api/tables/1 is 30 KB and 14 ms against 2 KB and 6 ms empty, so the log must be bounded where it is handed out, never where the arithmetic reads it. The forge's API has no DELETE for dependencies, so #11, #12 and #13 keep their edge on #10 and their comments say what they now mean.

Ready now: #9, #27, #31, #32, #33, #42, #46, #48, #52, #61, #62, #63, #65, #66, #67, #68, #70, #71, #72, #74, #77, with #60 in @mason's hands as pull #73. Nothing is parked and I assigned nobody.

commented

The wordless feedback was #73 merging, so the agent door now checks every argument against the schema it advertised and an id is digits. No pull is open and nothing is parked; #9 (the said register) and #46 (two acts, one turn) are being written. I replayed main (63a4041) before planning: initialize answers 2025-06-18 with a tools capability, open_table {"title": 123, "seats": 6} is refused by name with the argument list, an unknown tool is -32602, /rules renders in 5 ms, and a full turn cycle still plays over HTTP.

Nineteen issues were already ready, so this round is two more splits of the kind that worked last time, and two guards the app has now earned.

Opened, in order

  • #78 — Play a turn from the terminal: the table screen, the three keys, and the charter drawn in ANSI (waits on #74). Measured: src/tty.ts imports six actions and none of the four #50 added, so the terminal has been the lobby alone for two merges while HTTP plays a whole game. The pre-reveal half of #14, which keeps the ending. @mason, reviewed by @juniper.
  • #79 — A fortnight of turns still reads in milliseconds: the acts a read hands out are bounded. Measured: tableState returns every act at every door — 30 KB and 14 ms at 300 acts against 2 KB and 6 ms empty — and the shortest legal window against the longest legal deadline lapses on the order of a thousand acts a day once #77 writes them. The rule that matters is in the issue: bound what the view hands out, never what the arithmetic reads, because the turn is derived from the act count. Ready now. @mason, reviewed by @sentinel.
  • #80 — Every preview opens on a table mid-negotiation: a seeded game a reviewer can read in ten seconds. A preview's lobby is empty today; seeing a played table takes four signed identities and eight curl calls, which is what reviewing this round took. Built from the app's own actions, preview-only, empty-database-only, honestly labelled. The pre-reveal half of #45, which keeps the settled demo. Ready now. @mason, reviewed by @juniper.
  • #81 — The doors cannot fall behind: one test that every action is reachable at every door, or says plainly why not (waits on #76 and #78). This is the drift itself, written down: AGENTS.md promises one implementation and many doors, and two of the four have been a merge behind since #50 with nothing saying so out loud. A textual test with an exception table of reasons, not a to-do list.

Also done to keep the map true: #14 and #45 are narrowed by comments to their reveal halves; #75 hands the act-log bound to #79; and #16 is told that its "page" and "terminal" dependencies now mean #75 and #78, so the "it is your move" signal does not have to wait for the reveal either.

Ready now: #27, #31, #32, #33, #42, #48, #52, #61, #62, #63, #65, #66, #67, #68, #70, #71, #72, #74, #77, #79, #80, with #9 and #46 in @mason's hands. I assigned nobody, #1 stays open, and nothing was pushed.

commented

This was a planning run against the direction (#1): no code changed, the working tree is clean at 63a4041, and nothing was pushed — the map is the result.

I replayed main before planning: the agent door now refuses open_table {"title": 123, "seats": 6} by name with its argument list, an unknown tool is -32602, initialize answers 2025-06-18, /rules renders in 5 ms, and a full turn cycle plays over HTTP. Nineteen issues were already ready, so this round is two more splits and two guards.

  • #78 — Play a turn from the terminal (waits on #74). src/tty.ts imports six actions and none of the four that #50 added, so the terminal has been the lobby alone for two merges. The pre-reveal half of #14, which keeps the ending.
  • #79 — A fortnight of turns still reads in milliseconds (ready now). tableState hands out every act at every door: 30 KB and 14 ms at 300 acts against 2 KB and 6 ms empty, and #77's lapses will make long ledgers normal. Bound what the view hands out, never what the arithmetic reads.
  • #80 — Every preview opens on a table mid-negotiation (ready now). A preview's lobby is empty; seeing a played table takes four signed identities and eight curl calls. Preview-only, empty-database-only, built from the app's own actions. The pre-reveal half of #45.
  • #81 — The doors cannot fall behind: one test that every action is reachable at every door (waits on #76 and #78) — the drift itself written down, with an exception table of reasons rather than silence.

I also narrowed #14 and #45 to their reveal halves by comment, handed the act-log bound from #75 to #79, and told #16 that its page and terminal dependencies now mean #75 and #78, so the "it is your move" signal need not wait for the reveal.

Ready now: #27, #31, #32, #33, #42, #48, #52, #61, #62, #63, #65, #66, #67, #68, #70, #71, #72, #74, #77, #79, #80, with #9 and #46 in @mason's hands. Nothing is parked and I assigned nobody.

commented

Nothing has merged since #73, so the wordless feedback is work starting rather than landing: #82 is open (the transactional ledger, #46), and #9 and #74 are being written. With twenty issues already ready, this round is two findings rather than more queue — one from arithmetic, one from coverage.

Opened, in order

  • #83 — A table cannot be born unable to bind: the deadline is measured against the seats and the window. A binding takes one offer and quorum accept acts, and every act is one turn, so the fastest possible binding is quorum + 1 windows: 4 windows at four seats, 5 at five or six. On the app's own defaults (6h window, 24h deadline) that is exactly the deadline at four seats and past it at five and six — and §8 pays every seat 0 on an impound. The validation in openTable is deadline >= 2 × turn window, so the legal minimum is four windows short of what four seats need. A host who accepts the defaults at six seats is opening a table that cannot be won by patient players. It is a rules change, so RULES.md §2 and §8 lead and the code follows. Ready now. @mason, reviewed by @juniper.
  • #84 — The terminal's loop, under test: a key and a state in, an intent out — without a pty. bun test --coverage on main puts src/tty.ts at 53.07% of lines, all of the gap in run(): the raw-mode keyboard, the dispatch, the typing buffer, the refusal on the status line, the exit path. Every other module is 88% or better and most are 100%, because #57 made the screens pure and stopped there. #78 is about to add three keys to exactly that region. Ready now. @mason, reviewed by @juniper.

Also on the map: #78 gains two findings from reading src/tty.ts — the terminal prints your charge as raw JSON on the lobby line ((your charge: {"seat":0,"aims":[…]}), over your shoulder and in scrollback), and it needs the game's vocabulary in words the way src/pages/table.ts has it. #11 and #33 are told that #83 moves the deadline floor under them.

Ready now: #27, #31, #32, #33, #42, #48, #52, #61, #62, #63, #65, #66, #67, #68, #70, #71, #72, #77, #79, #80, #83, #84, with #9, #46 and #74 in @mason's hands. Nothing is parked, I assigned nobody, #1 stays open, and nothing was pushed — the clone is clean at 63a4041.

commented

This was a planning run against the direction (#1): no code changed, the working tree is clean at 63a4041, and nothing was pushed — the map is the result.

Nothing has merged since #73, so the feedback was work starting rather than landing (pull #82 for the transactional ledger; #9 and #74 in progress). With twenty issues already ready, I filed two findings rather than padding the queue:

  • #83 — A table cannot be born unable to bind. A binding takes one offer plus quorum acceptances, and each act is a turn, so the fastest binding is quorum + 1 windows — 4 at four seats, 5 at five or six. On the app's own defaults (6h window, 24h deadline) that lands exactly on the deadline at four seats and past it at five and six, and §8 pays every seat 0 on an impound; the openTable check is only deadline >= 2 × turn window. It is a rules change, so RULES.md §2 and §8 lead and the code follows. Ready now.
  • #84 — The terminal's loop, under test. bun test --coverage puts src/tty.ts at 53.07% of lines, the whole gap in run() — the raw-mode keyboard, the dispatch, the typing buffer, the refusal line, the exit path — while every other module is 88% or better. #78 is about to add three keys to exactly that region. Ready now.

I also gave #78 two findings from reading src/tty.ts (the terminal prints your charge as raw JSON on the lobby line, and it needs the game's vocabulary in words), and told #11 and #33 that #83 moves the deadline floor under them.

Ready now: #9, #27, #31, #32, #33, #42, #48, #52, #61, #62, #63, #65, #66, #67, #68, #70, #71, #72, #77, #79, #80, #83, #84, with #9, #46 and #74 in @mason's hands. Nothing is parked and I assigned nobody.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs