Skip to content

fix(groom): a failed finder job counts as a spent audit only if the agent step ran (BE-4809) - #84

Open
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-4809-groom-interval-agent-step
Open

fix(groom): a failed finder job counts as a spent audit only if the agent step ran (BE-4809)#84
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-4809-groom-interval-agent-step

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

ELI-5

Groom is billed work, so the cadence gate asks "did a real groom already run recently?" by looking at whether the finder job ran. It counted a failed finder job as "yes, we spent the money" — sensible when the agent ran and the job died later while filing issues. But the finder does a bunch of cheap setup first (two checkouts, npm install, building the prompt, chmod'ing the clone read-only), and if any of that flakes the job also fails — having spent nothing. The gate then believed a groom had happened and skipped every daily tick for a full GROOM_INTERVAL_DAYS. One flaky npm install cost a whole cycle, silently. Now a failed finder job only counts if the billed step (Run finder) actually ran.

What changed

  • run_audited (.github/groom/interval.py) — a failure conclusion is now eligible but not sufficient: it also needs _agent_step_ran(job). success still counts unconditionally (the job cannot have succeeded without the agent). New _AGENT_STEP_HINTS = ("run finder",) sits next to _FINDER_JOB_HINTS and matches case-insensitively, mirroring the existing hint style.
  • _agent_step_ran reads the per-job steps array ({name, status, conclusion, number}) that GET /actions/runs/{id}/jobs already returns. Three cases: agent step present with a real conclusion → ran; agent step absent from a non-empty array, or present as skipped / no conclusion → did not run; no usable steps at all → count it (fail-safe, see below).
  • fetch_run_jobs was already unprojected (no --jq, no field filter), so nothing had to be threaded through — but its docstring now states that steps is load-bearing, because adding a field filter later would silently degrade the gate with no test failing.
  • Pin testAgentStepNamePinTest parses .github/workflows/groom.yml, extracts the audit_find job's step names, and asserts Run finder is there and is the only step _AGENT_STEP_HINTS matches. Producer and consumer live in different files; this is the only thing that keeps them honest. .github/workflows/test-groom-scripts.yml now also path-filters on groom.yml so a rename there actually runs the pin (without that, the pin would sit dormant for exactly the change it guards).
  • README / docstrings updated with the rule and its deliberate fail-safe direction.

Verified the pin bites: renaming the step to Run the agent in a scratch copy of groom.yml fails test_groom_yml_names_exactly_the_agent_step_this_module_matches; restoring it goes green.

The one place the bias is deliberately inverted

Every other ambiguity in interval.py fails OPEN (groom more often). Missing step data does the opposite and counts the failure, per the ticket: a job with no usable steps (API shape change, truncated payload, an old hand-built fixture) keeps today's behavior, because making a genuinely-spent audit re-bill on the very next daily tick is the expensive direction. Called out explicitly in the code comment, the docstring, the README bullet and a test, since it reads as inconsistent otherwise.

Judgment calls

Overlap with #83 (BE-4757) — expect a textual conflict, they compose cleanly

#83 rewrites run_audited to make the clock per-scope, so whichever of the two lands second needs a small manual reconcile. The two checks are orthogonal and #83 already converged on the same guard-clause shape, so the merged body is: job-name hint → _AUDITED_CONCLUSIONS eligibility → if conclusion == "failure" and not _agent_step_ran(job): continue (this PR) → scope-marker branch (#83). _agent_step_ran, _AGENT_STEP_HINTS and _UNRUN_STEP_CONCLUSIONS are additive and untouched by #83. The pre-agent steps #83 adds (scope.py contain, the empty-tracked-file-list guard) land before Run finder, so they are covered by this gate automatically once merged.

Testing

python3 -m unittest discover -s .github/groom/tests -p 'test_*.py' -v95 tests, all green (was 87). New coverage: failure-after-the-agent still counts (agent success/failure/cancelled); failure whose steps stop before Run finder does NOT count; failure with Run finder present but skipped/null does NOT count; failure with no steps key / empty array / steps: null counts; success unaffected by step data; a spent failure behind an unspent one still counts (job order doesn't decide it); an end-to-end evaluate() case where yesterday's npm install failure leaves today's tick due; and the groom.yml step-name pin.

python3 .github/agents-md-integrity/check_agents_md.py --root . also passes (one pre-existing CODEOWNERS warning, unrelated).

Closes BE-4809.

…gent step ran (BE-4809)

The interval gate counted ANY finder job concluding `success` or `failure` as a
real groom. Counting a `failure` is right when the billed agent ran and the job
died later (filing, PR creation) — but the finder runs two checkouts, an
`npm install`, the prompt build and the read-only chmod BEFORE the agent, and a
transient failure in any of them concludes the job `failure` having spent
nothing. The gate then skipped every tick for a full GROOM_INTERVAL_DAYS: a
silent under-run, the exact failure mode the rest of the module biases against.

`run_audited` now additionally requires, for a `failure`, that the agent step
(`Run finder`) actually ran — read from the per-job `steps` array the jobs API
already returns. `success` still counts unconditionally. Missing/empty step data
falls back to today's behavior and counts the failure: re-billing a
genuinely-spent audit is the expensive direction. A step present but `skipped`
(GitHub's rendering for every step after a failing one) or without a conclusion
did not execute, so it does not count.

A test pins the step name against groom.yml's `audit_find` job, and the test
workflow's path filter now includes groom.yml so a rename runs that pin.
@mattmillerai mattmillerai added agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review labels Jul 28, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review July 28, 2026 01:39
@coderabbitai

coderabbitai Bot commented Jul 28, 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: 20 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: 0ea0d9d6-b5b3-41cd-9ba3-5dbb8d306e98

📥 Commits

Reviewing files that changed from the base of the PR and between f4cbb50 and 63a6413.

📒 Files selected for processing (4)
  • .github/groom/README.md
  • .github/groom/interval.py
  • .github/groom/tests/test_interval.py
  • .github/workflows/test-groom-scripts.yml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-4809-groom-interval-agent-step
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-4809-groom-interval-agent-step

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 3 finding(s).

Severity Count
🟡 Medium 1
⚪ Nit 2

Panel: 8/8 reviewers contributed findings.

Comment thread .github/groom/interval.py Outdated
Comment thread .github/groom/interval.py Outdated
Comment thread .github/groom/tests/test_interval.py Outdated
Addresses the cursor-review panel on #84.

- `fetch_run_jobs` now asks for `filter=all` and pages the result. The
  endpoint defaults to `filter=latest`, which was harmless while every
  `failure` counted but is not once the answer is step-level: an attempt
  that spent the billed agent and died at filing, followed by a manual
  re-run that flakes in `npm install`, read as unspent and re-billed a
  groom that already happened — the exact double-spend this gate exists
  to prevent. `run_audited` needs one attempt to show the step ran, so
  the union of attempts is correct and order-independent.

- Match the agent step by EXACT (case-insensitive) name instead of a
  substring. Job names need substring matching because GitHub mangles
  them in nested reusables; step names come back verbatim, so the
  tolerance bought nothing and would let a neighbour like `Rerun finder`
  stand in for the billed step.

- Harden the pin test's groom.yml scan against benign reformats
  (reindentation, quoted step names, inline comments) and cover the
  scanner itself, so it fails on a rename rather than on a whitespace
  change.
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