Uh oh!
There was an error while loading. Please reload this page.
docs(standards): (scope) is the component, never the ticket number - #354
Conversation
Four PRs in one day failed `closing-ref` for the same reason -- .github#349, #350, #352, #353 -- and all four were fixed by retitling. That is a rule the canon is missing, not four mistakes: a finding that recurs across PRs becomes a rule rather than a fifth hand-fix. THE COLLISION. The canon says commit subjects are `type(scope): summary`, `referencing the ticket (backend#1234)`. Read quickly, `(scope)` looks like the ticket slot, so sessions write `sec(2157): ...`. `closing-ref` then reads a bare number in the title as a PROMISE to close that ticket and fails unless the body carries a closing keyword -- which for partial work would be WRONG to add. Every one of the four was partial work correctly saying `Part of tracebloc/backend#N`, and every one went red for saying so. The canon's own example is already the qualified form (`backend#1234`), not a bare number; the gap is that it never says the number does not belong in the subject, and that a bare one has a specific machine meaning. Both are now stated. WHY NOT LOOSEN THE GATE INSTEAD. The gate is right and its failure message is excellent -- a title reference is inert, and it cannot tell which repo owns a bare number, so it would accept a link to any repo at that number. The defect is the convention pointing one way while the check reads the other. Verified: `classify` reports IN_SYNC (canon and CLAUDE.md agree), standards-sync selftest 31 checks / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aptracebloc
left a comment
There was a problem hiding this comment.
Reviewed — accurate and useful. Clarifies that (scope) in type(scope): summary is the component (mint-scope, kanban), never the ticket number, because a bare number in the title (sec(2157): …) is read by closing-ref as a promise to close that ticket and fails the check unless the body carries Closes <owner>/<repo>#N. That matches the actual closing-ref behavior, and the guidance (number in the subject only when the PR truly closes it; otherwise Part of tracebloc/backend#N in the body) is correct. Same sentence added identically to CLAUDE.md and org-standards.md. Docs-only, green.
Approving.
— drafted with Claude Code
saadqbal
left a comment
There was a problem hiding this comment.
Good rule, and the right two lines to spend on it — sec(2157): as the worked example is exactly the shape that bites, because a bare number resolves against this repo and the ticket lives in backend.
One refinement for whenever this text is next touched, non-blocking: "the check fails unless the body carries Closes <owner>/<repo>#N" describes the fix correctly but the mechanism slightly loosely. The gate reads GitHub's computed closingIssuesReferences, not body text — so a body that carries the keyword and the ref but not adjacently (Closes the interim half of tracebloc/backend#2327) satisfies the sentence as a reader might parse it and still computes zero links. Following your line literally produces the right result; it's the loose reading that fails. Worth "immediately followed by" if you're ever back in here. 👍
Uh oh!
There was an error while loading. Please reload this page.
…356) * fix(2284): the gate tolerates a review that never came, and says so Two absences were one verdict, and they mean opposite things. `PENDING` covered both "Bugbot is still running" and "Bugbot never showed up", and the timeout failed both identically. A check that STARTED and never finished is a review that BROKE -- worth blocking on. A check that never appeared is Bugbot dropping the PR, which this repo cannot fix, retry, or wait out. Measured 2026-08-26 on human-authored PRs, well past p50 164s / max 635s: six of nine never got a check at all -- .github#349 (57 min), #350 (55), #352 (40), #353 (37), #354 (32), e2e-test-agent#273 (2h+) -- while #351, opened BETWEEN two of them, was reviewed in three minutes. Not latency, not the seat limit, not the author. backend#2114 closed COMPLETED saying "no discriminator survives the data"; the drop is still live. `bugbot run` cannot recover it: Cursor refuses on a seat limit and the App will not be given one (decision, 2026-08-26). So requiring this context while failing UNCLAIMED would block roughly two thirds of PRs for the full wait and then fail them with no remedy -- the gate would look broken while behaving exactly as written. WHAT THIS IS NOT: it is not a pass. UNCLAIMED exits 0 so the context can be required, and every other word says the head is UNREVIEWED -- its own banner, its own summary line, and an explicit "read the diff yourself". The honest report is "nothing looked at this", not "this is clean". PENDING still blocks, and that asymmetry is the whole change. Also corrected: the vocabulary comment said "Only PENDING is waitable", which this makes false. `main` now READS `WAITABLE` rather than restating it. 79 selftest assertions (4 new through `main`, since the exit code is the behaviour and `evaluate` alone cannot show it). Registry: 1 stale marker fixed, 5 mutations added -- the collapse in both directions, the tolerance leaking onto PENDING, the tolerance removed, and the banner reading as a pass. Part of tracebloc/backend#2284 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(2284): pin the UNREVIEWED banner, which no assertion covered The mutation `the UNREVIEWED banner reads as a pass` came back UNCAUGHT. Every other assertion in this suite checks a verdict or an exit code, and the banner is neither -- it is the one line a human skimming the summary actually reads, and the only thing standing between "exits 0" and "looks clean". Three cases: the UNCLAIMED headline says UNREVIEWED, does not contain the word pass, and a genuine pass still does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: hoist the selftest imports ruff flagged E402/E401 from the banner test added in the previous commit. `make lint` is where I should have run it before pushing, not after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(2284): the threshold is applied before the head is classified Bugbot on #356, High: `evaluate` returned UNCLAIMED as soon as the head had no Bugbot check, without applying the open-finding threshold to the threads it had ALREADY loaded one block earlier. So the tolerance this PR adds for a dropped review would launder a finding that had already come -- review head A, get a High, push head B, Bugbot never claims B, and the gate reports UNREVIEWED-but-not-blocked over an open High at exit 0. The same hole was in PENDING, and fixing only the branch Bugbot named would have left its twin. So `blocking` is now computed before `bugbot_check` is consulted at all, and an absence of either kind with a blocking finding is a FAIL that names the finding and does NOT wait -- the answer would not change. Both absence verdicts now also state that the threshold was checked, so the report says which question was answered. The tolerance itself is intact and asserted: a Low on an unreviewed head is still UNCLAIMED, exit 0, and so is a resolved High -- otherwise the remedy the FAIL message names would not clear it. Not a shipped bug: `required_conversation_resolution` is true on every train branch, so the merge was blocked anyway. It was a gate naming the wrong reason, which is the failure mode this file exists to prevent. One renderer (`_finding_lines`) feeds both paths, because two would drift and one of them would quietly stop saying OPEN (rule 9). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…e truthful (backend#2556) (#360) * fix(closing-ref): re-run when the title changes, and let a child PR be truthful (backend#2556, backend#2616) Two defects in one gate, fixed together because either alone leaves the other's remedy unusable. BACKEND#2556 -- THE GATE READS THE TITLE AND NEVER RE-RUNS WHEN IT CHANGES. `closing-ref` decides its verdict from the PR title and body; the callers triggered on `[opened, reopened, ready_for_review, converted_to_draft]`, and `edited` is the only event GitHub fires when either field changes. So the two inputs the gate reads were the two inputs that could change without re-running it. That is a bypass, not a gap: open a PR titled `chore: tidy up`, the gate records NOTHING_NAMED and goes green, then retitle it to `fix(1234): ...` with nothing linked -- no event fires, the green stands. It also made the remediation unusable in the other direction: 20 sync PRs had to be cleared with 20 manual `gh run rerun` calls (backend#2555). Same one-word fix, same reason, as `fr-gate-caller.yml` (backend#1945). A CONSEQUENCE THAT HAD TO BE GUARDED, not discovered later. Every trigger this workflow had was reachable only on an OPEN PR, so `set-status` writing Status unconditionally was always correct. `edited` is not: GitHub fires it when a MERGED PR's title or body is edited, and this is the one board writer in the fleet that does not compare pipeline rank. Without the new guard, fixing a typo in a shipped PR's description would drag its card from `Prod` back to `Code review`. Both jobs are now `if: ...state == 'open'`. BACKEND#2616 -- ONE SATISFYING FORM, AND A CHILD PR CANNOT TRUTHFULLY USE IT. The checker read `closingIssuesReferences` and nothing else, and that field is populated only by a closing keyword. So a PR whose title named a ticket had exactly one satisfying form: promise to close it. For this org's most common shape -- a parent ticket with several child PRs titled `type(NNNN): ...`, none of which finishes it -- the only remedies were a FALSE `Closes`, or deleting the number from the title, which passes the check by removing the traceability the check exists to enforce. Four PRs in one day took the second (.github#349, #350, #352, #353) and #354 wrote that workaround into the canon. A declared NON-CLOSING body reference now satisfies the title, reported as its own state (`MENTIONED`) so a green run still says which promise was made. The closing link stays the stronger form, and `WRONG_REPO` is decided BEFORE any body reference is consulted -- a truthful `Part of tracebloc/backend#304` must not mask a `Closes#304` that closes `.github#304` on merge. DERIVED, NOT RESTATED (rule 1). The non-closing vocabulary is PARSED out of `org-standards.md`, which already declares the org's partial-work form. Add `Refs <owner>/<repo>#N` to the canon and the gate accepts it with no code change. The derivation fails closed in both directions: an unreadable canon and a canon declaring no non-closing form are separate cannot-tells (exit 2), never a silent reversion to closing-only -- reverting is the defect itself. GitHub's closing keywords are the one list here that cannot be derived from this org, so they are named, cited, and used only to subtract. The canon and CLAUDE.md carried two statements this change makes false -- that the check fails unless the body carries `Closes`, and to keep the number out of the subject. Both are rewritten; classify() reports IN_SYNC. EVIDENCE closing-ref-gate-selftest 156 assertions, all passed (was 109) closing-ref-gate-mutations 53 mutations, 0 stale, 0 uncaught (was 36) ruff / actionlint / shellcheck / house-rules clean standards-sync selftest 60 checks, 0 failed The mutation harness now rewrites the two WORKFLOW files as well as the checker: `edited` and the open-state guards are guarantees declared in YAML, and rule 5 does not exempt a guard for living in a different language. Part of tracebloc/backend#2556 Part of tracebloc/backend#2616 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(closing-ref): an escaped Unreadable reddens a case instead of killing the suite The multi-word-keyword mutation was scored UNCAUGHT for the right reason: it made `evaluate` raise, which killed the suite mid-run, and the harness refuses to count a broken harness as coverage. `evaluate` grew a path that can raise for a NEW reason (the derivation from org-standards.md coming back empty), so the positive cases now go through a wrapper, exactly as `value` already does for every other call. Part of tracebloc/backend#2616 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(closing-ref): the derived-vocabulary cases report a refusal instead of crashing `value` returns the exception TEXT on a refusal, and a string is iterable -- so a case that walked the derived list iterated characters and raised IndexError on the space in "RAISED Unreadable: ...". Normalised once. Found by the multi-word-keyword mutation, which is the job: it was scored UNCAUGHT for breaking the harness rather than being caught by a case. Part of tracebloc/backend#2616 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Four PRs failed
closing-reftoday for the same reason — .github#349, #350, #352, #353 — and all four were fixed by retitling. That's a missing rule, not four mistakes.The collision
The canon says commit subjects are
type(scope): summary, "referencing the ticket (backend#1234)". Read quickly,(scope)looks like the ticket slot — so sessions writesec(2157): ….closing-refthen reads a bare number in the title as a promise to close that ticket, and fails unless the body carriesCloses <owner>/<repo>#N. For partial work that keyword would be actively wrong. All four PRs were partial work, all four correctly saidPart of tracebloc/backend#N, and all four went red for saying so.The canon's own example is already the qualified form (
backend#1234), not a bare number. What it never says is that the number doesn't belong in the subject, and that a bare one carries a specific machine meaning.Why not loosen the gate
The gate is right, and its failure message is one of the better ones in the fleet — a title reference is inert, and it can't tell which repo owns a bare number, so it would happily accept a link to any repo at that number. The defect is the convention pointing one way while the check reads the other. Fixed on the convention side.
Verification
classifyreportsIN_SYNC— canon andCLAUDE.mdagreeFollows the same route as .github#350: the canon changes here, and the 19
CLAUDE.mdcopies pick it up on the nextstandards-syncdispatch withcreate-prs: true.Note
Low Risk
Prose-only updates to engineering standards with no runtime, auth, or data-path changes.
Overview
Updates the org-wide Branches & PRs bullet on naming and commits in
org-standards.mdand the synced block inCLAUDE.md.The change spells out that
(scope)intype(scope): summaryis the component (e.g.mint-scope,kanban), not the issue number, and that a bare number in a PR title (e.g.sec(2157): …) is treated by theclosing-refcheck as a promise to close that ticket unless the body hasCloses <owner>/<repo>#N. For partial work, the canon now says to usePart of tracebloc/backend#Nin the body and keep the ticket number out of the subject.This is documentation-only alignment with existing gate behavior (not a change to
closing-refitself); downstreamCLAUDE.mdcopies pick it up viastandards-sync.Reviewed by Cursor Bugbot for commit 1a279df. Bugbot is set up for automated code reviews on this repo. Configure here.