Skip to content

v2.7.0 fix(eng): pr-watcher v4 senses via a deterministic script, not a subagent - #58

Merged
mujtaba3B merged 8 commits into
mainfrom
fix/pr-watcher-deterministic-sensor
Jul 20, 2026
Merged

v2.7.0 fix(eng): pr-watcher v4 senses via a deterministic script, not a subagent#58
mujtaba3B merged 8 commits into
mainfrom
fix/pr-watcher-deterministic-sensor

Conversation

@mujtaba3B

@mujtaba3B mujtaba3B commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the /eng:pr-watcher sensor design so it can never sit parked after CodeRabbit has already finished (observed live on email-hero PR 79, 2026-07-20: the sensor subagent backgrounded its polling, then parked on Monitors whose conditions fired within a minute of CodeRabbit's terminal status, with no agent left to consume them).

Root cause: the sensor-subagent contract ("block 30 minutes inside one agent turn, end with exactly one JSON") is structurally unsatisfiable. Foreground sleep is blocked for agents, and both background tasks and Monitor end the agent's turn, which the dispatcher reads as the final answer.

The fix (pr-watcher v4):

  • The sensor is now scripts/sensor-poll.sh, a deterministic bash script the dispatcher runs in FOREGROUND Bash slices. One command in, one JSON out, by construction; a continue outcome plus sensor-state.json spans the 30-minute cycle budget across slices (a foreground call caps at 10 minutes).
  • The sensor subagent and its prompt template are removed from SKILL.md entirely; the v3 protocol semantics (status-primary polling, comment-stream fallback, init-pass already_settled / cr_failure / backlog-drain branches, settle conditions) carry over.
  • Pure decision logic lives in scripts/sensor-poll-lib.sh with bats coverage: 18 unit tests plus 10 stubbed-gh integration tests over every init-pass branch.
  • Hardening from the pre-landing review (5 toolkit lenses + codex cross-model pass): a terminal transition is never marked consumed before its comment streams are fetched (a single failed fetch was silently losing the round), unified failure accounting with an outcome: error threshold, init-pass retry tolerance, captured gh stderr in error_message, atomic/validated state and baseline reads, full-schema JSON on bootstrap/arg errors, and robust gh/jq resolution under Claude Code's stripped PATH (the incident's poll script died on a hardcoded /opt/homebrew/bin/jq).
  • eng plugin bumped to 2.7.0; stale sensor-subagent comments in the ship-watch-nudge hook scripts refreshed.

QA

📄 Plan view: https://claude.ai/code/artifact/d4780d67-94c1-46a8-9615-8c99dce6db0c

QA driver: Claude, the building agent (this session) - every Dev row is scriptable from this machine right now
Standard (all green): unit tests · lint/types · CI · /eng:cr

🖥️ Development

Before merge we prove the new watch script itself: unit-test its decision rules, then point it at real finished pull requests and watch it come back with one clean JSON answer in seconds instead of parking forever.

Tester Check Expect Notes
[x] claude Run bats eng/skills/pr-watcher/tests/sensor-poll.bats (single file) All tests pass Covers settle conditions, baseline filtering, the null last-terminal transition edge, and JSON assembly
[x] claude Run scripts/sensor-poll.sh against merged PR mujtaba3B/email-hero#79 Exactly one JSON on stdout, outcome pr_closed, returns in under 30s Foreground Bash, temp state dir; no background task, no Monitor
[x] claude Init pass against an open PR whose CodeRabbit status on HEAD is terminal success, with all CR items baselined Outcome already_settled immediately, no polling loop entered Candidate PR picked at QA time; baselines seeded into a temp state dir
[x] claude Slice continuation: two short slices, then an exhausted total budget Slice 1 emits outcome continue; slice 2 resumes cumulative ticks/elapsed from sensor-state.json; exhausted budget emits idle_timeout Run with tiny --slice-seconds / --total-seconds against an open PR with no fresh CR activity
[x] claude Stripped-PATH run: env -i PATH=/usr/bin:/bin Script still resolves gh/jq and emits valid JSON Regression guard for the hardcoded /opt/homebrew/bin/jq failure observed in the incident
[x] claude Contract sweep of the rewritten SKILL.md No sensor-subagent spawn remains (no subagent_type, no sensor prompt template); dispatcher loop calls the script in foreground Confirms the parked-agent failure class is designed out, not just discouraged

🚀 Production

After merge we reinstall the plugin so Claude actually runs the new copy, then watch it handle a real CodeRabbit review round without ever getting stuck.

Tester Check Expect Notes
claude After merge: run bin/install, then diff the plugin-cache copy against merged main Cache eng/2.7.0 carries skills/pr-watcher/scripts/sensor-poll.sh byte-identical to the repo Layer-walk: skills execute from the plugin cache, not the repo; the cache goes stale until bin/install re-runs, so this row is the refresh AND its verification
mujtaba On the next real /eng:pr-watcher run on a live PR, observe one full CodeRabbit round The settled round reaches the dispatcher within ~20s of CodeRabbit's terminal commit status; zero parked waits Evidence: watch transcript timestamps vs the CR status updated_at

Production artifacts: the plugin cache copy ~/.claude/plugins/cache/gstack-extensions/eng/2.7.0/skills/pr-watcher/ (SKILL.md + scripts/sensor-poll.sh) on Mujtaba's laptop, exercised by invoking /eng:pr-watcher; refreshed only by bin/install (prod row 1 performs and verifies that refresh). The eng plugin version is bumped to 2.7.0 in this PR so the cache path is machine-matchable.

Definition of Done:

  • Tests written and green
  • Independent local review clear (/eng:cr) + CodeRabbit addressed
  • Docs updated where user-facing
  • where-things-run.json bumped if the deploy changed hosts

QA posture: Pre-merge, state QA_STATUS: dev_verified plus EVIDENCE: once every Dev QA box and every Definition-of-Done bullet is checked. Post-deploy, state QA_STATUS: prod_verified plus EVIDENCE: once the Prod QA rows are verified live.

Summary by CodeRabbit

  • New Features

    • Added a deterministic PR-watching sensor that runs in foreground slices, outputs exactly one structured JSON outcome per run, resumes mid-cycle after timeouts, and handles settled, failure, idle-timeout, and error states.
    • Enhanced backlog draining and quiet-period/settlement detection for more accurate decisions.
  • Documentation

    • Updated watcher skill documentation and changelog to reflect the new polling workflow and state handling.
  • Tests

    • Added unit and integration coverage for polling outcomes, settlement logic, error payloads, and state recovery.
  • Chores

    • Updated ignore rules for local watcher state, and bumped the plugin version.
    • Refreshed watcher hook comments for clarity.

mujtaba3B and others added 4 commits July 20, 2026 17:30
sensor-poll.sh implements the whole /eng:pr-watcher sensing protocol
(init pass, status-primary 15s loop, comment-stream fallback, settle
conditions, slice/total budgets) and prints exactly one JSON object per
invocation. Pure decision logic lives in sensor-poll-lib.sh with bats
coverage in tests/sensor-poll.bats.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrite the SKILL.md sensing contract: the dispatcher now runs
scripts/sensor-poll.sh in foreground Bash slices (continue outcome +
sensor-state.json span the 30-minute budget) instead of spawning a
general-purpose sensor subagent with a prompt template. The subagent
contract was structurally unsatisfiable (foreground sleep blocked;
background tasks and Monitor end the turn), which parked the watcher
after CodeRabbit had already finished on email-hero PR 79 (2026-07-20).
Adds continue/error outcomes to the dispatcher branch table and updates
the failure table, state layout, and closing rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review batch (5 toolkit lenses + codex cross-model pass):
- Blocker: do not mark a terminal transition consumed until the comment
  streams are fetched; a single failed fetch was permanently losing the
  settled round to idle_timeout, the exact incident class this PR fixes.
- Unify failure accounting in a fail_tick helper; stream-only failures now
  count toward the error threshold (reset only after a fully clean tick).
- Init pass retries 3x before outcome error instead of failing on the
  first blip; failure-table docs now match.
- Capture gh stderr into error_message (makes the 401 dispatcher branch
  implementable); atomic save_state + validated load_state and baseline
  reads so corrupt files re-init instead of breaking the one-JSON
  contract; bootstrap and arg errors emit full-schema JSON.
- PATH append instead of prepend: still fixes the stripped-PATH case but
  lets a test stub win, enabling a stubbed-gh integration bats file
  covering every init-pass branch (10 new tests; 28 total green).
- sp_all_quiet: timestamp-less or unparseable items block quietness
  instead of counting as vacuously quiet.
- Refresh stale sensor-subagent comments in the ship-watch-nudge scripts;
  correct the CHANGELOG claim about the broadened fallback marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mujtaba3B mujtaba3B added the bug Something isn't working label Jul 20, 2026
@mujtaba3B mujtaba3B self-assigned this Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR watcher is migrated from a sensor subagent to a deterministic foreground polling script. Shared Bash decision logic, persisted slice state, expanded outcomes, dispatcher documentation, hooks, release metadata, and Bats tests are added or updated.

Changes

PR watcher sensor migration

Layer / File(s) Summary
Sensor decision logic and contracts
eng/skills/pr-watcher/scripts/sensor-poll-lib.sh, eng/skills/pr-watcher/tests/sensor-poll.bats
Adds status selection, new-item filtering, settlement detection, quiet-period checks, fingerprints, JSON emission, and unit coverage.
Foreground sensor polling and persistence
eng/skills/pr-watcher/scripts/sensor-poll.sh
Adds initialization and 15-second polling loops, persisted slice state, GitHub error handling, terminal outcomes, and continuation slices.
Sensor integration validation
eng/skills/pr-watcher/tests/sensor-poll-integration.bats
Covers stubbed GitHub responses, terminal outcomes, backlog handling, state recovery, pagination, CLI errors, and API failures.
Dispatcher integration and watcher lifecycle
eng/skills/pr-watcher/SKILL.md, eng/hooks/scripts/*, eng/skills/pr-watcher/CHANGELOG.md
Documents foreground sensor execution, outcome routing, clean exits, failure handling, transient state, and the v4 lifecycle.
Plugin and repository metadata
eng/.claude-plugin/plugin.json, .gitignore
Bumps the plugin version to 2.7.0 and ignores .gstack/ except for the required QA recipe path.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dispatcher
  participant SensorPoll
  participant SensorState
  participant GitHub
  Dispatcher->>SensorPoll: Run foreground sensor slice
  SensorPoll->>SensorState: Load or save slice progress
  SensorPoll->>GitHub: Fetch PR, statuses, and review streams
  GitHub-->>SensorPoll: Return current review data
  SensorPoll-->>Dispatcher: Emit one JSON outcome
  Dispatcher->>SensorPoll: Rerun when outcome is continue
Loading

Possibly related PRs

Poem

Foreground sensors wake,
Slices persist, errors break.
JSON flows in measured streams,
Tests patrol the watcher’s dreams.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: pr-watcher v4 now uses a deterministic script instead of a subagent.

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

@mujtaba3B

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 3

🤖 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 @.gitignore:
- Line 28: Update the .gitignore entry for .gstack/ so the directory remains
ignored while explicitly re-including .gstack/qa-quincey/recipe.yml for version
control. Ensure Git’s ignore rules allow both the required directory path and
recipe file to be tracked.

In `@eng/skills/pr-watcher/scripts/sensor-poll.sh`:
- Around line 48-60: Update the argument parsing loop around the case branches
for --owner, --repo, --pr, --state-dir, --slice-seconds, and --total-seconds to
validate that a value argument exists before reading "$2". Route missing values
through arg_error so the script preserves its single-JSON stdout contract
instead of triggering a set -u unbound-variable failure; keep the existing
required-option validation unchanged.
- Around line 140-148: Update fetch_streams to use gh api pagination for the
issue_comments, reviews, and review_comments requests, preserving the existing
error handling and filtering flow. Add since parameters to the issue and review
comment endpoints using the appropriate baseline timestamp or established state,
while retaining per_page=100, so all new feedback remains available to
sp_filter_new.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9fdccbca-a647-4b5b-a481-1786446cc1d9

📥 Commits

Reviewing files that changed from the base of the PR and between 8d9733d and e698332.

📒 Files selected for processing (10)
  • .gitignore
  • eng/.claude-plugin/plugin.json
  • eng/hooks/scripts/ship-watch-nudge-lib.sh
  • eng/hooks/scripts/ship-watch-nudge.sh
  • eng/skills/pr-watcher/CHANGELOG.md
  • eng/skills/pr-watcher/SKILL.md
  • eng/skills/pr-watcher/scripts/sensor-poll-lib.sh
  • eng/skills/pr-watcher/scripts/sensor-poll.sh
  • eng/skills/pr-watcher/tests/sensor-poll-integration.bats
  • eng/skills/pr-watcher/tests/sensor-poll.bats

Comment thread .gitignore Outdated
Comment thread eng/skills/pr-watcher/scripts/sensor-poll.sh
Comment thread eng/skills/pr-watcher/scripts/sensor-poll.sh
mujtaba3B and others added 3 commits July 20, 2026 17:53
…ack/

Comment: #58 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rvives the machine-global ignore

Comment: #58 (comment)

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

Both sensor-poll.sh findings from the same round (shared file, one commit):
a missing flag value now routes through arg_error instead of crashing past
the one-JSON contract, and fetch_streams follows all pages via gh api
--paginate (page 1 is oldest-first, so 100+ item streams went blind to new
feedback) with jq -s add flattening the per-page arrays.

Comment: #58 (comment)
Comment: #58 (comment)

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

@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: 2

🤖 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 `@eng/skills/pr-watcher/tests/sensor-poll-integration.bats`:
- Around line 123-127: Update the test “flag with no value emits one error JSON,
never a set -u crash” to parse all output values with jq -s, assert the
resulting array length is exactly 1, then validate the sole error payload’s
outcome and error_message fields.
- Around line 16-26: The gh stub currently ignores whether callers pass
--paginate, so the pagination test cannot detect its removal. Update the stub’s
endpoint matching in the heredoc and the pagination test setup around the
relevant polling test to require --paginate for the paginated request, while
preserving existing fixture responses and failure behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 590fd0fe-97b8-48ac-a6a7-7b523af18755

📥 Commits

Reviewing files that changed from the base of the PR and between 8922d65 and 3a1f2eb.

📒 Files selected for processing (3)
  • .gitignore
  • eng/skills/pr-watcher/scripts/sensor-poll.sh
  • eng/skills/pr-watcher/tests/sensor-poll-integration.bats

Comment thread eng/skills/pr-watcher/tests/sensor-poll-integration.bats Outdated
Comment thread eng/skills/pr-watcher/tests/sensor-poll-integration.bats
…tub; assert single JSON value)

Same round, same file, one commit: the gh stub now refuses stream fetches
that lack --paginate (dropping the flag fails the suite), and both arg-error
tests parse stdout with jq -s asserting exactly one JSON value.

Comment: #58 (comment)
Comment: #58 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mujtaba3B
mujtaba3B merged commit f26a6f8 into main Jul 20, 2026
1 of 2 checks passed
@mujtaba3B
mujtaba3B deleted the fix/pr-watcher-deterministic-sensor branch July 20, 2026 23:10
mujtaba3B added a commit that referenced this pull request Jul 20, 2026
…audited operator bypass (#59)

* feat(eng): detect CodeRabbit's rate-limited-as-FAILURE status shape

mc_cr_failure_rate_limited answers one question: is a CodeRabbit commit
status of "failure" on HEAD really a rate limit rather than a genuine CR
objection? Two independent proofs, either sufficient: the status
description says rate limit (case-insensitive substring, so "Review rate
limited" and "Rate limit exceeded" both hit), or CR posted its existing
"rate limited by coderabbit.ai" marker comment.

This is the third rate-limit shape. The two the lib already had key on
the marker comment and cover a MISSING status (CR never started) and a
stuck PENDING one (CR started, then hit the limit mid-flight). Neither
covers what CR does when an INCREMENTAL pass burns the limit: it resolves
its per-commit status to failure with a rate-limit description and often
posts no marker comment at all.

Fails closed in every degraded direction (non-failure state, empty
description, unparseable comments), so a genuine CR failure is never
mistaken for a rate limit.

* v2.8.0 fix(eng): clear a rate-limited CR failure, and add an audited operator bypass

The gate reached its rate-limit helpers only for a missing or pending
CodeRabbit status, so a failure status blocked unconditionally. On #58 CR
had fully reviewed the PR and acked all five findings; its final
incremental pass over a test-only commit tripped the limit and posted
status=failure "Review rate limited". Every dimension rendered checked,
the verdict was NOT CLEAR on that one line, and landing took the
documented human workaround: move .merge-clearance.json aside, gh pr
merge --admin, restore. That workaround is what this retires.

Two paths clear a failure now, and BOTH require a current /eng:cr review
on the head, so "never both reviewers down" still holds:

- rate-limited failure: machine-detectable via mc_cr_failure_rate_limited
  (lazy status-description fetch, only on the failure branch, so the
  happy path makes no extra API call). Auto-satisfies with the backstop,
  no flag, exactly like the missing and stuck-pending shapes.
- genuine failure: the new --override-cr-failure flag. Human judgment, so
  it is never inferred. Recorded in the checklist line, the JSON verdict,
  the stamp evidence and the posted status description.

Also fixes the checklist mark: cr_mark ignored both the failure status
and the reviewed-head blocker, so a PR blocked solely by either rendered
CodeRabbit as a green tick while the verdict said NOT CLEAR. The one
dimension actually blocking was the one shown as passing.

pr-watcher's Step 4b/4h now name all three shapes in a table and tell the
dispatcher to read the status description itself (the sensor does not
carry it), so watcher and gate reach the same conclusion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(eng): close the stale-marker bypass, and make the CR-failure decision a tested unit

Review (Codex plus the silent-failure and comment lenses, all three
independently) found the shape-3 marker proof reused the LOOSE
mc_cr_rate_limited, which matches the marker anywhere in a PR's history.
So: CR is rate-limited on an early commit and posts its notice, later
recovers, reviews, and fails HEAD for real. The gate read the stale
marker, auto-cleared the genuine failure with no operator flag, and
labelled the audit trail "rate-limited". That is the exact trap
mc_cr_rate_limited_latest already exists to close for the stuck-pending
shape; shape 3 now gets the same discipline. Reproduced before the fix,
and pinned by a regression test.

Also from the review:

- the description classifier matched a bare "rate limit" substring, so
  "not a rate limit issue" qualified while the hyphenated "Rate-limited"
  did not. Now a positive rate[ -]?limit match with a negation guard. The
  residual (a trailing negation) is documented, not silently accepted.
- the whole CR-failure decision moves into a pure mc_cr_failure_disposition
  in the lib, with its truth table as bats cases. It was the one line
  standing between --override-cr-failure and a bare merge bypass, and it
  was verifiable only by hand. It also fixes attribution: the rate-limit
  check runs first, so passing the flag defensively on a rate-limited
  failure records "rate-limited", not a human override that never
  happened, and a later grep for real overrides stays clean.
- the checklist mark now reads a CR_BLOCKED flag set beside each blocker
  instead of re-deriving the same conditions in a second dialect. That
  mirroring is what produced #58; reading the blockers makes "green iff
  nothing is blocking" true by construction rather than by review.
- STATUS_DESC accumulates ordered notes and truncates at GitHub's ~140
  chars instead of last-wins overwrites. A docs-only PR cleared through a
  rate-limited CR failure posted only the bookkeeping note, dropping the
  security-relevant reason from the most durable audit surface while the
  local stamp still recorded it.
- the marker proof is now genuinely lazy (description first, comments only
  if inconclusive), the flag warns instead of silently no-opping on a
  non-failure status, and several comments that the new behavior had made
  false are corrected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mujtaba3B

Copy link
Copy Markdown
Owner Author

QA_STATUS: prod_verified
EVIDENCE: Prod row 2 verified live today (2026-07-20) during the CodeRabbit watch on PR #61: two full rounds sensed by scripts/sensor-poll.sh running in foreground Bash slices (byte-identical between merged main and plugin cache eng/2.8.0). Round 1 settled via status_transition after 172s of polling (CR status updated_at 2026-07-21T00:24:59Z), round 2 after 254s (updated_at 2026-07-21T00:30:26Z); both rounds reached the dispatcher in the same foreground call, zero parked waits, one JSON per invocation, and the dispatcher applied a fix, replied, and exited on the all-clear. Prod row 1 (bin/install + cache diff) re-confirmed today at eng/2.8.0. Confirmed by Mujtaba.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant