Releases: karlkfi/claude-pr-sentinel
Release list
v0.7.0
Wake your session on CI failures and merge conflicts — no foreground polling, no comment-channel injection.
Note
The watcher now keeps its hands off a PR while it sits in a merge queue. Every branch-state remedy is a push, and any push to a queued PR evicts it — so conflict and behind are suspended while the queue owns the PR. Only closed, dequeued, and timeout can end the watch while queued.
Highlights
A merge-queue eviction is now its own wake. Every existing event describes PR health; none described queue membership, so a session could heal everything the watcher ever named and still sit outside the merge queue with nothing left to say re-enqueueing is owed. The new terminal dequeued event is that missing fact: the PR held a merge-queue entry on an earlier poll of this watch, and the entry is gone while the PR is still open. The report names the merge state, folds in the PR_SENTINEL_HEAL-appropriate heal commands when the branch reads DIRTY or BEHIND, and ends with the handback — re-enqueueing starts a merge, so it stays a human action.
An eviction is confirmed before it is reported. GitHub removes the queue entry a moment before a successful queue merge lands, so a single open-and-unqueued poll can be a merge in flight. dequeued fires only after PR_SENTINEL_DEQUEUED_POLLS consecutive polls (default 2, at the base interval); when the "eviction" was really a merge, the confirming poll sees the PR MERGED and reports closed instead of a phantom eviction.
ready is withheld while mergeStateStatus is UNKNOWN. UNKNOWN is not a merge state — it is GitHub saying it hasn't computed one yet, and it can resolve to DIRTY. That window opens whenever a sibling PR merges: for a poll or two the conflict check cannot see the conflict, and a green-but-conflicting PR fired ready. The watcher now requires a computed state, polling at the base interval while it waits (the view query itself triggers the recomputation, so the hold releases within a poll or two); a state that never computes ends the watch in a timeout whose report names the withheld ready.
Upgrading
No action required. Nothing is removed, no configuration is renamed, and the report format and additionalContext shape are unchanged.
Three changes that need no action, but will be visible.
- A new
dequeuedevent can reach a session — any agent guidance that enumerates watcher events should learn it. It is a wake, not a handoff: the Stop hook keeps holding the session responsible until the PR is healed, re-watched, and handed back (#43). - A queued PR stops producing
conflictandbehindwakes until it leaves the queue — the suspension described in the banner above (#43). readycan arrive a poll or two later when GitHub has not yet computed the merge state (#42).
One new knob, documented in Configuration:
| Variable | Default | Purpose |
|---|---|---|
PR_SENTINEL_DEQUEUED_POLLS |
2 |
consecutive polls a once-queued, still-open PR must be missing from the merge queue before dequeued fires |
One known limitation: dequeue detection needs the same watcher run to have seen the PR queued. Queue membership has no before/after outside the run's own memory, so a watcher launched after an eviction reports nothing about it — the PR_SENTINEL_WATCH_UNTIL=closed conflict path still catches the common, branch-dirtying eviction.
Everything since v0.6.0
Two changes ship in the product; docs, test, and backlog work is left out.
New behaviour (1)
- Terminal
dequeuedevent, merge-queue membership tracking, and the queued-PR suspension, gated onPR_SENTINEL_DEQUEUED_POLLS(#43)
Fixes (1)
- Withhold
ready(and theready_watchingnotice) whilemergeStateStatusisUNKNOWN(#42)
Trust boundary
One new read, no new writable channel. Queue membership is a GraphQL mergeQueueEntry query — GitHub-controlled metadata, like everything else the watcher reads — and the gh pr view field list gains the PR's canonical url so that query can be addressed (the field-list test pins the widened list). If the membership query fails — say a token that cannot run GraphQL — queue tracking disables itself and the watcher behaves exactly as it did before the feature. There is still no comment or PR-body ingestion, no merge, and no enqueueing: dequeued hands re-enqueueing to a human, never to the session. No security advisory accompanies this release. PRIVACY.md is updated to list the new query.
Validation
make check on the tagged commit: shellcheck clean, 174 tests passing — 13 new since v0.6.0, all watcher scenarios run against the stub-gh subprocess harness. The nine dequeue scenarios cover eviction with DIRTY/CLEAN/heal-merge reports, the remove-then-merge race reading as closed, BEHIND suppressed while queued, a requeue resetting the confirmation streak, PR_SENTINEL_DEQUEUED_POLLS=1, WATCH_UNTIL=closed, and a GraphQL failure keeping prior behaviour. The four UNKNOWN scenarios cover perma-UNKNOWN ending in timeout, resolution to CLEAN (ready) and to DIRTY (conflict — the observed bug, which fired ready before the fix), and the withheld ready_watching notice; three of the four fail against the pre-fix script.
Full changelog: v0.6.0...v0.7.0
v0.6.0
Wake your session on CI failures and merge conflicts — no foreground polling, no comment-channel injection.
Note
ready now arrives one poll interval later than it used to. That is the cost of the fix below, and it is deliberate. PR_SENTINEL_GREEN_POLLS=1 restores the old timing along with the bug.
Highlights
A green PR is no longer automatically a ready one. ready was decided from gh pr checks buckets alone, and those buckets only contain checks that exist. A required check whose workflow never registered — a path-filtered heavy gate on a PR that opened docs-only — produces no row, so pending_count is zero because the check is absent, not because it reported. In the window right after a push, nothing has registered yet and the whole PR reads green. ready now requires mergeStateStatus != BLOCKED and a streak of consecutive green polls, so an unregistered run turns up as pending on the second one.
A new terminal blocked event, for green PRs that aren't going anywhere. BLOCKED doesn't say which requirement is unmet, and an outstanding approval reads identically to a missing required check — so gating ready on it outright would mean a green PR in a review-gated repo burns the whole budget and wakes with timeout. The two are separated by persistence instead: after PR_SENTINEL_BLOCKED_POLLS consecutive green-but-BLOCKED polls the watcher wakes you with blocked, naming both candidate causes. The Stop hook treats it as concluded — both need a human, neither can be waited out.
A job marked continue-on-error: true no longer wakes you forever. It fails its own check row — gh pr checks reports bucket=fail, indistinguishable from a real failure — but does not fail the workflow run. The run conclusion was the only place the distinction survived, and it was never read, so an intentionally advisory job woke the session on every poll of every PR. Failing checks are now resolved to their runs and treated as passing when every one of them sits in a run GitHub concluded success. Absorption is all-or-nothing and fails safe: no Actions run behind the check, a run still in progress, or an unreadable conclusion all stay a wake.
A network blip no longer reads as revoked credentials. The watcher probed gh auth status after a gh pr view failure and treated any non-zero exit as permanent auth loss — but that probe fires in the moment most likely to break it, and gh doesn't distinguish: with the network unreachable it prints "The token in keyring is invalid." for a perfectly valid token (verified against gh 2.96). The probe is now acted on only where it is conclusive — no credentials configured at all, which gh answers from local config with no round-trip. A revoked token still gets reported, but has to fail for the whole retry horizon rather than for one instant.
Upgrading
No action required. Nothing is removed, no configuration is renamed, and the report format and additionalContext shape are unchanged.
Three changes that need no action, but will be visible.
readyis onePR_SENTINEL_INTERVALslower. The confirming poll is scheduled at the base interval rather than the current backoff, so the cost is bounded to one interval per genuine handoff instead of up toMAX_INTERVAL(#37).- A new
blocked/blocked_watchingevent can reach a session that has only ever seenready. Any agent guidance that enumerates watcher events should learn it (#29). - Some PRs stop waking the session that used to wake it every poll — advisory
continue-on-errorjobs, andghfailures during a network blip (#32, #26).
Two new knobs, both documented in Configuration:
| Variable | Default | Purpose |
|---|---|---|
PR_SENTINEL_GREEN_POLLS |
2 |
consecutive green polls before ready fires; 1 decides on a single poll |
PR_SENTINEL_BLOCKED_POLLS |
3 |
consecutive green-but-BLOCKED polls before blocked fires |
Everything since v0.5.0
Seven changes ship in the product; docs, test, and backlog work is left out.
New behaviour (2)
- Terminal
blockedevent and itsblocked_watchingnotice, gated onPR_SENTINEL_BLOCKED_POLLS(#29) readyconfirmed acrossPR_SENTINEL_GREEN_POLLSconsecutive polls (#37)
Fixes (5)
- Don't wake for a failure the workflow run absorbed (#32)
- Don't call a correlated auth-probe failure permanent (#26)
- Don't nudge on a tag push, and say so when no PR number resolves (#34)
- Scope the foreground-poll deny to the harm it names: a backgrounded Bash call can't block the session, and a
sleeploop aroundcurlisn't polling CI (#36) - Honour an inline
PR_SENTINEL_OVERRIDE=1prefix, not just an exported variable (#27)
Trust boundary unchanged
No new data source. Resolving a failing check to its workflow run reads GitHub-controlled run metadata through a link the check row already carries. There is still no comment or PR-body ingestion, no merge, and no fix outside the visible session.
Full changelog: v0.5.0...v0.6.0
v0.5.0
Watcher: keep watching past green with PR_SENTINEL_WATCH_UNTIL=closed
The watcher exits on ready, but the PR is still open at that point and sitting in human merge review — exactly the window where a sibling PR merging turns it CONFLICTING. Nothing is left watching, so nothing wakes you. This only bites with concurrent PRs, so it's an opt-in stopping condition rather than a change to the default.
Set PR_SENTINEL_WATCH_UNTIL=closed and the watcher reports green once as a non-terminal ready_watching notice and keeps polling. The DIRTY/BEHIND checks already at the top of the loop still fire, so a later conflict, a branch falling behind, or a newly failing check wakes the session; the watch ends when the PR is merged or closed.
Exiting on a still-green PR and relying on a relaunch does not work: the relaunched watcher re-evaluates immediately, sees the same green state, and exits again with no sleep in the cycle — a spin loop, not a watch.
Trade-offs
- The session is not woken when the PR turns green, so it can't announce "ready for review" the moment it happens — the notice lands in the watcher's task output instead.
- A watch that now spans human review time usually wants a larger
PR_SENTINEL_TIMEOUTthan the 1-hour default, or it wakes with atimeoutevent and needs a relaunch.
Stop hook
ready_watching is a distinct event name so the Stop hook can tell "handed off" from "green, still watching". A watch in closed mode that exits on the notice without a terminal event now blocks once to relaunch, instead of going quiet on a PR that is still open and unwatched.
Trust boundary unchanged
No new data source: the loop already read mergeStateStatus every cycle. There is still no merge, no comment ingestion, and no fix outside the visible session.
Closes #23.
Full changelog: v0.4.1...v0.5.0
v0.4.1
What's changed
- fix(stop-hook): stop treating pr-link records as PR ownership (#22). The Stop hook treated every
pr-linktranscript record as a PR the session opened, so a session that merely viewed or commented on someone else's PR was repeatedly blocked with a demand to launch a watcher for it. Ownership is now derived from agh pr createcorrelated with the PR URL in its own output, plus any PR the session launched a watcher for —pr-linkrecords are ignored. Sessions that only touch foreign PRs are no longer falsely blocked, while the backstop for resumed sessions with an unwatched own PR still works.
Full changelog: v0.4.0...v0.4.1
v0.4.0
Watcher: tolerate transient GitHub API failures
The background watcher no longer wakes your session with a false error event when GitHub has a brief hiccup. gh failures are now classified:
- Permanent failures (failing
gh auth status, a definitively unresolvable PR) still exit immediately with theerrorevent. - Transient failures are retried with backoff over a generous horizon (default 15 minutes) before giving up. Transient gaps are logged to stderr only — never the stdout wake payload — so the session stays asleep.
Closes #20.
Configuration change
PR_SENTINEL_GH_RETRIES has been replaced by PR_SENTINEL_GH_RETRY_HORIZON (seconds to retry transient gh failures before an error event; default 900). Update any environment overrides that referenced the old knob.
Full changelog: v0.3.2...v0.4.0
v0.3.2
v0.3.1
Fixes
- Stop hook: read the watcher output file directly instead of relying on a Read-tool handshake (#14). A session that inspected watcher output via a Bash
cat/tail(rather than the Read tool) was never recognized as having handled the hand-off, so the Stop hook re-blocked on every stop — looping forever even after the watcher authoritatively reported the PR merged or closed. The hook now opens the watcher's output file directly (learned from the task-notification) and judges its header region for the ready/closed marker, independent of how the session surfaced the output. A transcript Read remains a fallback for when the file has been cleaned up. Header-region forgery guard and fail-open behavior are unchanged.
Full changelog: v0.3.0...v0.3.1
v0.3.0
Hardening of the Stop backstop that stands the session down when a PR is
concluded, closing two prompt-injection / livelock gaps in how it reads the
watcher's report.
Changes
-
feat(stop-hook): dampen repeated, unfixable check_failure blocks (#9) —
the watcher now prints the head commit (Head SHA:) in the check_failure
header, and the hook fingerprints each report by its failed-check set + head
SHA (read from the header region only). Once two reports carry an identical
signature — a real fix would have moved the SHA — the hook infers no fix is
coming, allows the stop, and emits a non-blockingsystemMessageso the red
PR stays visible. One block to try a fix; no more livelock that previously
requiredPR_SENTINEL_DISABLE=1to escape; never a silent walk-away. -
fix(stop-hook): reject a forged ready marker in a CI-log excerpt (#10) —
aPR-SENTINEL EVENT: ready/closedmarker planted in a semi-untrusted CI
log excerpt could stand the backstop down on a still-red PR. Marker matching
is now anchored to the report's header region (above the first
----- BEGIN CI LOG EXCERPTbanner), which the watcher always writes before
any excerpt, so a planted marker cannot climb above the real one.
v0.2.0
What's new
Features
- Configurable conflict healing — the watcher now lets you choose how a conflicting PR is healed, defaulting to rebase. Configure via the documented env-var knob; see the Configuration table in the README.
- Guard auto-allows the plugin's own watcher launch — the guard hook now recognizes and auto-allows pr-sentinel's own background watcher command, so launching it no longer trips a permission prompt.
Docs & brand
- Added brand images (icon, favicon set, social preview) and a rendering-images guide.
- Documented the watcher-launch auto-allow and the configurable conflict-healing behavior.
Full changelog: v0.1.0...v0.2.0
v0.1.0
First tagged release of pr-sentinel — a Claude Code plugin that wakes your session on CI failures and merge conflicts, without foreground polling and without ingesting the PR comment channel.
What's in it
- Background watcher (
scripts/pr-sentinel-watch.sh) — pollsghper-PR and wakes the session when a check fails, a conflict appears, the PR goes green, or the PR closes. Reads GitHub-controlled metadata only; never queries the PR body or comments. - PostToolUse hook (
scripts/pr-sentinel-hook.py) — nudges the session to launch the watcher aftergh pr create/git push. - Stop-hook backstop — blocks the stop once when the session has an open PR with pending required checks and no live watcher.
- PreToolUse foreground-poll deny — denies
gh pr checks --watch,gh run watch, anduntil/while … sleeppoll loops, pointing at the watcher instead (with aPR_SENTINEL_OVERRIDEescape hatch).
Security posture
- No ingestion of PR/issue comments or the PR body (the injection channel, excluded by design).
- No auto-merge — human merge review stays the trust boundary.
- CI log excerpts are size-capped, ANSI-stripped, and framed as data, not instructions.
Deferred to later minor releases
- Friction/activity report (Q3)
- Distinguishing required vs optional checks (Q4)