Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/groom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,41 @@ real groom, so a skipped tick costs ~nothing (it never reaches the finder).
interval-skip ticks in between never reset the clock. (A repo variable would
need a `Variables: write` credential the run doesn't carry, and a missing grant
would fail *silently* into a daily over-spend — run history has no such trap.)
- **A NON-SUCCESS finder job counts only if it actually spent the audit**
(BE-4814). A failure that reached the agent and died later (the JSON assert, an
upload) still counts — otherwise a run that cost money re-spends on tomorrow's
tick. But the job can also die *before* the agent (checkout, asset load, prompt
build), and those bill nothing, so counting them would advance the clock and
suppress every tick for a whole `GROOM_INTERVAL_DAYS` — hiding a typo'd input
or a broken caller for a week rather than letting it recur daily until someone
notices. The gate therefore requires **positive evidence**: the jobs API's
per-job `steps[]` must show the agent step (`Run finder`, pinned as
`interval.agent_step_name()`) actually started. Every ambiguity — no `steps[]`,
an empty one, the step absent, still `queued`, or `skipped` — reads as **not
audited**, i.e. re-run. A duplicated audit costs one run; a suppressed one
hides a broken caller for a full interval. A `success` needs no such check (the
agent step is upstream of everything that could still fail, and no `if:`
guards it).
- Which endings take the evidence path is a **denylist**, not an enumeration:
only `skipped` (this gate's own interval-skip) and an unfinished run are
excluded outright. Everything else — `failure`, `timed_out`, `cancelled`,
and the rarer `neutral`/`stale`/`action_required` — is decided by the step
evidence, because if the agent ran, the audit was spent however the job was
finally stamped. An allowlist would silently forget any ending it missed.
- `timed_out`/`cancelled` are the expensive members: the finder job runs under
`timeout-minutes: 40`, so a **hung agent bills the whole window** and only
then trips the timeout. GitHub stamps that in-flight step `cancelled` —
indistinguishable *by conclusion* from a step that was never reached, so the
gate reads the step's **timestamps**: a `started_at` strictly before its
`completed_at` proves it ran; no span is no evidence (fail-open).
- Evidence is looked for across a run's **earlier attempts**, not just the
latest. The jobs endpoint reports only the newest attempt, so a manual
re-run that dies in checkout would otherwise erase the record of an earlier
attempt that did reach the agent — and re-spend that audit. The walk is
newest-first and bounded, and when an earlier attempt supplies the evidence
the clock anchors on **that attempt's** finder-job start, not the run's
`run_started_at` (which tracks the re-run and would date a week-old audit to
today).
- **`workflow_dispatch` bypasses THIS gate** — a manual dispatch is never
interval-throttled. It is not a blanket "always runs": the volume gate is a
second, independent throttle, so a live dispatch into a quiescent repo can
Expand Down
Loading