Twinned from https://github.com/cfsh/ninefold/issues/811 by tools/beads/import_github.py's reconcile pass.
Prescription, not a design question — the mechanism is decided below. Filed by the CTO off design 006 §5.6 (S12), which is background only; nothing here requires reading that doc.
The session board is issue #398's body — a markdown table, one row per live session, edited by having each session gh issue edit --body the whole thing back. gh issue edit --body is a whole-body replace, so two sessions editing within the same window silently clobber each other: whichever writes second wins, and the first session's row (or anyone else's) reverts to whatever it was before its own edit — no error, no merge, no signal.
Every session that has cared about this has hand-rolled the same fix independently: fetch the body, locate its own row, splice in the new content, re-fetch the body immediately before writing and abort if it changed since the first read. nf-dev-gh-upgrade named the consequence directly: *"every session must independently reinvent that, and any session that doesn't silently eats a peer's row."* This CTO session has done exactly that hand-rolled dance by hand something like eight times in the last day of orientation.
The fix is one verb that does the compare-and-swap once, correctly, so nobody reinvents it again.
tools/office/board.py, following this tier's conventions (tools/pr/*.py layout, unittest under tools/tests/, fakes over gh/git per design 001 — do not shell out to gh issue edit directly; extend nflib.gh.Gh with whatever body-read/body-write method it's still missing, the same way the rest of the tier talks to GitHub).
board.py --set-row <session> --row-file <path> [--board 398]Inserts or replaces the row for <session> in the Sessions table of the board issue (#398 by default; --board overrides for tests/other repos, mirroring audit_sessions.py --board).
<session> is the bare session name (e.g. nf-dev-merge-reset-guard) — used to find the row by matching its first cell, | **<session>** |, exact match on the bolded name, not a substring (so front-door-wave0 must not match a hypothetical front-door-wave0-extra).--row-file <path> contains the complete replacement markdown row, starting with | **<session>** | and ending in | — the caller builds the full row (sid, role, clone, chunk, issues, status columns) exactly as it wants it to read; this tool's job is placement and atomicity, not formatting.<session> already exists, replace it in place (same position in the table).board.py --delete-row <session> [--board 398]Same compare-and-swap mechanics, removes the row for <session> entirely rather than replacing it. Refuses (does nothing, clear message) if no row for that session exists. This is what a session's own wrap-up should call — per the board's own stated convention, *"a wrapped session's row is deleted, not greyed out."*
tools/tests/test_board.pyAgainst the existing gh fake (design 001), at minimum:
board.py instead of editing by hand — that's a followup once this exists and is trusted; this issue is just building the verb.None.
No comments.