Public: anyone can read this; the crew can change it.

A proposal that never reaches quorum: a deadline, and what it means when it passes #15

open not picked up yet opened this
opened this

A decision lands when the rule is met — and if it never is, the proposal sits open forever and the group learns nothing from it. Give a proposal a deadline and a clear end.

What done looks like

  • [ ] propose takes an optional closesInDays (a whole number 1–30); a migration adds closes_at to proposal, null when the proposer set none. The default is a repository setting an admin can change, or none.
  • [ ] A migration extends the state check so expired is a state alongside open, decided and withdrawn — appended as a new table-and-copy migration if SQLite will not alter the check in place, and src/db.test.ts proves an existing database migrates forward with its rows intact.
  • [ ] A proposal whose closes_at has passed without meeting its rule becomes expired: no more votes (invalid, with a sentence saying the deadline passed), while remarks continue, because the record continues. proposal.expired is emitted once.
  • [ ] Expiry happens without a cron: a sweep runs after a response, at most once a minute, and any read of a proposal past its deadline settles it before answering. Deciding always wins a race — a proposal that met its rule is decided, never expired, even if the tally and the deadline land together.
  • [ ] The page shows the deadline in words on an open proposal ("closes in 3 days", "closes tomorrow"), shows what the tally stood at when an expired one closed, and lists expired proposals with decided ones rather than with open ones. A proposal with no deadline says so.
  • [ ] Every door carries it: closesInDays in POST /api/proposals and in the MCP propose schema, the deadline in getProposal, and the terminal showing it in the list and asking for it when proposing (Enter for none).
  • [ ] Tests: a deadline in the past on a fresh proposal is refused; 0 and 31 days are refused; a vote after the deadline is refused; a proposal that meets its rule on its last second is decided, not expired; proposal.expired fires once even when two requests sweep at the same time; an expired proposal still takes remarks.
  • [ ] README.md says what a deadline does and what it does not (it never decides anything by itself). bun test is green.

Files I expect to change: src/db.ts, src/db.test.ts, src/actions.ts, src/actions.test.ts, src/quorum.ts (read only — expiry is not a rule), src/routes.ts, src/pages/home.ts, src/pages/proposal.ts, src/mcp.ts, src/tty.ts, src/server.test.ts, README.md.

Risks: time is the easiest thing to get wrong — store closes_at as the same ISO-8601 UTC string the rest of the schema uses, compare in SQL against strftime rather than in two places, and let a test inject the clock rather than sleep. Expiry must never silently pick a winner: an expired proposal has no decided_option_id. Do not let the sweep run inside a request's critical path; it belongs after the response, like the trim in issue #9.

Fit: Ledger — a state machine where every transition is a row with a reason, and a race that resolves the same way every time. A person assigns; I do not.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs