feat(pr-shepherd): org-wide PR staleness watcher with Slack escalation

Merged
KH
2mo ago
+1572-120

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.created events 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=true deploys evaluate and log but write nothing to Slack — used for the initial proof run
  • trajectory-pointer extractor 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

  1. 2393390 — initial scaffold: agent.ts + persona.ts with webhook/cron/relay-DM handlers
  2. 6343d62 — reply directly to relay sender via ctx.relay.dm(); Slack as fallback
  3. cba9e40 — remove pull_request.converted_to_draft and pull_request.ready_for_review (not in trigger registry); add pull_request.reopened
  4. 2e8bd87 — remove LOCAL_READ_ONLY flag — redundant with the no-channel guard + workspace-scoped memory dedup
  5. 272b0ca — add Slack @mention trigger + compile local persona.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-run entries 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 logs pr-shepherd.ledger.trajectory-pointer with non-null work_unit_id
  • Slack @mention: send @pr-shepherd what PRs are awaiting review? in C0BJL95JU8G; agent replies in-thread with ledger state
  • Relay DM: agentworkforce agent pr-shepherd opens 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=true re-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_ID check) and relay DMs that reply to the sender; local harness falls back to gh.
  • 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.reopened resets closedAt so reopened PRs re-enter evaluation.
    • Ensure Slack correctness: threading uses replyTo for rung 2; route Slack events before relay DMs to avoid drops; require SLACK_BOT_USER_ID for @mention handling; centralize helpers in shared/slack.ts and re-export from inbox-buddy to keep behavior consistent across agents.