feat(pr-shepherd): org-wide PR staleness watcher with Slack escalation
Merged
KH
2mo ago+1572+1572+0123456789012345678901234567890123456789-120-120-012345678901234567890123456789
Summary
Adds pr-shepherd, a cloud persona that watches every open PR org-wide, classifies stale ones into four named bins, and escalates via Slack with a per-rung dedupe key.
What it does
- Webhooks maintain a ledger —
pull_request.*,pull_request_review.*,check_run.completed,issue_comment.createdevents update per-PR state as they land - Hourly cron evaluates the ledger — staleness is detected from stored timestamps, not a fresh 300-PR crawl
- First-tick backfill — on the first cron tick, a REST crawl seeds the ledger from all currently-open PRs (webhooks only cover PRs that move after listening starts)
- Slack escalation ladder — rung 1 posts a header, rung 2 replies in-thread via
replyTo, rung 3 DMs Chief; each rung is deduped by{owner}/{repo}/{prNumber}/{bin}/{rung} - Four staleness bins with configurable thresholds:
awaiting-review(3d),awaiting-author(2d),ci-red(1d),abandoned(14d) - Slack @mention chat — responds to direct @mentions in the configured Slack channel
- Relay DM chat — responds to relay inbox messages; same workspace-scoped memory as the cloud instance
Architecture decisions
- Cloud is the sole writer — local instances share the same workspace-scoped memory and can answer queries but never fire the escalation ladder (prevents the AR-448 double-ping defect)
- Ledger in workspace-scoped memory (not Relayfile projection — found a 3-day-stale projection reporting lag=0 during design; see workstream for details)
DRY_RUN=truedeploys evaluate and log but write nothing to Slack — used for the initial proof runtrajectory-pointerextractor parses<!-- trajectory: work_unit_id={id} work_unit_surface={surface} session_ref={uuid} -->from PR bodies — a preparation hook for the trajectory pipeline; never overwrites an existing non-null value
Commits
2393390— initial scaffold: agent.ts + persona.ts with webhook/cron/relay-DM handlers6343d62— reply directly to relay sender viactx.relay.dm(); Slack as fallbackcba9e40— removepull_request.converted_to_draftandpull_request.ready_for_review(not in trigger registry); addpull_request.reopened2e8bd87— removeLOCAL_READ_ONLYflag — redundant with the no-channel guard + workspace-scoped memory dedup272b0ca— add Slack @mention trigger + compile localpersona.json
Deployed
Already running as daeafe10-ada4-4de6-8d02-c3fa8188099a on workspace rw_7ccfea89 in dry-run mode (DRY_RUN=true, no SLACK_CHANNEL). Webhooks live. First cron tick seeds the ledger.
Test plan
- First hourly cron tick: logs appear with
pr-shepherd.evaluate.dry-runentries listing bins, reasons, and rung 1 targets — nothing posted to Slack - Trajectory pointer proof: stamp
<!-- trajectory: work_unit_id=X work_unit_surface=linear session_ref=Y -->on a PR body; next tick logspr-shepherd.ledger.trajectory-pointerwith non-nullwork_unit_id - Slack @mention: send
@pr-shepherd what PRs are awaiting review?inC0BJL95JU8G; agent replies in-thread with ledger state - Relay DM:
agentworkforce agent pr-shepherdopens local harness with same ledger visible
🤖 Generated with Claude Code
Summary by cubic
Adds pr-shepherd, an org-wide PR staleness watcher that classifies stale PRs and escalates in Slack with safe dedupe and proper threading. Also centralizes Slack helpers in shared/slack.ts (with an inbox-buddy re-export) for consistent parsing, channel guard, and replies.
-
New Features
- Cloud ledger from GitHub webhooks; hourly cron classifies staleness; one-time backfill seeds from open PRs;
FORCE_BACKFILL=truere-runs the crawl. - Four bins with configurable thresholds (awaiting-review, awaiting-author, ci-red, abandoned) and rung-2 timing via
ESCALATION_RUNG2_MULTIPLIER. - Slack escalation ladder with per-rung dedupe: rung 1 header + threaded body, rung 2 thread reply, rung 3 Chief DM.
- Ledger-grounded Q&A: Slack @mention (channel guard, strict
SLACK_BOT_USER_IDcheck) and relay DMs that reply to the sender; local harness falls back togh.
- Cloud ledger from GitHub webhooks; hourly cron classifies staleness; one-time backfill seeds from open PRs;
-
Bug Fixes
- Deduplicate ledger snapshots and always load the newest to prevent stale state and double counting.
- Authenticate backfill with GitHub App tokens; mark “backfill done” only on success so failures retry.
- Aggregate CI across
check_run.completed(any failing check →ci-red);pull_request.reopenedresetsclosedAtso reopened PRs re-enter evaluation. - Ensure Slack correctness: threading uses
replyTofor rung 2; route Slack events before relay DMs to avoid drops; requireSLACK_BOT_USER_IDfor @mention handling; centralize helpers inshared/slack.tsand re-export frominbox-buddyto keep behavior consistent across agents.