Intent. A negotiation that runs for hours needs somewhere to persuade, so every act may carry one short public line — unaudited, never scored, and only on your turn, so the rhythm of the game is unchanged and the said register keeps its meaning.
Done looks like
[ ] RULES.md gains a short section after §7 naming it: talk is free text carried by an act, at most 280 characters, public forever, never verified, never scored, and never a substitute for a said claim — if you want a claim about your charge audited at the reveal, you say it, and saying it is what gets a verdict. §12's table of what each rule reads gains its row.
[ ] Migration appended to migrations in src/db.ts (never editing one that has run): act.talk TEXT NULL.
[ ] makeOffer, acceptOffer and passTurn take an optional talk, validated at the boundary in the action: trimmed, 1–280 characters after trimming, no newline and no control character, absent or empty means none. One named failure per rule, with a sentence a player can act on.
[ ] Talk appears in tableState's act log for everyone, guests included, and in the MCP table_state payload — it is public by construction, so there is nothing here to redact.
[ ] No new spam surface: one act is one turn, so the turn rule already caps talk. A test proves a second line out of turn is refused by the turn rule and not by a new one.
[ ] Tests: 280 characters accepted and 281 refused; a newline and a control character refused; <script> in talk comes back escaped from the page and from the terminal; a guest reading a playing table sees the talk and no charge.
[ ] The page shows the line under its act in the log, wrapping, never truncated silently; the terminal shows it the same way.
FilesRULES.md, src/db.ts, src/actions.ts, src/actions.test.ts, src/routes.ts, src/mcp.ts, src/server.test.ts, and the table page (in src/pages/ if #12 has landed, otherwise that row lands with #12).
Risks This is the app's only unstructured input: escape it at every door — page, terminal and MCP text — and keep it out of the score and out of the reveal's verdicts, or the said register stops being the thing you are audited on. Resist making it a chat room: talk rides an act, and there is no endpoint that posts talk alone.
Brief that fits @vesper — the rules engine of a game, with the boundary written first. (A person assigns.)
Intent. A negotiation that runs for hours needs somewhere to persuade, so every act may carry one short public line — unaudited, never scored, and only on your turn, so the rhythm of the game is unchanged and the
saidregister keeps its meaning.Done looks like
RULES.mdgains a short section after §7 naming it: talk is free text carried by an act, at most 280 characters, public forever, never verified, never scored, and never a substitute for a said claim — if you want a claim about your charge audited at the reveal, you say it, and saying it is what gets a verdict. §12's table of what each rule reads gains its row.migrationsinsrc/db.ts(never editing one that has run):act.talk TEXT NULL.makeOffer,acceptOfferandpassTurntake an optionaltalk, validated at the boundary in the action: trimmed, 1–280 characters after trimming, no newline and no control character, absent or empty means none. One named failure per rule, with a sentence a player can act on.tableState's act log for everyone, guests included, and in the MCPtable_statepayload — it is public by construction, so there is nothing here to redact.<script>in talk comes back escaped from the page and from the terminal; a guest reading a playing table sees the talk and no charge.Files
RULES.md,src/db.ts,src/actions.ts,src/actions.test.ts,src/routes.ts,src/mcp.ts,src/server.test.ts, and the table page (insrc/pages/if #12 has landed, otherwise that row lands with #12).Risks This is the app's only unstructured input: escape it at every door — page, terminal and MCP text — and keep it out of the score and out of the reveal's verdicts, or the said register stops being the thing you are audited on. Resist making it a chat room: talk rides an act, and there is no endpoint that posts talk alone.
Brief that fits @vesper — the rules engine of a game, with the boundary written first. (A person assigns.)