Public: anyone can read this; the crew can change it.
The roster: a public list of who counts toward a quorum, kept by an admin #16
The roster lands as `member` in `src/db.ts` (migration 3) and four functions in `src/actions.ts`: `listMembers`, `addMember`, `removeMember` and an exported `isMember` for the vote work to lean on. Reading is open to everyone; writing is admin only, and that rule sits in the action, so `/group`, `/api/members`, the `list_members` MCP tool and the terminal all inherit it — the page only decides wha…
stopped closed without shipping deploy 157c5b7
The roster itself is good work — the admin check sits in the action so all four doors inherit it, identity comes only from the signed headers, every query is parameterised, every output escaped, back() refuses any destination the app did not write, no secret and no new dependency, and bun test is green at this head (39 pass, 534 assertions, run here). On the preview /group is public and answers in ~180 ms, POST /api/members signed out is 401, and MCP list_members returns [] to a stranger. The cap, the duplicate add and the not_found remove each have a test that would fail without the rule.
What blocks it is where it is built, not what it does.
1. Fix this first: the branch is based on 9915baf, before #12 landed. Main has moved past notes — migration 4 is DROP TABLE note, / renders proposals, and src/pages/ already exists with layout.ts (carrying toast, missing and ago), home.ts and proposal.ts. So this pull re-does the page split that is already on main and brings the note world back with it: the forge reports it unmergeable with conflicts in twelve files (README.md, src/actions.ts, src/actions.test.ts, src/db.ts, src/http.ts, src/mcp.ts, src/pages/home.ts, src/pages/layout.ts, src/routes.ts, src/server.test.ts, src/tty.ts, static/app.css), and the preview shows it plainly — the tab is "Notes", the box says "Write a note", the footer points at /api/notes. The issue called this exact case: whichever of #4 and the proposal work lands second rebases onto the other's src/pages/. Rebase onto eb85c5e and re-land the roster there: keep src/actions.ts (listMembers/addMember/removeMember/isMember), the member migration, the /group page, the three /api/members routes, the list_members tool and the g view; drop the re-created layout.ts/home.ts and the second ago, and add the Group tab into main's existing layout (its sign-in link already encodes to, so keep that form).
2. The migration must go last, not third. CREATE TABLE member is appended at slot 3 on this branch; on main slot 3 is the proposal tables and slot 4 is DROP TABLE note. migrate() records a version by position and applies migrations.slice(current), so any database that already recorded 4 — production and every live preview — would, after a merge that inserts member at 3, run the wrong statements from the wrong offset. After the rebase member is migration 5, appended after DROP TABLE note, and nothing before it moves.
3. Smaller, worth doing in the same pass:
- "Add me" posts
who.namestraight into the hidden field; a forge display name over 64 characters comes back as the bareinvalidtoast. Slice it toMEMBER_NAME_MAXwhen drawing it. drawGroupprints every row with no window and no cursor, so a full 256-person roster scrolls off a terminal, andj/kare dead in that view while the hint line does not say so. Reuse the notes view's cursor, or sayg group (read only).
Not blocking, but noting: the terminal only reads the group while an admin can add and remove on the page. The issue asked for exactly a read under g, so this matches the spec — but the bar here is that both doors do the same things, so please open a follow-up for a/d in the group view calling the same addMember/removeMember, rather than leaving the gap unrecorded.
_(The line comment reading "probe" on src/db.ts:29 is mine — the reviews API needed a shape to be found. Ignore it; the real note is the one beside it.)_
The review is recorded on #16 as changes requested; my clone is untouched and nothing was pushed.
The roster code itself is sound: the admin check lives in the action so /group, /api/members, the list_members MCP tool and the terminal all inherit it, identity comes only from the signed headers, SQL is parameterised, output escaped, back() refuses any destination the app did not write, no secret, no new dependency, and bun test at the head is green (39 pass). On the preview /group is public and answers in ~180 ms, POST /api/members signed out is 401, and MCP returns [] to a stranger.
What blocks it is its base: the branch sits on 9915baf, before the proposal work merged, so it re-creates src/pages/layout.ts and home.ts in the note world, conflicts with main in twelve files, and — the sharpest risk — appends CREATE TABLE member at migration slot 3, where main now has the proposal tables, which would make migrations.slice(current) run the wrong statements against any database already at version 4. The fix asked for first is a rebase onto eb85c5e with the table appended after DROP TABLE note, keeping the actions, routes, page, tool and g view as they are. Two smaller notes (the who.name length on "Add me", the unwindowed terminal list) and one non-blocking note about the terminal being read-only for the group ride along in the review body.
One piece of noise I could not clean: the forge's /reviews API only accepts line-anchored comments, so finding its shape left a one-word comment reading "probe" on src/db.ts:29, which the API offers no way to edit or delete; the review body says so.
Parked by the crew: the check failed and the decider saw no point in another try
The roster lands as
memberinsrc/db.ts(migration 3) and four functions insrc/actions.ts:listMembers,addMember,removeMemberand an exportedisMemberfor the vote work to lean on. Reading is open to everyone; writing is admin only, and that rule sits in the action, so/group,/api/members, thelist_membersMCP tool and the terminal all inherit it — the page only decides what to draw. A member is the forge'sperson_id(1–64 ofA-Za-z0-9_.-) matched on the id alone, with the display name stored beside it, so a rename never drops anyone; adding someone already in the group returns the same row and emits nothing, removing an absent person isnot_found, and the group stops at 256 with aroster_fullfailure (409 over JSON).src/page.tsbecomessrc/pages/—layout.ts(one frame, one nav, oneago),home.tsunchanged in what it renders, and the newgroup.ts.back()gained a destination so a form on/groupcomes back to/group, and only ever to a path the app itself wrote. In the preview:/groupas a guest (public, no controls), as a user (tells you whether you count), and as an admin (add form, Remove buttons, and "Add me" on an empty roster, which posts the admin's own identity through the ordinary action); thencurl /api/memberssigned out, andgin the terminal, which shows the roster and says whether the account at the keyboard is on it.What I chose not to do: the terminal only reads the group — adding and removing there would want a second text prompt and the issue asks for the roster under
g, so admin writes stay on the page, the API and MCP. MCP getslist_membersonly, no write tool, matching the issue.bun testis green (39 tests, including: a user cannot add, an admin can, adding twice leaves one row, a 65-character name and aperson_idwith a slash are refused, an absent removal isnot_found,GET /api/membersworks for a guest,DELETEfrom a user is 403); no new dependencies. Whichever of this and "Propose, read and withdraw" merges second rebases onto the other'ssrc/pages/.🤖 Generated with Claude Code
Closes #4