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

A bug closed as won't fix must carry a reason #8798

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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_required when the issue is labelled bug, 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: … for dropped and Closed: … otherwise.
  • Page: the close form has a "why" field next to the reason select. The refusal shows as a flash message.
  • API: PATCH …/issues/:number accepts note. Without it on a bug, it answers 422 reason_required.
  • MCP: close takes note and 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.md is new.

Left out

  • src/flow/bugs.ts and the bug_clock alarm 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 done or dropped without 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

Closes #8782

commented

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_required when the issue is labelled bug, 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: … for dropped and Closed: … otherwise.
  • Page: the close form has a "why" field next to the reason select. The refusal shows as a flash message.
  • API: PATCH …/issues/:number accepts note. Without it on a bug, it answers 422 reason_required.
  • MCP: close takes note and 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.md is new.

Left out

  • src/flow/bugs.ts and the bug_clock alarm 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 done or dropped without 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

commented

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 bug issue is refused with reason_required unless 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: … for dropped and Closed: … otherwise.
  • Page: the close form has a "why" field next to the reason select.
  • API: PATCH …/issues/:number accepts note and answers 422 reason_required without one.
  • MCP: close takes note on 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.md is 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.ts and test/fleet-ops.test.ts pass, and tsc is clean.
  • Not rerun: I did not rerun the whole suite.

Left out

  • src/flow/bugs.ts and the bug_clock alarm 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 done or dropped without 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

commented

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 — before the latest push

Changes requested (no high-severity security finding; these are correctness, regression and test gaps). Fix in one round:

  1. 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 every setIssueState/setIssueStateThrough caller (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.
  1. The 300-character cap is only enforced in closingFrom (API/MCP). setIssueState takes any note and calls insertComment directly, and the terminal path (src/fleet/verbs.ts, close <issue> "why") goes straight there. Before, it went through addComment, which validated. A terminal close can now post an unbounded comment that skips the comment checks. Enforce the length (and \u0000/control checks addComment does) inside setIssueState, returning invalid_reason, or write the comment via the same validator.
  1. Tests cover only the action and two helpers. test/bug-close-reason.test.ts never exercises: the PATCH route returning 422 reason_required; the MCP close tool refusal; the page's stateSubmit (flash message, note field, >300 rejection); the terminal verb end to end; the exemptions (triage, stale, doneBy non-empty, split); or a pull close with a note. Each is a behaviour this pull adds; add a test that fails without it.
  1. closingFrom returns undefined for a bad note, which callers report as invalid_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.
  1. Mirrored repos: setIssueStateThrough forwards 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.
  1. Style: the nested ternary in stateSubmit and the widened Result unions 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.

commented

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.

  1. Crew callers. I listed every close caller. Closing has a new system flag for the forge's own closes. It is set on the sentinel's two issue closes, the superseded-pull sweep's issue close in autonomy.ts, the gate review and the withdrawn app note. Live triage and the stale sweeps are already exempt through triage and stale. 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 through setIssueState.
  2. Note length. The 300-character cap and the type check now live in setIssueState, so every door gets them, including the terminal verb. I checked addComment: it enforces only a non-empty body and MAX_BODY, with no control-character check, so I added the 300 cap but no control-character check.
  3. Tests. The new test/bug-close-doors.test.ts covers:

- 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.

  1. Error message. A bad note now gets its own invalid_note error on the API, MCP, page, terminal and TUI, in place of the misleading invalid_reason text.
  2. 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.
  3. Style. The nested ternary in stateSubmit is now a lookup table, and the Result unions use a named CloseError type. 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 added reason_required and invalid_note wording to the TUI's error messages, since its x close 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

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs