Skip to content

feat(groom): bail_sink — suppress builder bail issues, and document what max_findings actually caps (BE-6157) - #109

Open
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-6157-groom-bail-sink
Open

feat(groom): bail_sink — suppress builder bail issues, and document what max_findings actually caps (BE-6157)#109
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-6157-groom-bail-sink

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

ELI-5

Groom has a knob called max_findings that says it caps "the number of NEW issues opened in one run". Someone set it to 0 on a repo, expecting no issues — and got one anyway. The knob wasn't broken: it caps the findings the file job opens, while the auto-builder files a different kind of issue from a different job when a patch is too big to open as a PR. The setting did what it documented; it did not do what it implied. This PR says so out loud in the docs, and adds bail_sink — the knob that actually silences those builder bail issues, settable live from GROOM_CONFIG with no PR.

What changed

1. Documentation (the trap itself). max_findings' description in groom.yml now names its scope (the file job's findings issues, after dedup) and explicitly disclaims builder bail issues, pointing at bail_sink. Mirrored in .github/groom/config.py's _OPERATIONAL_KEYS comment, .github/groom/README.md (new "Builder bail-outs" section) and the root README catalog row.

2. New bail_sink input, default issue — today's behavior, unchanged for every current caller.

  • issue (default) — a bail files a groom-labeled issue, exactly as now.
  • none — files nothing. Instead build_pr emits ::warning::bail_sink=none — NOT filing a bail issue for '<title>' (idx=N, signature=<sig>): <reason> and appends a run-summary line, so a suppressed bail is visible in the run, not invisible.
  • linear is reserved for the sibling sink: linear ticket and is not implemented here. It is refused by name, with its own warning, and falls back to issue.

3. Placement: _OPERATIONAL_KEYS, not _LOCKED_KEYS — per the ticket. bail_sink can only make groom quieter, never grant it privilege, so a repo can set it via vars.GROOM_CONFIG without a PR. That is the whole point: the operator who set max_findings: 0 can get real silence the same way. pr_size_limit stays locked (a reviewed commit in the caller) — the config.py docstring now spells out that suppressing the bail is not a reason to unlock raising the ceiling (ticket item 3).

4. Single allowlist. config.py owns BAIL_SINKS and exports normalize_bail_sink, which build_pr's inline Python imports rather than re-deriving the list. Everything not provably none — empty, unknown, linear, a dropped-by-coercion value — resolves to issue. The fail-safe direction is deliberate: a redundant issue is recoverable, a silently discarded CONFIRMED finding is not.

Verification

  • python3 -m unittest discover -s .github/groom/tests -p 'test_*.py'183 passed (was 170; +13 for bail_sink). Also ran the cursor-review and agents-md suites, shellcheck on bump-callers, and check_agents_md.py --root . — all green (the CODEOWNERS warning is pre-existing).
  • actionlint clean on groom.yml and test-groom-scripts.yml; the workflow parses and all six inline Python blocks compile.
  • The suppress path was exercised, not just asserted. build_pr's bail branch is inline Python inside a YAML run: block, so it cannot be imported by the unit suite. I extracted it into a scratch harness with a fake decision/result and a stub gh on PATH, and ran it four ways: BAIL_SINK=none → warning + summary line, no gh invocation, exit 0; unset → filed the issue exactly as before, no summary line; linear → filed (fail-safe); NONE → suppressed (case-insensitive). A model-authored title containing a newline and a literal ::error:: was flattened onto one line, as intended.
  • Unit tests cover the allowlist coercion, normalize_bail_sink's fail-safe defaulting, and — via a new TestBailSinkWiring class that reads groom.yml — the four wiring points a silent regression would live in (the input + its issue default, the defaults-layer entry, the BAIL_SINK: env read, the if bail_sink == "none": branch and its warning). test-groom-scripts.yml's path filter now also watches .github/workflows/groom.yml so a workflow-only edit that unhooks one of those actually runs the suite.

Judgment calls / corrections worth reading

  • The ticket says a suppressed bail leaves "the patch artifact still uploaded, so it is recoverable from the run." That is not quite true today, and I did not change it. The bail() helper in the build job truncates /tmp/out/patch.diff before upload — deliberately, because the same helper carries the secret-scan withhold, where publishing the patch is the thing being prevented. Preserving the patch on the size/CI-privileged bails only would change artifact contents for current callers, against the "byte-identical" criterion. So the recovery path for bail_sink: none is the ::warning:: (title + bail reason + dedup signature) and the run-summary line, not the patch bytes. The docs I wrote say exactly that; they do not claim the patch is recoverable.
  • none suppresses every bail, including the prompt-injection secret-scan withhold. I kept it uniform rather than carving out an exception: a carve-out needs a machine-readable bail kind in result.json, which is scope the ticket did not ask for, and the secret-scan path already prints its own ::error:: on the run page independently. Called out in the input description and both READMEs.
  • none means the finding is not recorded in the ledger, so a later run can re-propose and re-bail on it, re-spending builder tokens. Unavoidable without filing something — a durable record is what the linear sink phase is for. Documented on the input so nobody discovers it from a bill.
  • from config import normalize_bail_sink couples build_pr to the workflows_ref assets. A caller that pins uses: to this SHA but workflows_ref: to an older one gets an ImportError. This is a pre-existing coupling, not a new one — the same job already imports builder_pr_body from ledger.py at that path — the bumper moves both pins together, and the import runs before any git push, so it cannot orphan a branch.
  • Small drive-by: the two existing GITHUB_STEP_SUMMARY writes in build_pr were folded into the summarize() helper the new path needed. Behavior-identical (verified in the harness above); it avoids a third copy of the same four lines.

…hat max_findings actually caps (BE-6157)

max_findings is documented as a cap on "NEW issues opened in one run", but it
only slices the file job's findings list. Builder BAIL issues — a CONFIRMED
finding whose patch exceeded pr_size_limit or touched a CI-privileged path — are
opened by build_pr and were never capped, so an operator who set
max_findings: 0 expecting silence still got an issue.

- Amend the max_findings description (plus config.py and both READMEs) to name
  its scope and disclaim the bail path.
- Add a bail_sink input, default `issue` (byte-identical behavior). `none`
  files nothing and instead emits a ::warning:: naming the finding, its bail
  reason and its signature, plus a run-summary line, so a suppressed bail is
  visible in the run rather than invisible.
- bail_sink is an _OPERATIONAL_KEY, not a locked one: it can only make groom
  quieter, so GROOM_CONFIG can set it with no PR. pr_size_limit stays locked.
- config.py owns the allowlist; build_pr imports normalize_bail_sink so the two
  cannot disagree, and any unrecognized value (including the reserved `linear`)
  resolves back to `issue` — never to silent suppression.
@mattmillerai mattmillerai added cursor-review Multi-model cursor review agent-coded Authored by the agent-work loop labels Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 638ff784-f368-4896-8ae4-6f920f39dfb1

📥 Commits

Reviewing files that changed from the base of the PR and between bcde90f and d44030f.

📒 Files selected for processing (6)
  • .github/groom/README.md
  • .github/groom/config.py
  • .github/groom/tests/test_config.py
  • .github/workflows/groom.yml
  • .github/workflows/test-groom-scripts.yml
  • README.md

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 7 finding(s).

Severity Count
🟠 High 1
🟡 Medium 2
🟢 Low 3
⚪ Nit 1

Panel: 8/8 reviewers contributed findings.

Comment thread .github/workflows/groom.yml Outdated
Comment thread .github/workflows/groom.yml Outdated
Comment thread .github/workflows/groom.yml Outdated
Comment thread .github/workflows/groom.yml Outdated
Comment thread .github/workflows/groom.yml
Comment thread .github/workflows/groom.yml Outdated
Comment thread .github/groom/config.py
…old from bail_sink=none (BE-6157)

Review-panel follow-ups on the bail_sink knob:

- The dedup `signature` is as model-authored as `title`/`reason` (the verifier
  schema does not require it, and `normalize_signature` only strips SURROUNDING
  whitespace), so an interior newline in it ended the `::warning::` and let the
  remainder pose as a fresh workflow command in a job holding a write-scoped bot
  token. It now goes through `oneline` in both the annotation and the run
  summary, as do the remaining raw `title`/`reason` emissions on the dry-run and
  built-PR paths — the same class, one sink away.

- `bail_sink: none` no longer suppresses the pre-publish secret-scan withhold.
  `bail_sink` is classified OPERATIONAL (settable from vars.GROOM_CONFIG with no
  PR) on the grounds that it can only make groom quieter, never grant it
  privilege — and erasing the durable record of a possible key-exfil attempt,
  leaving only an `::error::` in expiring run logs, is where "quieter" stopped
  being merely quieter. The carve-out rides on a `"withheld": true` machine field
  in result.json, not a substring match on the prose reason, so rewording the
  bail message cannot silently disarm it.

- The `if not sig:` producer-error guard moves back ahead of the sink branch: a
  signatureless finding is a schema failure, not an operator's suppression.

- Docs: `none` starves the build queue, not just tokens — a DETERMINISTIC bail
  re-bails every run and permanently holds a `max_prs` slot (at `max_prs: 1`
  nothing else is ever built). And an unrecognized value falls back to the
  CALLER's value, not unconditionally to `issue`.

Tests pin all three new invariants (withhold exemption end to end, guard
ordering, signature sanitization).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants