Let a card say more than its title: a plain-text note, on the card page #48 creates.
A card on tempo is one line and nothing else (card table: title, position, timestamps). On a board two people and an agent share, the thing people reach for first after a title is why — a link, a paragraph of context, what "Ship it" actually means. This is the smallest honest version of that: one plain-text note per card, no markdown, no attachments, no comments thread.
Done looks like
[ ] note TEXT NOT NULL DEFAULT '' on card, added with the in-place migration pattern src/db.ts already uses for owner_id (PRAGMA table_info + ALTER TABLE), so a database that exists today gains the column on boot without losing a row. A test opens a database written without the column and reads a card from it.
[ ] The card page gets a <textarea name="note" maxlength="2000"> and a Save button — a plain form, working with JavaScript off, posting to the card's existing route.
[ ] setCardNote is a mutation like every other one: inside the same transaction, logging its previous value to the action table so Undo brings the old note back, and carrying the human label #43 asks for (edited the note on "Ship it").
[ ] The note is validated at the boundary — trimmed, capped at 2000 characters, control characters stripped as #34 asks — escaped through escape() on output, and rendered with white-space: pre-wrap so newlines survive without any HTML being interpreted.
[ ] A card with a note shows one small marker on the board (a character with an aria-label, e.g. "has a note"), costing no extra request and no layout shift.
[ ] Saving a note reaches other people's pages live, through whatever the bus publishes for a card edit once #12 lands, so a note typed by one person appears for the others within the second.
[ ] MCP keeps up: list_cards returns note, and add_card takes an optional note argument with the same cap and the same stripping. tools/list describes it.
[ ] Tests that fail without the change: the migration; a note round-trips through the page; Undo restores the previous note; a note containing <script> and an ANSI escape comes back inert; list_cards carries it. bun test green.
Files
src/db.ts, src/pages/card.ts, src/pages/board.ts (the marker), src/routes.ts, src/mcp.ts, src/db.test.ts, src/server.test.ts, README.md.
Risks
Waits for #48 — without the card page there is nowhere to put a textarea that does not undo #48's whole point. It also touches every mutation's undo payload, like #42 and #43, so land those first or expect a conflict in src/db.ts. A 2000-character note times the 500-card cap is about 1 MB of text per board: that is the number to state in the README beside the other caps, not a reason to skip the cap.
Out of scope
Markdown or any rendering of the note as HTML. Comments, history, or per-person authorship of a note — attribution is #18.
Suggested: scout, reviewed by sentinel (it is someone else's text, on a board anyone can edit).
Let a card say more than its title: a plain-text note, on the card page #48 creates.
A card on tempo is one line and nothing else (
cardtable:title,position, timestamps). On a board two people and an agent share, the thing people reach for first after a title is why — a link, a paragraph of context, what "Ship it" actually means. This is the smallest honest version of that: one plain-text note per card, no markdown, no attachments, no comments thread.Done looks like
note TEXT NOT NULL DEFAULT ''oncard, added with the in-place migration patternsrc/db.tsalready uses forowner_id(PRAGMA table_info+ALTER TABLE), so a database that exists today gains the column on boot without losing a row. A test opens a database written without the column and reads a card from it.<textarea name="note" maxlength="2000">and a Save button — a plain form, working with JavaScript off, posting to the card's existing route.setCardNoteis a mutation like every other one: inside the same transaction, logging its previous value to the action table so Undo brings the old note back, and carrying the human label #43 asks for (edited the note on "Ship it").escape()on output, and rendered withwhite-space: pre-wrapso newlines survive without any HTML being interpreted.aria-label, e.g. "has a note"), costing no extra request and no layout shift.list_cardsreturnsnote, andadd_cardtakes an optionalnoteargument with the same cap and the same stripping.tools/listdescribes it.<script>and an ANSI escape comes back inert;list_cardscarries it.bun testgreen.Files
src/db.ts,src/pages/card.ts,src/pages/board.ts(the marker),src/routes.ts,src/mcp.ts,src/db.test.ts,src/server.test.ts,README.md.Risks
Waits for #48 — without the card page there is nowhere to put a textarea that does not undo #48's whole point. It also touches every mutation's undo payload, like #42 and #43, so land those first or expect a conflict in
src/db.ts. A 2000-character note times the 500-card cap is about 1 MB of text per board: that is the number to state in the README beside the other caps, not a reason to skip the cap.Out of scope
Markdown or any rendering of the note as HTML. Comments, history, or per-person authorship of a note — attribution is #18.
Suggested: scout, reviewed by sentinel (it is someone else's text, on a board anyone can edit).