Skip to content

feat(groom): configurable run cadence via GROOM_INTERVAL_DAYS interval gate (BE-4004)#56

Open
mattmillerai wants to merge 8 commits into
mainfrom
matt/be-4004-groom-interval-days
Open

feat(groom): configurable run cadence via GROOM_INTERVAL_DAYS interval gate (BE-4004)#56
mattmillerai wants to merge 8 commits into
mainfrom
matt/be-4004-groom-interval-days

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

ELI-5

Groom used to run on a hardcoded weekly cron — to change how often it runs you had to edit the workflow file. GitHub Actions crons can't be "every N days" at runtime, so this switches groom to the standard trick: fire a cheap tick every day, but only actually do work every N days, where N is a live knob you set with a repo variable (GROOM_INTERVAL_DAYS, default 7 = today's weekly). A tick that's too soon exits in a few seconds before the expensive finder ever starts. Change the variable, cadence changes — no PR, no YAML edit.


⚠️ REQUIRED MERGE STEP — bump the caller pin

The interval gate lives in the new groom.yml, so ci-groom.yml pins a placeholder (@REPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHA) that fails loud (unresolvable ref / failed assets checkout) until bumped. This is the same reusable-lands-then-bump-caller two-step the pilot used (#49#53), compressed into one PR. After squash-merge, bump both uses: and workflows_ref in .github/workflows/ci-groom.yml to this PR's merge SHA. groom is schedule/dispatch-only (never on-PR), so nothing breaks on the PR itself; the placeholder just guarantees the daily cron can't silently run against the old (gate-less) reusable.

(No bump-groom-callers.yml automation exists yet — groom has a single caller. A dedicated bumper is a reasonable follow-up if more repos enroll.)

What changed

  • ci-groom.yml (caller): weekly cron → daily 17 9 * * *; grants actions: read; wires interval_days + cadence to ${{ vars.GROOM_INTERVAL_DAYS || '7' }}.
  • groom.yml (reusable): new interval_days input (default 7); the gate job now checks out the assets, runs the interval gate before the finder, then the (unchanged) volume gate, composing both into should_run. Gate job gains actions: read.
  • .github/groom/interval.py + tests: the gate. Derives the last real groom run from this caller's Actions run history — a run counts only if its Audit — finder job actually ran, so interval-skip ticks never reset the clock — and early-exits unless interval_days elapsed. Pure decision logic split from the gh I/O shell, mirroring ledger.py. Fail-open on any history-read error.
  • Docs: README table, .github/groom/README.md (new section), AGENTS.md.

Acceptance criteria

  • Cadence set by GROOM_INTERVAL_DAYS (default 7); changing it changes frequency with no workflow-file edit.
  • Base cron fires daily; a within-interval tick no-ops cheaply (exits in the gate job, before the finder).
  • workflow_dispatch always runs regardless of the interval.
  • Last-run state durable across CI runs (GitHub run history — no net-new secret, only actions: read).
  • Default (unset variable) stays weekly.

Judgment calls (worth a reviewer's eye)

  1. Last-run state = run history, not a GROOM_LAST_RUN repo variable. The ticket sanctioned either. A repo variable needs a Variables: write credential the run doesn't otherwise carry — and a missing grant would fail silently into a daily over-spend. Run history is durable, needs only actions: read, and has no silent-failure trap.
  2. Interval delivered as a reusable input wired to the var (rather than the reusable reading vars.GROOM_INTERVAL_DAYS directly). The caller evaluates the var in its own context — unambiguous — matching how cadence/dry_run/volume_gate are already wired. Same "no file edit" ergonomics.
  3. workflow_dispatch bypasses the interval gate but not the pre-existing volume gate for a non-dry-run dispatch. That volume-gate-on-dispatch behavior is a deliberate spend guard the pilot chose (volume_gate: dry_run != 'true'); the ticket's "always runs regardless of the interval" is about the interval gate, so I left the volume gate alone (Chesterton's fence). A dry-run dispatch bypasses both, as before.
  4. github.workflow_ref resolves to the caller (ci-groom.yml), not the reusable — the OIDC workflow_ref (caller) vs job_workflow_ref (reusable) distinction. If that assumption were ever wrong, the run-history query returns nothing → the gate fails open (runs), never crashes.
  5. A finder job that failed still counts as a real run (not just success) — so a run that spent the billed audit but died at a later step doesn't re-spend on the next daily tick. Trade-off documented in interval.py.

Testing

  • python3 -m unittest discover -s .github/groom/tests54 passed (16 new for the interval gate: within/at/after-interval, skip-ticks-don't-reset, dispatch-always-runs, no-history/API-error fail-open, job-name matching).
  • actionlint (with shellcheck on the embedded run: blocks) clean on both workflows.

…l gate (BE-4004)

GitHub Actions `schedule:` cron is static in the workflow file — there is no
native runtime "every N days". Replace groom's hardcoded weekly cron with the
standard frequent-base-cron + runtime-gate pattern so cadence is a live knob:

- ci-groom.yml now fires DAILY; the effective cadence is a new runtime gate.
- New reusable input `interval_days` (default 7 = today's weekly behavior),
  wired in the caller to repo Actions variable GROOM_INTERVAL_DAYS. Changing the
  variable retunes cadence (weekly -> every-3-days -> daily) with no file edit.
- `.github/groom/interval.py`: the gate. At run start it derives the last REAL
  groom run from this caller's Actions run history (a run counts only if its
  finder job actually ran, so interval-skip ticks never reset the clock) and
  early-exits cheaply — before the finder — unless interval_days have elapsed.
  Durable across stateless CI runs, no net-new secret, only `actions: read`.
  Pure decision logic split from the gh I/O shell + a unittest suite, mirroring
  ledger.py.
- `workflow_dispatch` always bypasses the interval gate (manual override).
- Fail-open on any history-read error (mirrors the volume gate).

The gate runs in the existing `gate` job before the finder; the volume gate is
kept as a second throttle with its window matched to the effective cadence.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

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: 48 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: 4f9b351f-6ed8-4534-b8cc-cb7aac9d56e1

📥 Commits

Reviewing files that changed from the base of the PR and between 7ec610b and 55edfa6.

📒 Files selected for processing (3)
  • .github/groom/interval.py
  • .github/groom/tests/test_interval.py
  • .github/workflows/groom.yml
📝 Walkthrough

Walkthrough

The groom workflows now run on a daily base schedule and apply configurable interval and volume gates. The interval gate inspects audited workflow history, supports manual-dispatch bypasses, fails open on errors, and emits JSON decisions.

Changes

Groom cadence control

Layer / File(s) Summary
Interval decision logic
.github/groom/interval.py, .github/groom/tests/test_interval.py
Adds cadence normalization, timestamp handling, audited-run detection, and pure run-or-skip decisions with unit coverage.
Run-history evaluation and CLI
.github/groom/interval.py, .github/groom/tests/test_interval.py
Queries workflow runs and jobs, identifies the last audited run, applies dispatch and fail-open behavior, and emits JSON decisions.
Workflow cadence and gate integration
.github/workflows/ci-groom.yml, .github/workflows/groom.yml
Changes schedules to daily base cron, wires GROOM_INTERVAL_DAYS, adds actions: read, and combines interval and volume gate results.
Cadence documentation
.github/groom/README.md, AGENTS.md, README.md
Documents runtime cadence, history-based gating, dispatch behavior, permissions, and configuration.

Sequence Diagram(s)

sequenceDiagram
  participant GroomWorkflow
  participant interval.py
  participant GitHubActions
  participant VolumeGate
  GroomWorkflow->>interval.py: Evaluate interval gate
  interval.py->>GitHubActions: Read workflow runs and finder jobs
  GitHubActions-->>interval.py: Return run history
  interval.py-->>GroomWorkflow: Return interval result
  GroomWorkflow->>VolumeGate: Evaluate merged-PR volume when interval allows
  VolumeGate-->>GroomWorkflow: Return volume result
  GroomWorkflow-->>GroomWorkflow: Set should_run from both gate results
Loading
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-4004-groom-interval-days
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-4004-groom-interval-days

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

@mattmillerai mattmillerai added agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review labels Jul 22, 2026
@mattmillerai mattmillerai added cursor-review Multi-model cursor review and removed cursor-review Multi-model cursor review labels Jul 22, 2026

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Around line 55-59: Update the interval.py description in AGENTS.md to state
that the cadence gate derives its state from GitHub Actions run history, rather
than GitHub issue and PR state. Keep the issue/PR storage description associated
only with the ledger behavior and preserve the existing GROOM_INTERVAL_DAYS
context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 679677d0-a97e-48ad-837f-ec1ed95ed544

📥 Commits

Reviewing files that changed from the base of the PR and between cc325ca and bfb6a5e.

📒 Files selected for processing (7)
  • .github/groom/README.md
  • .github/groom/interval.py
  • .github/groom/tests/test_interval.py
  • .github/workflows/ci-groom.yml
  • .github/workflows/groom.yml
  • AGENTS.md
  • README.md

Comment thread AGENTS.md Outdated
mattmillerai and others added 2 commits July 21, 2026 20:48
The cadence gate derives state from Actions run history, not GitHub
issue+PR state (that's the ledger's store); drop the misplaced sentence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…terval-days

# Conflicts:
#	.github/workflows/ci-groom.yml
#	.github/workflows/groom.yml
#	README.md

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/ci-groom.yml (2)

82-94: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Replace both placeholders before merge. uses: and workflows_ref need the same real squash-merge SHA, and uses: should keep the trailing # v1 comment. As-is, the reusable pin won’t resolve cleanly once this lands, and the groomer will go bonkers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-groom.yml around lines 82 - 94, Replace both
REPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHA placeholders in the groom workflow
configuration with the actual squash-merge commit SHA, using the identical SHA
for uses and workflows_ref. Preserve the trailing # v1 comment on the uses
reference.

Source: Coding guidelines


98-104: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize the cadence once before both gates. interval.py already turns blank, negative, and bad values into 7 while honoring 0, but the volume gate still feeds inputs.cadence straight into date -d. That means the two gates can drift apart, and 0 won’t mean the same thing in both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-groom.yml around lines 98 - 104, Normalize
GROOM_INTERVAL_DAYS once using the existing interval.py behavior, including
blank, negative, invalid, and zero values, then reuse that normalized value for
both interval_days and cadence. Update the workflow’s volume-gate input so it no
longer passes the raw inputs.cadence directly to date -d, keeping both gates
aligned on the same effective cadence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 18: The README workflow description incorrectly says the audit job is
contents-read-only. Update the groom.yml description in the table entry to state
that the audit agent jobs have no repository write permissions, while preserving
the existing explanation that finder/verifier jobs require id-token: write for
OIDC.

---

Outside diff comments:
In @.github/workflows/ci-groom.yml:
- Around line 82-94: Replace both REPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHA
placeholders in the groom workflow configuration with the actual squash-merge
commit SHA, using the identical SHA for uses and workflows_ref. Preserve the
trailing # v1 comment on the uses reference.
- Around line 98-104: Normalize GROOM_INTERVAL_DAYS once using the existing
interval.py behavior, including blank, negative, invalid, and zero values, then
reuse that normalized value for both interval_days and cadence. Update the
workflow’s volume-gate input so it no longer passes the raw inputs.cadence
directly to date -d, keeping both gates aligned on the same effective cadence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35764ef5-53ef-4885-9882-f0855d3d0063

📥 Commits

Reviewing files that changed from the base of the PR and between bfb6a5e and acf6445.

📒 Files selected for processing (4)
  • .github/workflows/ci-groom.yml
  • .github/workflows/groom.yml
  • AGENTS.md
  • README.md

Comment thread README.md Outdated
…4004)

Both gates are wired to the same GROOM_INTERVAL_DAYS knob in the caller, but
only the interval gate normalized it — the volume gate fed inputs.cadence
straight to `date -d`, so the two drifted on reachable values:

  -3  interval.py -> 7 (safe weekly default), but `date -d '-3 days ago'` is a
      cutoff 3 days in the FUTURE, matching no merged PR -> the volume gate
      skips EVERY run, silently disabling groom.
  0   a legitimate "no throttle" for the interval gate, but a 0-day merge
      window is today-only, so most ticks are judged quiescent.

Add normalize_cadence_days() next to parse_interval_days() (same
blank/garbage/negative -> 7 degradation, then floored at 1 whole day) plus a
flag-only `interval.py --normalize-cadence <raw>` mode the volume gate shells
out to, so one parser backs both gates. The floor removes no capability:
`volume_gate: false` is the input that expresses "no merge-activity throttle".

Also correct the README's groom permission claim — the audit jobs are not
`contents: read` only; they also hold the `id-token: write` claude-code-action
needs to mint its OIDC token. Reworded to "no repository write credentials",
which is the property that actually matters.

Both per CodeRabbit review on #56.
@mattmillerai

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit review in 160fdea. Both out-of-diff findings, plus the inline README one:

ci-groom.yml 98-104 — normalize the cadence once before both gates: fixed, and it was a real bug.

Fixed in the reusable (groom.yml) rather than this caller, so every consumer gets it — a caller can't sanitize a vars.* value in a GitHub expression anyway.

normalize_cadence_days() now sits next to parse_interval_days() in interval.py (same blank/garbage/negative → 7 degradation, then floored at 1 whole day), and the volume gate shells out to a new flag-only interval.py --normalize-cadence "$CADENCE" mode instead of feeding the raw value to date -d. One parser now backs both gates.

Confirmed the drift empirically against a GNU-date harness (today = 2026-07-23):

GROOM_INTERVAL_DAYS interval gate volume gate before volume gate after
-3 7 (safe default) cutoff 2026-07-263 days in the future, matches no merged PR → skips every run 7 → 2026-07-16
0 0 = no throttle 2026-07-23 — today-only window 1 → 2026-07-22
abc 7 date errors → fail-open (gate effectively off) 7 → 2026-07-16

The -3 row is the one that bites: a misconfigured negative value degraded the interval gate safely to weekly while silently disabling groom entirely through the volume gate. Note the floor removes no capability — volume_gate: false is the input that expresses "no merge-activity throttle", and it stays available (this caller already uses it for dry-runs).

Covered by 5 new cases in test_interval.py (70 pass); actionlint clean on both workflows.

ci-groom.yml 82-94 — replace the placeholders: working as intended, no change.

REPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHA can't be resolved before merge — the squash SHA doesn't exist yet, and this caller deliberately pins by SHA rather than a local ./ path so a PR can't run its own copy of the auditor that judges it (the AGENTS.md "scripts are the single source of truth" rule). This is the established reusable-lands-then-bump-caller two-step used for the pilot (#49#53), and the placeholder is chosen to fail loud (unresolvable ref) rather than silently run a stale groom — the header comment at lines 82-86 documents the post-merge bump. The trailing # v1 convention applies to third-party action pins; this self-referential pin carries a # BE-4004 interval gate — bump to merge SHA marker instead, matching the existing ci-*.yml callers here.

README.md 18 — audit permission description: fixed (replied on the thread).

Reconcile the interval-gate feature (BE-4004) with main's BE-4214
agent-CLI migration: agent jobs no longer need id-token: write (they
invoke the Claude CLI directly), but the interval gate still needs
actions: read to inspect this workflow's run history.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/groom/interval.py (1)

93-133: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle non-finite cadence values before the int() cast
GROOM_INTERVAL_DAYS=inf or nan slips past parse_interval_days() and makes normalize_cadence_days() throw on the int() conversion, so the --normalize-cadence path can crash instead of falling back like the other bad inputs. A tiny math.isfinite() guard would keep the float gremlins from sneaking through.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/groom/interval.py around lines 93 - 133, Update parse_interval_days
to reject non-finite float values using math.isfinite before returning the
parsed value, so inf and nan fall back to default like other invalid inputs.
Keep normalize_cadence_days and valid finite-value behavior unchanged.
.github/workflows/groom.yml (1)

287-294: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add issues: read and pull-requests: read to gate
gate runs gh api search/issues in the volume gate, and that token scope needs both permissions. Without them, the call can 403 and the gate fail open, so the repo keeps on gadding about instead of skipping quiet runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/groom.yml around lines 287 - 294, Add issues: read and
pull-requests: read to the gate job’s permissions block in the workflow,
alongside contents: read and actions: read, so the gh api search/issues call in
the volume gate has the required scopes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.github/groom/interval.py:
- Around line 93-133: Update parse_interval_days to reject non-finite float
values using math.isfinite before returning the parsed value, so inf and nan
fall back to default like other invalid inputs. Keep normalize_cadence_days and
valid finite-value behavior unchanged.

In @.github/workflows/groom.yml:
- Around line 287-294: Add issues: read and pull-requests: read to the gate
job’s permissions block in the workflow, alongside contents: read and actions:
read, so the gh api search/issues call in the volume gate has the required
scopes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 28e9a0c1-f810-4a68-9e12-1a4aa040be20

📥 Commits

Reviewing files that changed from the base of the PR and between acf6445 and 5195cbe.

📒 Files selected for processing (6)
  • .github/groom/README.md
  • .github/groom/interval.py
  • .github/groom/tests/test_interval.py
  • .github/workflows/ci-groom.yml
  • .github/workflows/groom.yml
  • README.md

@Comfy-Org Comfy-Org deleted a comment from linear-code Bot Jul 26, 2026

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

Solid design overall — daily base cron + runtime gate, state from run history, fail-open everywhere, pure logic split from gh I/O, good test coverage. Two issues before merge (inline comments):

  1. Cron-jitter drift in interval_decision: elapsed >= interval_days with once-a-day ticks and GitHub's late-firing cron makes a 7-day interval oscillate to 7–8 days and drift later over time. One-line fix: half-a-tick tolerance or calendar-day comparison.
  2. type: number on interval_days/cadence defeats the documented garbage-degradation: a non-numeric GROOM_INTERVAL_DAYS fails the workflow at call time (fail-closed) before parse_interval_days ever runs. Either switch the inputs to type: string so the Python parser owns normalization, or fix the docs.

Comment thread .github/groom/interval.py
Comment thread .github/workflows/groom.yml Outdated
…nputs (BE-4004)

Review follow-ups on the BE-4004 cadence gate.

1. Cron jitter drifted the cadence by whole days. The gate compared fractional
   days elapsed since the last real run's `run_started_at` against the full
   `interval_days`, but ticks only arrive once per day and GitHub's scheduler
   fires cron late by an unpredictable amount. A last real run that started Mon
   09:31 (scheduler delay) against next Monday's 09:19 tick measures 6.99 days
   < 7 and skips; the run slips to Tuesday, the clock re-anchors on THAT run,
   and the cadence ratchets later every cycle. The bar is now `interval_days`
   less a half-tick of slack (`interval_threshold`), capped at half the interval
   so a sub-daily cadence keeps a real throttle. It cannot pull a run a whole
   tick early — consecutive daily ticks are ~1.0 day apart, well over the 0.5
   tolerance. Boundary tests at 6.99/6.75/6.5 (run) and 6.49/6.25/6.0 (skip).

2. `type: number` made the documented garbage-degradation unreachable, and
   failed CLOSED. Both `interval_days` and `cadence` are wired to
   `${{ vars.GROOM_INTERVAL_DAYS || '7' }}` — a free-text Actions variable a
   human edits. A non-numeric value ("weekly", "7d") is rejected by GitHub's
   string->number coercion at workflow-call time, so the run fails loudly and
   `parse_interval_days` / `normalize_cadence_days` never get to degrade it to
   7. That is fail-closed, the opposite of this gate's stated design, and the
   README/docstrings claimed a path GitHub's own type check made unreachable.
   Both inputs are now `type: string` (default '7'), making interval.py the
   single normalization authority. Both only ever flow into shell env vars, and
   groom.yml's sole caller passes string expressions already.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mattmillerai
mattmillerai requested a review from bigcat88 July 26, 2026 09:36

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/groom.yml (1)

101-109: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document manual dispatch behavior precisely.

The documentation presents workflow_dispatch as an always-run bypass, but the final decision at Lines [426-438] requires both interval and volume gates to be true. Manual dispatch bypasses interval throttling; the existing volume gate can still prevent execution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/groom.yml around lines 101 - 109, Update the workflow
documentation near the cadence description and the final decision logic to state
that workflow_dispatch bypasses interval throttling but remains subject to the
volume gate. Ensure the documented behavior matches the implementation at the
final decision path, rather than describing manual dispatch as an unconditional
run.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/groom/interval.py:
- Around line 76-90: Correct the explanatory comment above the elapsed-days
tolerance so it states that consecutive daily ticks are approximately one day
apart and therefore exceed the 0.5-day tolerance. Do not change the tolerance
logic or surrounding cadence behavior.

---

Outside diff comments:
In @.github/workflows/groom.yml:
- Around line 101-109: Update the workflow documentation near the cadence
description and the final decision logic to state that workflow_dispatch
bypasses interval throttling but remains subject to the volume gate. Ensure the
documented behavior matches the implementation at the final decision path,
rather than describing manual dispatch as an unconditional run.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1adbf2e6-69f5-4dd1-ad7c-7082ee06c2dc

📥 Commits

Reviewing files that changed from the base of the PR and between 5195cbe and 7ec610b.

📒 Files selected for processing (4)
  • .github/groom/README.md
  • .github/groom/interval.py
  • .github/groom/tests/test_interval.py
  • .github/workflows/groom.yml

Comment thread .github/groom/interval.py Outdated
… comment (BE-4004)

Addresses remaining CodeRabbit findings: parse_interval_days now rejects
inf/nan (would otherwise wedge the interval gate via a NaN threshold, or
crash normalize_cadence_days's int() cast); the gate job's permissions
block was missing issues:read/pull-requests:read, so the volume gate's
search/issues call 403s on every run and always fails open, silently
neutering the gate; and corrects the inverted tolerance comment at
interval.py:90.
@mattmillerai

Copy link
Copy Markdown
Contributor Author

Addressed the remaining CodeRabbit findings in 55edfa6:

  • .github/groom/interval.py 93-133 — non-finite (inf/nan) cadence values: parse_interval_days now rejects them via math.isfinite, same as any other garbage input. Previously nan slipped through as a valid float and would wedge the interval gate silently (a NaN threshold makes every >= comparison False, so it never fires again), and inf/nan would raise inside normalize_cadence_days's int() cast. Added 2 test cases.
  • .github/workflows/groom.yml 287-294 — missing issues: read/pull-requests: read on the gate job: confirmed and fixed. A job's permissions: block isn't additive with the caller's grant — it was capped to contents: read + actions: read only, so the volume gate's gh api search/issues call 403'd on every run (not just an edge case) and always fell back to fail-open run=true. That's the safe direction (never silently skips), but it meant the volume gate never actually gated anything. Added the two scopes; the one caller (ci-groom.yml) already grants issues: write (⊇ read) + pull-requests: read, so no caller change needed.
  • .github/groom/interval.py:90 — inverted tolerance comment: fixed and resolved on its thread.

actionlint clean on both workflow files; groom test suite (78 tests) green.

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