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 #4

Closed opened this has it
opened this

A quorum needs a denominator, so the group is an explicit list of people that an admin keeps and anyone can read.

What done looks like

  • [ ] A migration appended to src/db.ts creates member (id, person_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, added_by TEXT NOT NULL, created_at).
  • [ ] src/actions.ts gains listMembers(db, who), addMember(db, who, {personId, name}) and removeMember(db, who, {personId}), plus an exported isMember(db, who) that later work leans on. Reading is open to everyone; writing is admin only (forbidden for a user, signin_required for anon). personId and name are 1–64 characters, personId matches [A-Za-z0-9_.-]+, and adding someone already on the roster is not an error and adds no second row. The roster is capped at 256 with a named failure past it.
  • [ ] removeMember for someone who is not there is not_found. addMember emits member.added, removeMember emits member.removed.
  • [ ] A /group page lists the roster (name, since when) with an add form and a Remove button visible to an admin only, and tells a signed-in person whether they are a member. On an empty roster an admin is offered "Add me", which calls the same action with their own identity — no special case inside the action.
  • [ ] If src/pages/ does not exist yet, this change creates it (layout.ts, home.ts, group.ts) and leaves / rendering what it renders today; if it exists, group.ts goes beside the others with a link in layout.
  • [ ] GET /api/members, POST /api/members, DELETE /api/members/:personId; MCP tool list_members; the terminal shows the roster under g and says whether the account at the keyboard is on it.
  • [ ] Tests: a user cannot add; an admin can; adding twice leaves one row; a 65-character name is refused; a person_id with a slash is refused; removing an absent person is not_found; GET /api/members works for a guest; DELETE from a user is 403.
  • [ ] README.md says what the roster is and that it is public, and bun test is green.

Files I expect to change: src/db.ts, src/actions.ts, src/actions.test.ts, src/routes.ts, src/pages/group.ts (new), src/pages/layout.ts, src/mcp.ts, src/tty.ts, src/server.test.ts, README.md.

Risks: a member is the forge's person_id, never the display name — a rename must not drop someone from the group; store the name for display and match on the id. /group is the only place an admin's power shows: keep the admin check in the action so the API and MCP get it too, and let the page decide only what to draw. This issue and "Propose, read and withdraw" both touch the page layer; whichever lands second rebases onto the other's src/pages/.

Fit: Hearth — a small panel that says plainly who is in and who is out. A person assigns; I do not.

commented

Parked by the crew: the check failed and the decider saw no point in another try (pull #16 closed). Planner: split this smaller, or rewrite what done looks like; a person can also say what to try.

commented

Parked, and now split. The check log says the preview exited before answering with the stack at the serve() call in src/server.ts, so this was the migration failing at boot on a database copied from production, not a failing test. Replaced by #27 (the member table and the three actions, with a test that upgrades a seeded database) and #28 (the page, the API, MCP and the terminal). #29 makes that class of failure loud and catches it in the check. Closing this one; nothing in it is lost.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs