The argument happens on the proposal page, asynchronously, and stays part of the record.
What done looks like
[ ] A migration creates remark (id, proposal_id REFERENCES proposal(id) ON DELETE CASCADE, kind CHECK in comment/question/counter, body 1–2000, option_label nullable 1–80, accepted_at nullable, author_id, author_name, created_at), with an index on remark(proposal_id).
[ ] listRemarks(db, who, {proposalId}) reads for anyone, oldest first. addRemark(db, who, {proposalId, kind, body, optionLabel?}) refuses anon with signin_required, an unknown kind with invalid, a missing proposal with not_found; a withdrawn or decided proposal still takes remarks, because the record continues after the decision.
[ ] deleteRemark(db, who, {id}) is the author or an admin, the rule exported the way the others are.
[ ] A counter remark may name the option it wants instead (optionLabel). acceptCounter(db, who, {id}) — the proposal's author or an admin, open proposals only — appends that label as a new option and stamps accepted_at; accepting twice is invalid, a counter with no label is invalid, and a label duplicating an existing option is invalid. Existing ballots are untouched.
[ ] The proposal page shows the thread oldest first with the kind as a visible label, a composer with a kind selector for a signed-in person, and an Accept button on an unaccepted counter for whoever may. The thread is live on remark.added and remark.removed and does not shift the page when one arrives.
[ ] GET /api/proposals/:id/remarks, POST /api/proposals/:id/remarks, DELETE /api/remarks/:id, POST /api/remarks/:id/accept; MCP tools list_remarks and add_remark; the terminal shows the thread under an open proposal and c writes one.
[ ] Tests: a guest reads the thread and cannot write; an unknown kind is refused; 2001 characters refused; the author deletes their own and an admin deletes any; an accepted counter adds exactly one option and cannot be accepted twice; a counter on a decided proposal cannot be accepted.
[ ] README.md explains the three kinds in a sentence each, 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/proposal.ts, src/mcp.ts, src/tty.ts, src/server.test.ts, static/app.css, README.md.
Risks: a remark is the longest user text in the app and it renders at three doors — escape at the page, and let the terminal print no control character it was handed. Accepting a counter changes a proposal people may already have voted on; keep it to appending an option and say on the page that earlier ballots stand. If acceptCounter makes this run too long, land the thread and open a follow-up for it rather than half-building both.
Fit: Hearth — a thread that streams and reads the same in a terminal. A person assigns; I do not.
The argument happens on the proposal page, asynchronously, and stays part of the record.
What done looks like
remark(id, proposal_id REFERENCES proposal(id) ON DELETE CASCADE,kindCHECK incomment/question/counter, body 1–2000,option_labelnullable 1–80,accepted_atnullable, author_id, author_name, created_at), with an index onremark(proposal_id).listRemarks(db, who, {proposalId})reads for anyone, oldest first.addRemark(db, who, {proposalId, kind, body, optionLabel?})refuses anon withsignin_required, an unknown kind withinvalid, a missing proposal withnot_found; a withdrawn or decided proposal still takes remarks, because the record continues after the decision.deleteRemark(db, who, {id})is the author or an admin, the rule exported the way the others are.counterremark may name the option it wants instead (optionLabel).acceptCounter(db, who, {id})— the proposal's author or an admin,openproposals only — appends that label as a new option and stampsaccepted_at; accepting twice isinvalid, a counter with no label isinvalid, and a label duplicating an existing option isinvalid. Existing ballots are untouched.remark.addedandremark.removedand does not shift the page when one arrives.GET /api/proposals/:id/remarks,POST /api/proposals/:id/remarks,DELETE /api/remarks/:id,POST /api/remarks/:id/accept; MCP toolslist_remarksandadd_remark; the terminal shows the thread under an open proposal andcwrites one.README.mdexplains the three kinds in a sentence each, andbun testis green.Files I expect to change:
src/db.ts,src/actions.ts,src/actions.test.ts,src/routes.ts,src/pages/proposal.ts,src/mcp.ts,src/tty.ts,src/server.test.ts,static/app.css,README.md.Risks: a remark is the longest user text in the app and it renders at three doors — escape at the page, and let the terminal print no control character it was handed. Accepting a counter changes a proposal people may already have voted on; keep it to appending an option and say on the page that earlier ballots stand. If
acceptCountermakes this run too long, land the thread and open a follow-up for it rather than half-building both.Fit: Hearth — a thread that streams and reads the same in a terminal. A person assigns; I do not.