0039 โ€” A PR's lifecycle reaches the session that opened it by push, not by poll

The problem

A session that opened a propflowai PR learned two things about it without polling, and only if it had armed review-sub by hand: the review verdict and the merge. Everything between and after was invisible. Did checks fail? Did auto-merge arm? Did the PR enter the merge queue, and was it kicked out of it, and why? And nothing in the fleet tracked the last stage at all: is this PR live in production? Under batched promotes (one production deploy per 30-minute window) that is 30 to 90 minutes after the merge, longer behind a red main. A session that wanted any of it had to poll gh, which review-turns forbids for good reason (shared API rate limits, and turns spent asking).

The decision

  1. The review-webhook listener follows every propflowai PR through its whole life and writes one status file per PR, ~/.claude/review-subs/status/<owner>-<repo>-<pr>.json, with the stage history and timestamps, whether or not anyone subscribed. review-sub status <pr> reads it. Stages and their sources:

    Stage Webhook Wakes the subscriber?
    OPENED, CHECKS_RUNNING pull_request opened / reopened / synchronize no, status file only
    CHECKS_FAILED check_suite completed, failure, whose head_sha is the PR's current head yes, once per head
    REVIEW the existing verdict gate the existing verdict wake, not a second one
    AUTO_MERGE_ARMED pull_request auto_merge_enabled yes
    QUEUED pull_request enqueued yes
    KICKED_OUT_OF_QUEUE pull_request dequeued, reason not MERGE/ALREADY_MERGED yes, with GitHub's reason
    MERGED pull_request closed + merged the existing merge wake
    DEPLOYED_TO_PRODUCTION deployment_status success, environment Production yes: "PR #N is live in production as of
  2. "Live in production" is the GitHub Deployment vercel[bot] records for ci.yml's vercel-promote. Measured 2026-10-02: db36814abc's Production deployment got success at 19:05:59Z and the promote job logged READY โ€” production serves db36814abc at 19:06:11Z, same Vercel URL. It is also prod-freshness-alarm.yml's own fallback definition of what production serves. The PRs that went live are the (#N) squash subjects between the previous production SHA and the new one, from one compare call per deploy. A deployment that is not ahead of the last one (a late status, a rollback) moves nothing. Only the web is tracked; the Lambdas and renewal worker deploy after it in the same job.

  3. A session is subscribed by its own gh pr create. A PostToolUse hook sees the PR URL in the command's output and runs review-sub arm --auto (36 h TTL, because the production stage lands long after the 2 h default would expire). Inside a sub-agent it records agentId and wakeIsProxy: true: the wake lands on the Driver's pane, which is the only pane there is. A later manual re-arm never shortens it.

    A sub-agent's PR wakes its PARENT, and that is the decision, not a defect (settled 2026-10-02 after the first live run: #15707's ๐ŸŸก and CHECKS_FAILED wakes landed in the coordinator that spawned the lane). A sub-agent has no pane and no turn to resume, so nothing can type into it, and the parent is the session that owns its lanes' PRs (ADR-0030: wake the Driver that owns it). What was missing was the routing fact, so the wake text now ends (opened by your sub-agent <id>: forward this to it with SendMessage) (dotclaude#146). Rejected: arming nothing inside a sub-agent (the lane's PR would then be heard by nobody) and waking the lane directly (there is no channel into an Agent-tool sub-agent except its parent's SendMessage).

  4. One wake per stage change, never per tick. Dedupe keys live in the status file (stage plus head sha, merge sha or the PR's updated_at), so a GitHub redelivery or a listener restart cannot re-announce a stage. Stage wakes for one PR are coalesced over 30 s, because enqueue and auto-merge-armed arrive seconds apart. The production wake bypasses the per-PR wake cap, as the merge wake already does.

Why this shape

Relation to other ADRs

Consequences

Amendment 2026-10-07: wake only when there is something to do

Gera, 2026-10-07: "turn off all of them except for merge โ€ฆ or if it failed and we need to make some edits." WAKE_STAGES is now CHECKS_FAILED and KICKED_OUT_OF_QUEUE only. AUTO_MERGE_ARMED, QUEUED and DEPLOYED_TO_PRODUCTION are still recorded in the status file (review-sub status <pr>) but wake nobody. The merge wake and the review-verdict wake are separate paths and unchanged. Live in ~/.claude/services/review-webhook/listener.js (local commit 1e17648); the stage table above reads "yes" for those three stages and is superseded by this amendment.

Amendment 2026-10-08: a passing review wakes nobody

Gera, 2026-10-08, on still being pinged: "we only want to have when there's an error we need to fix something, or when we merge." A review verdict of APPROVED or NO_REVIEW_NEEDED no longer wakes a session (QUIET_VERDICTS); the signal file is still written. A review asking for changes (๐ŸŸก, ๐Ÿ”ด, CHANGES_REQUESTED) still wakes, as "need to fix something". Because an approval no longer wakes, a session arms pr-merge when it opens the PR rather than on the approval; the merge wake follows. Live in ~/.claude/services/review-webhook/listener.js (local commit 731453a).

Amendment 2026-10-22 โ€” a repeat crash wakes again, and a silent PR re-wakes

propflowai #21732 and #21733 sat ~4h: the review job crashed twice at one head and only the first crash woke anyone. A GitHub Actions rerun reports under the SAME check_suite id (run 39752869336, run_attempt 3, one suite), and the CHECKS_FAILED key was head-only, so the second failure was already in history. Gera approved three fixes:

  1. Key (dotclaude#285): CHECKS_FAILED@<head12>#<suiteId>@<updated_at>. A repeat failure is fresh; a redelivery of the same completion is still deduped; older heads still ignored; the 30 s coalescing still makes a burst one wake.
  2. The PR says so (propflowai#21751): ci-followups' handle-review-infra-failure (retry once, then a check-run plus one marker comment) was gated on workflow_run.name, which is the run-name (the PR title) for claude-code-review.yml, so it had skipped every crash since 2026-10-01. Now gated on workflow_run.path.
  3. Silence (dotclaude#286): a webhook cannot see a PR that stops moving, so a 10-minute sweep over the status files re-wakes subscribers with STUCK(<stage>) when a PR has sat on failed checks or a fixes-needed verdict for 60 min, GitHub confirms it is open at the same head with nothing running, at most hourly and 6 times per episode. This is the one place the listener polls anything, and it polls its own files; GitHub is asked only for a PR that would otherwise wake.

Amendment โ€” 2026-10-29: superseded in part by ADR-0076

ADR-0076 supersedes ยง3's "a sub-agent's PR wakes its PARENT, and that is the decision" and the typed delivery mechanics of the 2026-10-07/08 amendments: a worker now waits on its own signal file and the harness resumes it; owners get coalesced notices (one per owner per 30 s across PRs; a deploy is one line); nothing is typed into a pane; quiet holds everything. The stage table and the dedupe rule (keys in the status file, ยง4) stand. Appended rather than inserted so every line-number citation into this file stays true (ADR-0023 decision 6).