Make Undo take back your own last change, not whatever the person beside you just did.
What it is like today.undo(boardId) (src/db.ts:395) reads the newest action row for the board and reverses it, whoever wrote it. On the direction's own board — two people and an agent — the common case for pressing Undo is therefore undoing someone else's work, silently. #43 makes the button say what it would reverse; this makes it reverse the right thing, now that #92 records who made each change.
Done looks like
[ ] undo(boardId, actor) reverses the newest action row belonging to that actor and returns #43's label, or null when that person has nothing of their own to undo. A board's log is capped at 200 rows (MAX_UNDO_DEPTH), so filtering on the existing action_board_idx is cheap — say so in a comment, or add action(board_id, actor, id) if the read measures otherwise.
[ ] peekUndo(boardId, actor) is scoped the same way, so the button describes what this caller would reverse.
[ ] Rows with actor IS NULL (written before #92, or by a terminal nobody signed into) stay undoable by anyone. A comment says why: the old log has no owner, and dropping it would silently make history unreachable.
[ ] The board page's Undo button renders disabled with Nothing of yours to undo when this caller's own log is empty, even while other people's changes sit on the board — and a POST into that state still answers the way #43 settles on, never a 404 a person can trigger by pressing a live button.
[ ] Per-actor undo can skip rows, which the board-wide version could not, so a restore can now aim at a column someone else has since deleted. That case answers "that change can no longer be undone": the transaction rolls back whole, nothing is half-restored, the action row is left in the log, and the page says the sentence. A test deletes a column, then has the other person undo their older move of a card that lived in it, and asserts no 500 and no orphan row — today that path would fail the card.column_id foreign key inside the transaction and surface as an internal error.
[ ] The terminal keeps working and says which rule it follows: u in src/keys.ts undoes the signed-in operator's own last change when the forge named one, and the board's last change when it did not. Its test states the rule.
[ ] MCP is explicitly out of scope: no undo tool here (#68 owns the agent seat's reach). Note it in the pull so nobody reads the omission as an oversight.
[ ] Tests: two actors each undo their own newest change and leave the other's alone; a guest's cookie-scoped undo does not reach a signed-in person's change on the same board; an actor IS NULL row is still undoable; the stale-restore refusal above.
Files
src/db.ts (undo, peekUndo), src/routes.ts (the undo route passes the actor key), src/pages/board.ts (the button's state and label), src/keys.ts and src/tty.ts, src/db.test.ts, src/server.test.ts, src/keys/tty tests, README.md (one sentence: Undo is yours).
Risks
Scoping undo hides a colleague's mistake from you: you can no longer take back the move they fumbled. That is the intended trade — the page still shows what happened (#76) — but it is the decision to write into the README, not to leave implied.
Undo becomes reachable out of order, so every restore path has to tolerate a vanished parent row; the refusal above is the whole of that, and it is where this can fail quietly.
If the page half of this outgrows one run, landing the scoped undo/peekUndo plus the stale-restore refusal, with the button label unchanged, is a complete and useful increment.
Make Undo take back your own last change, not whatever the person beside you just did.
What it is like today.
undo(boardId)(src/db.ts:395) reads the newestactionrow for the board and reverses it, whoever wrote it. On the direction's own board — two people and an agent — the common case for pressing Undo is therefore undoing someone else's work, silently. #43 makes the button say what it would reverse; this makes it reverse the right thing, now that #92 records who made each change.Done looks like
undo(boardId, actor)reverses the newest action row belonging to that actor and returns #43's label, ornullwhen that person has nothing of their own to undo. A board's log is capped at 200 rows (MAX_UNDO_DEPTH), so filtering on the existingaction_board_idxis cheap — say so in a comment, or addaction(board_id, actor, id)if the read measures otherwise.peekUndo(boardId, actor)is scoped the same way, so the button describes what this caller would reverse.actor IS NULL(written before #92, or by a terminal nobody signed into) stay undoable by anyone. A comment says why: the old log has no owner, and dropping it would silently make history unreachable.disabledwithNothing of yours to undowhen this caller's own log is empty, even while other people's changes sit on the board — and a POST into that state still answers the way #43 settles on, never a 404 a person can trigger by pressing a live button.card.column_idforeign key inside the transaction and surface as an internal error.uinsrc/keys.tsundoes the signed-in operator's own last change when the forge named one, and the board's last change when it did not. Its test states the rule.undotool here (#68 owns the agent seat's reach). Note it in the pull so nobody reads the omission as an oversight.actor IS NULLrow is still undoable; the stale-restore refusal above.Files
src/db.ts(undo,peekUndo),src/routes.ts(the undo route passes the actor key),src/pages/board.ts(the button's state and label),src/keys.tsandsrc/tty.ts,src/db.test.ts,src/server.test.ts,src/keys/ttytests,README.md(one sentence: Undo is yours).Risks
undo/peekUndoplus the stale-restore refusal, with the button label unchanged, is a complete and useful increment.Waits for #92 (which waits for #43).