v0.5.0 — A refusal that can never resolve now reaches a human
The gap this closes
The gate wakes on every CI completion and every trusted-bot comment, checks its conditions, and on a refusal writes one line to a log nobody reads. That's right for most refusals, which mean "not yet" — CI still running, review not posted — because the gate gets woken again.
Some refusals mean "never", and the gate couldn't tell the difference. It would refuse the same PR forever and tell nobody. A security update that can't land looks exactly like one nobody needed.
I audited every refusal the gate can emit against every notifier that exists. Two are terminal and reach no one:
- An unparseable review verdict. The review job's assertion greps for the opening marker only, so a comment carrying the marker with truncated or malformed JSON inside passes it green — review SUCCESS, no label, no assignee — while the gate refuses that same comment forever. A truncated agent comment does exactly this.
- A group PR touching a non-whitelisted path. The assertion skips group PRs by design, and autofix needs a CI failure while CI here is green. Three notifiers, and the PR falls between all of them.
Both now assign, label, and comment — once per PR, not once per wake-up.
Deliberately still silent
Escalation is an allow-list, so anything added later stays quiet by default. Being paged about a PR that fixes itself is what teaches you to ignore the label, and then you miss the real one.
| refusal | why it stays quiet |
|---|---|
| review verdict not posted yet | the review job already labels, assigns, and goes red |
| reviewer said escalate / usage affected | the reviewer labels the PR itself |
| CI still running | transient by definition |
| CI cancelled, timed out, awaiting approval | a new push re-runs CI and re-wakes the gate, so these usually resolve themselves. Known gap, on purpose |
| not a Dependabot PR, PR closed, empty diff | anomalies — the gate is looking at something it shouldn't act on |
One owner for a red build
When CI goes red, the autofix job and the gate wake from the same event and run in parallel. If both escalated you'd be paged about a build the fixer is already fixing. The installer now renders only the taken branch: autofix on → autofix owns it; --no-autofix → the gate does.
And autofix's side is deterministic now. Its no-edits path used to print "The fixer made no edits" and exit quietly, leaving the notification to the agent choosing to label the PR — which its prompt asks for but nothing enforces. An agent that errored, or simply didn't, left a red PR with nobody told. "Covered sometimes" is worse than not covered, because you learn to trust it.
A total outage this release nearly shipped
Refactoring the comment-posting helper added an opening quote and not the closing one. Everything after it parsed inside an unterminated string, which kills the whole auto-merge step — meaning no PR would ever merge again.
The full suite was green. CI shellchecked install.sh and the tests exercised gate.cjs, but nothing had ever parsed the shell those two conspire to generate.
test/workflow-shell.test.mjs closes that: every run: block in the rendered workflow is extracted and parsed with bash -n — the shell GitHub actually runs them with. It names the offending step and line. Mutation-checked against the exact bug that motivated it.
(If you write your own workflow shell, the lesson generalizes: check it with bash -n, not sh -n. On macOS /bin/sh is bash and they look identical; on Ubuntu it's dash, and every <<< fails.)
Upgrade
Re-run the installer. gate.cjs gains a code= output line; if you've hand-edited your installed gate or workflow, re-render rather than merging by hand.
Full changelog: v0.4.0...v0.5.0