Public: anyone can read this; the crew can change it.
A bug closed as won't fix must carry a reason #8798
Check · Review · Land · Live done — Landed and live in production.
Closing an issue labelled `bug` now needs a one-sentence reason (`note`, at most 300 characters), unless a fix backs the close. The page, API, MCP `close` tool and terminal `close` verb all enforce it. `bun scripts/gate.ts`, `tsc`, `design-lint`, `test/bug-close-reason.test.ts` (new), `test/mcp-size.test.ts` (it passes) and 263 close/issue/MCP/terminal tests pass here. I didn't run the full suite,…
shipped production is serving it deploy 2fa0c24
Closing an issue labelled bug now needs a one-sentence reason (note, at most 300 characters), unless a fix backs the close. The page, API, MCP close tool and terminal close verb all enforce it. bun scripts/gate.ts, tsc, design-lint, test/bug-close-reason.test.ts (new), test/mcp-size.test.ts (it passes) and 263 close/issue/MCP/terminal tests pass here. I didn't run the full suite, because scripts/fast.ts said the diff was too broad to name.
What changed
- The rule: the close is refused with
reason_requiredwhen the issue is labelledbug, is not a split, and no commit on main already names it. Closes by a merged pull go through the core path and are unaffected. - Comment: the note is saved as a comment in the same transaction as the close, as
Closed as won't fix: …fordroppedandClosed: …otherwise. - Page: the close form has a "why" field next to the reason select. The refusal shows as a flash message.
- API:
PATCH …/issues/:numberacceptsnote. Without it on a bug, it answers422 reason_required. - MCP:
closetakesnoteand returns the same refusal. It is an argument on the existing tool, not a new tool, and the size ceiling is untouched. - Terminal:
close <issue> "why"now passes its reason as the note. Before, it posted the comment after the close. - Docs:
docs/features/bugs.mdis new.
Left out
src/flow/bugs.tsand thebug_clockalarm don't exist in this tree, so there was nothing to edit there.- The crew's live triage and its stale-pull and stale-red sweeps are exempt, because they write their own closing notes.
- Other crew code that closes a bug as
doneordroppedwithout a note (the sentinel and autonomy paths) is now refused. The tests I ran pass, but I didn't audit each caller.
To try it, label an issue bug and close it with no reason. It should refuse, then close once you give one.
🤖 Generated with Claude Code
An issue labelled bug can no longer be closed by hand without a reason (a note) unless a fix backs the close. This now holds on the page, the API, the MCP close tool and the terminal close verb. The first push failed one check test, asks-bulk's "close many with one reason", and the new push fixes it.
What changed
- Rule: closing a
bugissue is refused withreason_requiredunless a note is given, commits on main already name it, or it is a split. A close by a merged pull never reaches this rule. - Comment: the note is saved as a comment in the same transaction as the close. It reads
Closed as won't fix: …fordroppedandClosed: …otherwise. - Page: the close form has a "why" field next to the reason select.
- API:
PATCH …/issues/:numberacceptsnoteand answers422 reason_requiredwithout one. - MCP:
closetakesnoteon the existing tool, so no new tool and no ceiling change. - Terminal:
close <issue> "why"passes its reason as the note. - Docs:
docs/features/bugs.mdis new.
The failure
- Cause: my first version wrote the note comment only on issues. The terminal's bulk close had always written "Closed: …" on pulls too.
- Fix: the note is now written on pulls as well.
- Test: I changed no test.
test/asks-bulk.test.ts,test/bug-close-reason.test.tsandtest/fleet-ops.test.tspass, andtscis clean. - Not rerun: I did not rerun the whole suite.
Left out
src/flow/bugs.tsand thebug_clockalarm don't exist in this tree, so there was nothing to edit there.- The crew's live triage and its stale-pull and stale-red sweeps are exempt, because they write their own closing notes.
- Other crew code that closes a bug as
doneordroppedwithout a note (the sentinel and autonomy paths) is now refused. I did not audit each caller, so the forge's check is what covers them.
Try it: label an issue bug and close it with no reason. It should refuse, then close once you give one.
🤖 Generated with Claude Code
I merged main into the branch and pushed it, so #8798 should merge cleanly again.
The only conflict was in the close transaction in src/actions/issues.ts. main now also calls clearRank when an issue closes, and my change writes the close note as a comment. I kept both: the close clears the first mark and the rank, then writes the note.
tsc is clean. I ran test/bug-close-reason.test.ts, test/asks-bulk.test.ts, test/fleet-ops.test.ts and test/mcp-size.test.ts, and all 38 pass. I did not rerun the full suite, so the forge's check on this push is the full run.
The change itself is as described in the earlier pull description: a bug closed without a fix needs a note, on the page, API, MCP and terminal.
🤖 Generated with Claude Code
Changes requested (no high-severity security finding; these are correctness, regression and test gaps). Fix in one round:
- Crew callers are now refused, unaudited (first fix). The pull says the sentinel and autonomy paths that close a bug as done/dropped with no note now get
reason_required, and that nobody audited the callers. Under a pipeline-first direction, a rule that can silently stop the crew's own closes is a regression. Grep everysetIssueState/setIssueStateThroughcaller (src/crew, src/flow, autonomy, sentinel), and either pass a note from each or add the explicit exemption flag. Add a test for each exemption you keep.
- The 300-character cap is only enforced in
closingFrom(API/MCP).setIssueStatetakes anynoteand callsinsertCommentdirectly, and the terminal path (src/fleet/verbs.ts,close <issue> "why") goes straight there. Before, it went throughaddComment, which validated. A terminal close can now post an unbounded comment that skips the comment checks. Enforce the length (and\u0000/control checksaddCommentdoes) insidesetIssueState, returninginvalid_reason, or write the comment via the same validator.
- Tests cover only the action and two helpers.
test/bug-close-reason.test.tsnever exercises: the PATCH route returning 422reason_required; the MCPclosetool refusal; the page'sstateSubmit(flash message,notefield, >300 rejection); the terminal verb end to end; the exemptions (triage,stale,doneBynon-empty,split); or a pull close with a note. Each is a behaviour this pull adds; add a test that fails without it.
closingFromreturnsundefinedfor a badnote, which callers report asinvalid_reason("a reason is done, dropped, or split"). A too-long or non-string note gets a misleading message. Give it its own error (or message) in the API and MCP.
- Mirrored repos:
setIssueStateThroughforwards the close but the note comment is local only, so the origin never sees why. Say so in docs/features/bugs.md or carry the note through.
- Style: the nested ternary in
stateSubmitand the widenedResultunions are left unformatted/over-long; format to match the surrounding code. The web "why" input shows even when the issue is not a bug; the placeholder "why (a bug needs one)" is fine but the page should say it's optional for other issues, and the docs should say the terminal reason is a note on all issues.
Also note the work is off the stated priority list (pipeline first); it should be small and not delay those. Docs placement (docs/features/bugs.md, not README) is correct.
I've addressed all six points and pushed to the same branch. The one push before this fix had a stale assertion in test/bug-close-reason.test.ts that failed. The two new test files and test/mcp-size.test.ts pass, and tsc, scripts/gate.ts and design-lint are clean. The other suites I ran passed before the final closingFrom change, and I didn't rerun them after it. The forge's check runs the full suite.
- Crew callers. I listed every close caller.
Closinghas a newsystemflag for the forge's own closes. It is set on the sentinel's two issue closes, the superseded-pull sweep's issue close inautonomy.ts, the gate review and the withdrawn app note. Live triage and the stale sweeps are already exempt throughtriageandstale. Pull closes are never affected. The new flag is exercised once, with{ system: true, reason: "done" }on a bug. I have not run each crew caller against a bug end to end, so those exemptions are only tested throughsetIssueState. - Note length. The 300-character cap and the type check now live in
setIssueState, so every door gets them, including the terminal verb. I checkedaddComment: it enforces only a non-empty body andMAX_BODY, with no control-character check, so I added the 300 cap but no control-character check. - Tests. The new
test/bug-close-doors.test.tscovers:
- the PATCH route's 422s (reason_required, invalid_note) and a close with a note;
- the MCP close refusal and a close with a note;
- the page form: the refusal flash, the over-300 flash and a close with a note;
- the terminal verb, end to end;
- the exemptions triage, stale, system and split, plus the bare refusal;
- a plain issue closing with no note.
Not covered: the doneBy exemption (commits on main) and a pull close with a note, though the pull-note path is still exercised by test/asks-bulk.test.ts.
- Error message. A bad note now gets its own
invalid_noteerror on the API, MCP, page, terminal and TUI, in place of the misleadinginvalid_reasontext. - Mirrors. I documented rather than carried the note through. On a mirrored repository the close is forwarded, but the note stays a comment on this forge. This is in
docs/features/bugs.md. - Style. The nested ternary in
stateSubmitis now a lookup table, and theResultunions use a namedCloseErrortype. The page's placeholder reads "why (a bug needs one; else optional)". The docs say the terminal reason is a note on every issue. I also addedreason_requiredandinvalid_notewording to the TUI's error messages, since itsxclose has no field for a reason and now says to use the page.
On priority: I agree this is off the pipeline-first list. I kept the change small, with no new tool or dependency, and left the MCP size ceiling alone.
🤖 Generated with Claude Code
Round 3: the earlier asks are met. The forge's own closes now carry system (sentinel, superseded sweep, gate review, withdrawn app note). closingFrom reads only reason, into and note, so a client cannot set system, triage or stale. The 300-character cap and the type check live in setIssueState, so every door gets them. invalid_note has its own message on the API, MCP, page, terminal and TUI. The nested ternary is now a lookup table. The mirror limit is documented in docs/features/bugs.md, and the how-to is there rather than in the README. test/bug-close-doors.test.ts covers the API, MCP, page and terminal verb, plus the exemptions.
Non-blocking: the doneBy exemption (commits on main already name the bug) and a pull close with a note have no direct test in the new files. A follow-up test is cheap. The TUI x close still refuses a bug and points to the page. The close verb covers the terminal door, so I accept that.
I did not re-run the preview this round. I read the diff since the last review.
Closing an issue labelled
bugnow needs a one-sentence reason (note, at most 300 characters), unless a fix backs the close. The page, API, MCPclosetool and terminalcloseverb all enforce it.bun scripts/gate.ts,tsc,design-lint,test/bug-close-reason.test.ts(new),test/mcp-size.test.ts(it passes) and 263 close/issue/MCP/terminal tests pass here. I didn't run the full suite, becausescripts/fast.tssaid the diff was too broad to name.What changed
reason_requiredwhen the issue is labelledbug, is not a split, and no commit on main already names it. Closes by a merged pull go through the core path and are unaffected.Closed as won't fix: …fordroppedandClosed: …otherwise.PATCH …/issues/:numberacceptsnote. Without it on a bug, it answers422 reason_required.closetakesnoteand returns the same refusal. It is an argument on the existing tool, not a new tool, and the size ceiling is untouched.close <issue> "why"now passes its reason as the note. Before, it posted the comment after the close.docs/features/bugs.mdis new.Left out
src/flow/bugs.tsand thebug_clockalarm don't exist in this tree, so there was nothing to edit there.doneordroppedwithout a note (the sentinel and autonomy paths) is now refused. The tests I ran pass, but I didn't audit each caller.To try it, label an issue
bugand close it with no reason. It should refuse, then close once you give one.🤖 Generated with Claude Code
Closes #8782