ADR-311: PR event detector and watch_pr_poll.py - #82
Conversation
build-and-test: Python test resultsStatus: ✅ Passed Test log |
jodavis-claude
left a comment
There was a problem hiding this comment.
Reviewed against ADR-311's exit criteria in _spec_ConcurrentDevelopment.md. All six checklist items are met: detect_pr_events covers all five event types with correct no-refire guards, dependency_merged reports the actual merge target (including the stacked/out-of-order case), watch_pr_poll.py's loop/timeout mechanics are fully unit-tested with an injectable clock/sleep and no real wall-clock delay, and multiple simultaneous events are returned together. 193 tests pass in the scripts directory. Two fault-tolerance gaps below, both around unhandled gh output — worth addressing before merge since detect_pr_events is meant to run unattended inside watch_pr_poll.py's bounded polling loop, where a single transient gh hiccup shouldn't hard-crash the whole window.
Address PR #82 review comment: _pr_state_and_base()'s `gh pr view` call and the review-comments `gh api` call both parsed result.stdout with a bare json.loads(...), unlike the ci_failure check right below which already wraps the same class of gh parse in try/except json.JSONDecodeError. A transient gh failure (rate limit, network blip, auth hiccup) raised an unhandled JSONDecodeError out of detect_pr_events, which would kill watch_pr_poll.py's whole bounded polling window instead of skipping that one check and retrying next iteration. Wrap both parses the same way the CI-checks call already is, so a flaky gh call degrades to "no signal this round" rather than a crash. Added tests: review comments and gh pr view (both the task's own call and a dependency's) returning non-JSON no longer raise, and the corresponding signal is simply skipped for that call.
Address PR #82 review comment: `_PR_URL_RE.match(ctx.pr_url).groups()` raised a confusing `AttributeError: 'NoneType' object has no attribute 'groups'` when ctx.pr_url didn't match the expected `https://github.com/<owner>/<repo>/pull/<number>` shape, instead of a clear diagnostic naming the offending value. Extracted the match into a `_parse_pr_url()` helper that raises `ValueError(f"pr_url does not match expected GitHub PR URL format: {pr_url!r}")` on no match, so detect_pr_events still fails loudly (this isn't a transient gh flake, unlike the JSON parse issue fixed in the previous commit) but with a message a human can act on when this runs unattended inside watch_pr_poll.py's polling loop. Added a test asserting a malformed pr_url raises ValueError naming the URL.
jodavis-claude
left a comment
There was a problem hiding this comment.
Sign-off review
Both prior review threads are addressed and resolved:
- Unhandled
json.JSONDecodeErroronghparses (_pr_state_and_base, review-comments
gh apicall) — fixed in9fbf869. Both call sites now degrade to "no signal this round"
on a transientghfailure instead of raising and killingwatch_pr_poll.py's whole
bounded polling window, matching the existing CI-checks pattern. Covered by 3 new tests
(own pr-view, dependency pr-view, review-comments, each returning non-JSON output). - Unguarded
.match(...).groups()onctx.pr_url— fixed ine477f16. Extracted a
_parse_pr_url()helper that raises a clearValueErrornaming the malformedpr_url
instead of a confusingAttributeErroronNone. Intentionally still raises (a malformed
pr_urlis a control-plane bug, not a transient flake), just with an actionable message.
Covered byTestDetectPrEventsMalformedPrUrl.
Confirmed both fix commits are minimal and scoped exactly to the two flagged lines (diff
1916e24..e477f16 touches only pr_event_detector.py and its test file — watch_pr_poll.py
untouched, as the fix summary claims). Full regression suite is green: 197 passed
(python3 -m pytest in plugins/dev-team/skills/workflow-orchestrate/scripts/).
No new Priority 1-5 issues found in the files modified since the last review pass.
Approved.
jodavis-claude
left a comment
There was a problem hiding this comment.
Sign-off review — ADR-311 (fix cycle 2)
Decision: Approved.
Prior review threads
Both PR review threads are confirmed isResolved: true on GitHub:
- Unhandled
json.JSONDecodeErroronghparses (pr_event_detector.py L31/L67) — fixed in9fbf869, verified with 3 new regression tests. - Unguarded
_PR_URL_RE.match(...).groups()on malformedpr_url— fixed ine477f16via a new_parse_pr_url()helper raising a clearValueError.
No further action needed on either thread.
This cycle's change (commit 2271a2a)
Closed the sign-off validation gap flagged in the prior cycle: the "no false re-fires on an already-seen item" exit criterion was only demonstrated for review_comment, ci_failure, and dependency_merged, not base_updated/task_merged.
- Added
base_sha_unchanged_no_refiretoTestDetectPrEventsBaseUpdated's parametrize list — assertsbase_updateddoes not fire whengit ls-remotereports the same sha already recorded inbase_branch_sha. Verified this correctly exercises the existingelif current_sha != stored_sha:guard inpr_event_detector.py. - Added
TestDetectPrEventsTaskMergedCalledAgain— documents (rather than changes) the accepted design limitation thattask_mergedhas no persisted "already reported" field and fires on every call while the PR stays merged, since the brief'sextra_frontmatterfield list doesn't name one for it. Reasonable call — inventing a new field here would be scope creep for a signoff-driven fix cycle. - Both new tests pass against the unmodified implementation; no production code was touched this cycle (test-only diff, 53 lines added).
- Full regression: 199 passed locally, matching the commit's own report. Re-ran independently: 199 passed, 0 failed.
gh pr checks 82:build-and-test: pass,gate: pass.
No new Priority 1–4 issues found in the modified file. All five exit-criteria-mapped event types now have explicit individual-firing, multi-firing, and no-refire test coverage. Nice work closing this out cleanly with a minimal, test-only diff.
…y covered — all five event types (review_comment, ci_failure, base_updated, dependency_merged, task_merged) firing individually and in combination, no-refire guards for review_comment/ci_failure/dependency_merged, the quiet-call no-op case (with unchanged saved context), the stacked out-of-order dependency_merged branch-reporting case, early-return guards (no context file / no pr_url), and all boundary/error inputs from the exit criteria (non-JSON checks output, missing base_branch, missing/unparseable spec_path, dependency-less task, dependency with no context file or no pr_url, and a failed git ls-remote lookup))
… covered — `poll()`'s immediate-fire branch (single and multiple simultaneous events, no sleep), the loop-until-fired branch (sleeps between empty checks, exits before `max_seconds`), the `max_seconds`-elapsed → `"no_change"` branch, and the `max_seconds=0` boundary (zero sleeps); `main()`'s CLI wrapper for both success shapes (fired-event-list and `"no_change"` JSON) and the exception → `Error: ...`/exit-1 path. All 8 tests green.)
Address PR #82 review comment: _pr_state_and_base()'s `gh pr view` call and the review-comments `gh api` call both parsed result.stdout with a bare json.loads(...), unlike the ci_failure check right below which already wraps the same class of gh parse in try/except json.JSONDecodeError. A transient gh failure (rate limit, network blip, auth hiccup) raised an unhandled JSONDecodeError out of detect_pr_events, which would kill watch_pr_poll.py's whole bounded polling window instead of skipping that one check and retrying next iteration. Wrap both parses the same way the CI-checks call already is, so a flaky gh call degrades to "no signal this round" rather than a crash. Added tests: review comments and gh pr view (both the task's own call and a dependency's) returning non-JSON no longer raise, and the corresponding signal is simply skipped for that call.
Address PR #82 review comment: `_PR_URL_RE.match(ctx.pr_url).groups()` raised a confusing `AttributeError: 'NoneType' object has no attribute 'groups'` when ctx.pr_url didn't match the expected `https://github.com/<owner>/<repo>/pull/<number>` shape, instead of a clear diagnostic naming the offending value. Extracted the match into a `_parse_pr_url()` helper that raises `ValueError(f"pr_url does not match expected GitHub PR URL format: {pr_url!r}")` on no match, so detect_pr_events still fails loudly (this isn't a transient gh flake, unlike the JSON parse issue fixed in the previous commit) but with a message a human can act on when this runs unattended inside watch_pr_poll.py's polling loop. Added a test asserting a malformed pr_url raises ValueError naming the URL.
Sign-off validation flagged one exit criterion as only partially covered: review_comment, ci_failure, and dependency_merged each had an explicit already-seen/no-refire test, but base_updated and task_merged did not. - Added a base_sha_unchanged_no_refire case to TestDetectPrEventsBaseUpdated: asserts base_updated does not fire when git ls-remote reports the same sha already recorded in base_branch_sha. - Added TestDetectPrEventsTaskMergedCalledAgain: documents that task_merged fires on every call for as long as the PR stays merged, since (per the task brief's stated context-file fields) there is no persisted "already reported" field for it, unlike the other four events. This is an accepted design limitation, not a defect — adding a new field would be scope creep beyond the brief's named extra_frontmatter fields (base_branch_sha, last_seen_review_comment_id, last_seen_ci_conclusion, base_branch). Both new tests pass against the existing implementation unmodified — pr_event_detector.py already behaves correctly for both cases; this commit only closes the test-coverage gap the signoff review found. Full regression: 199 passed (197 pre-existing + 2 new).
2271a2a to
5d4c6b4
Compare
Work item
ADR-311: Implement the PR event detector (
detect_pr_events) and the bounded, self-looping poll scriptwatch_pr_poll.py— the detection half of the post-hand-off PR monitor (dev-team:watch-pr, ADR-313) — so a future monitor can learn what changed on a task's PR (new review comment, CI failure, base branch moved, a dependency's PR merged, or the task's own PR merged) without needing to poll GitHub directly itself.Changes
plugins/dev-team/skills/workflow-orchestrate/scripts/pr_event_detector.py— implementsdetect_pr_events(task_work_item_id) -> list[Literal["review_comment", "ci_failure", "base_updated", "dependency_merged", "task_merged"]]. Loads the task's context file viaPipelineContext.load(); returns[]immediately with noghcalls if the context file doesn't exist orpr_urlis empty. Detects all five event types by shelling out togh/gitviasubprocess.run(never MCP), updatingbase_branch_sha,last_seen_review_comment_id, andlast_seen_ci_conclusiononly for events actually returned.dependency_mergedreports the actual branch a dependency's PR merged into (not a hardcoded feature branch), covering the stacked/out-of-order case where a dependency's PR merges into another still-open task's branch.plugins/dev-team/skills/workflow-orchestrate/scripts/watch_pr_poll.py— implementspoll(task_work_item_id, max_seconds=480, sleep=time.sleep, clock=time.monotonic) -> list[str] | Literal["no_change"], looping the detector on a 30s interval with an injectable sleep/clock for fully simulated unit tests. Returns as soon as any event fires; returns"no_change"once the bounded window elapses with nothing new. Includes a thin CLImain()(argparse, JSON to stdout,Error: ...+ exit 1 on failure).test_pr_event_detector.py(20 tests) andtest_watch_pr_poll.py(8 tests), covering each event type individually and in combination, no-refire guards, early-return guards, and boundary/error inputs — all via mockedsubprocess.run/ realtmp_path-backed context files, no real GitHub or wall-clock calls.Design decisions
gh api repos/<owner>/<repo>/pulls/<number>/comments), the semantically precise match forlast_seen_review_comment_idand for actual reviewer feedback — not general conversation comments.last_seen_ci_conclusionresolved to an aggregate value ("failing"/"pending"/"passing"/"") derived fromgh pr checks --json bucket;ci_failurefires only on a transition into"failing". Documented limitation: a failing→passing→failing (different check) sequence won't re-fire, since the field is only updated when an event fires — out of scope for this task's exit criteria.dependency_mergedis resolved via the spec'sDepends on:graph (reusingtask_dependencies.parse_task_dependencies()and each dependency's own context file), not by reverse-matchingbase_branchagainst another task'sworking_branch.dependency_mergedderives entirely from the existingbase_branchfield (no new context-file field): once fired,base_branchis updated to the merge target, so the next call's comparison naturally stops matching.base_branch_shaobservation records a baseline without firingbase_updated(no prior baseline to diff against).Testing completed
python3 -m pytestinplugins/dev-team/skills/workflow-orchestrate/scripts/— 193 passed, no existing tests broken.dev-team:watch-pr, is ADR-313, out of scope here).