This dashboard is a maintainer aid, not a transactional notification system. Some rare timing and notification edge cases are intentionally accepted to keep the implementation understandable and operationally cheap.
- Scheduled backfills run from
open-telemetry/shared-workflowsinstead of each target repository hosting its own workflow. - Target repositories only need GitHub App access and an entry in
repositories.json. - The top-level workflow validates targets and enqueues work. Independent live and maintenance drains process up to four repositories concurrently; each repository remains serial within a wave. A failure in one repository does not prevent the others from processing.
- Calculated state and delivery intent for each target repository live on a
worker-owned branch under
otelbot/pull-request-dashboard-state/<repository>, with files still namespaced by repository name inside the branch. - Publisher receipts and rollout cursors live on a separate branch under
otelbot/pull-request-dashboard-delivery/<repository>. Publishers read a detached snapshot of accepted worker state and never push to the worker branch. - The combined processor renders markdown from accepted dashboard state and the target repository's current open PR list.
- The dashboard issue is discovered dynamically by title and label, so target repositories do not need to store issue numbers in config.
- Refresh events that carry no pull request number report the head commit
instead. GitHub omits the pull request association from check and status
events whose head branch lives in a fork, which is where nearly every
contribution comes from, so without this the CI columns of those PRs would
only refresh on the hourly backfill. The webhook bridge cannot resolve the
commit itself, because its GitHub App is installed only on
shared-workflows. - Both channels' processors paginate each target repository's open PRs and compare head SHAs. The commit-to-PR endpoint can omit fork heads or other PRs sharing the commit. Each matching open PR is refreshed before a head-SHA claim is acknowledged; a head with no open PR needs no refresh.
- Dashboard changes reach repositories in two stages. Canary repositories run
processor scripts from the immutable commit that triggered the drain; every
other repository runs them from a promoted release commit.
stable-release.jsonrecords its SHA and release. A regression therefore shows up on a small set of repositories before it reaches the whole fleet. - Staging the code rather than gating individual changes behind feature flags also covers the changes nobody thought were risky, which is where regressions actually come from. Feature flags remain available for a single genuinely risky behavior.
- The drain loads an immutable archive and a separate dependency environment per code ref. It never substitutes main's evaluation or state codecs for stable code, and each target repository gets an isolated Git clone.
- Repository configuration is deliberately not staged.
repositories.jsonis always read from the commit that triggered the run, so opting a repository in or changing its settings takes effect immediately in both channels. The cost is that configuration changes must be additive for one promotion cycle, because pinned code reads live configuration. - Promotion is a pull request that pins the ref, rather than a moving tag or branch. A manually dispatched workflow prepares that pull request only after an operator names the release that has soaked on canaries. The version that each repository runs is then visible in the release configuration, a code rollback is a revert, and the reference stays hash pinned the way every other action reference in this repository is.
- Queue drains process both channels through calculation, accepted-state persistence, delivery, and publication. Neither channel acknowledges a dispatch as completed work. Accepted state and delivery receipts retain their existing formats and version guards.
- A rollback stops a bad change rather than restoring delivery. Downgraded code reads newer delivery state as empty, so delivering from it would repeat reminders and re-review requests already sent; the delivery version check makes it skip delivery instead. Rolling forward is the way out, and a paused dashboard is the cheaper failure.
- Dashboard state version 18 regenerates every earlier dashboard state. Earlier
versions use
open_threadfor any unresolved thread and do not persist the pending action needed to reconstruct the separate author-action state.
- In owned execution, live PR/head refreshes and maintenance backfills/reminder sweeps have independent dispatcher leases and runners. A maintenance batch cannot occupy live processing slots. Both lanes use the same combined processor and acknowledge only after persistence, delivery, and publication finish.
- Repository delivery leases serialize side effects across lanes without holding ownership during classification. State compare-and-swap and PR-slot acceptance still reconcile overlapping calculations; dispatcher heartbeats renew delivery ownership and stale lease generations cannot release a replacement's lease.
- The Netlify bridge coalesces webhook refreshes by target repository and PR before it starts GitHub Actions. Each lane's drain claims a bounded batch. An event that arrives during processing marks the item dirty and schedules one follow-up pass.
- Coalescing is safe because each refresh loads current PR state from GitHub. Intermediate states can go unobserved, but the surviving run reflects the state that exists when it executes.
- Submitted reviews can coalesce with generic PR refreshes because the live PR status comment is rendered from current accepted dashboard state rather than a review-specific event. Backfills use separate maintenance capacity because they refresh a repository rather than one pull request.
- Events coalesce in Netlify before a drain dispatch. Enqueue workflow runs use independent concurrency groups so pending runs cannot replace accepted requests.
- Delivery leases serialize side effects per repository. Each backfill transaction records a monotonic full-publish generation on the worker branch. A targeted publisher normally limits status-comment and Slack delivery to its PR, but drains repository-wide work when it observes a generation without a delivery receipt. The publisher records the receipt only after full delivery and the dashboard issue both succeed. A full queue publication satisfies the batch's pending issue update; another issue publication is needed only after a later active delivery. Webhooks without an outstanding full-publish generation remain targeted, so unrelated PR events do not repeatedly fan out. Status-comment rollout remains bounded to 50 per untargeted delivery; a webhook publisher puts its triggering PR first in that batch.
- Recovery reports retries, dead letters, queue age, and ownership per lane. The stalled-run watchdog watches only webhook deployment. Delivery, issue publication, and receipt errors retry the combined item.
- Scheduled backfills are batch jobs: they read repository PR lists, call REST and GraphQL, run Copilot classification, and update git-backed state. A combined processor renders and publishes the issue body from accepted state.
- GitHub Actions provides clearer logs, concurrency controls, artifacts, and normal retry/cancel behavior for that workload.
- Netlify remains appropriate for small webhook-sized work, but it was a poor fit for long backfill workers.
- Dashboard calculations and delivery intent are stored on a worker-owned git branch rather than in the live dashboard issue body.
- Notification receipts, author-nudge and Copilot-request receipts, status-comment rollout progress, and delivery-version claims are stored on a publisher-owned delivery branch. Both branches keep files namespaced by target repository.
- Workers only push the accepted-state branch. Publishers only push the delivery branch. Continuous worker compare-and-swap traffic therefore cannot reject a publisher receipt push, and publisher activity never delays a worker.
- Every untargeted backfill save advances the full-publish generation, including saves with unchanged dashboard state. Successful hourly backfills therefore add worker commits even when no dashboard result changes; PR-by-PR backfills also advance the generation throughout the run.
- Updates use
git push --force-with-lease, so each owned ref remains its own durable compare-and-swap boundary. - The first publisher copies compatible receipt state from the accepted branch before writing a delivery-state marker. Existing notification history, pending reminders, Copilot requests, status-comment rollout work, and version claims therefore move without a gap. Later publishers reconcile immutable worker intent with receipts instead of copying mutable files back.
- A missing repository state branch is bootstrapped by non-PR backfills. The dashboard state records when every open non-draft PR has been populated at least once. Targeted PR runs, dashboard publishing, status comments, and Slack notifications skip until that initial backfill is complete, so no partial dashboard is exposed.
- Full-publish generations live on the worker branch and completion receipts live on the delivery branch. The generation is advanced in each backfill compare-and-swap update, including an empty or unchanged backfill, so a publisher that runs between two updates cannot acknowledge later work. Delivery-version claims include the full-publish state version; downgraded publishers cannot acknowledge work whose receipt protocol they do not know.
- Targeted PR runs compute the triggered PR and merge that one PR slot with the latest accepted state on each state-branch compare-and-swap retry.
- Non-PR dashboard runs are backfills, not repository-wide refreshes. They are capped so one run cannot exhaust the dashboard GitHub App's hourly API quota.
- Each backfill lists open PRs, prunes cached PRs that are no longer open non-draft, then refreshes at most 50 open non-draft PRs.
- Status-comment rendering rollouts use separate versioned state and a durable queue. Incrementing the implementation revision snapshots all open PRs, then hourly runs update at most 50 queued comments until the rollout completes. Dashboard refreshes record a deterministic intent revision when their persisted result changes. The delivery branch records the last delivered revision per PR, so an intent remains durable without forcing every publisher to rewrite the same comment. A targeted publisher updates only its triggering PR and cannot initialize or drain the repository-wide rollout. Untargeted publishers drain up to 50 queued comments. This confines rollout fan-out to the hourly path and delivers work left by a pending publisher that GitHub replaced.
- Selected PRs are processed one at a time through the same single-PR merge path as targeted refreshes. Each accepted PR update pushes structured state before the next selected PR is processed.
- The one-PR transaction size keeps state-branch compare-and-swap retries cheap: a rejected push retries one PR instead of refreshing a whole large repository and spending the same GitHub GraphQL rate-limit budget again. Both backfill and targeted retries reuse the already computed PR result and only redo the latest-state merge and state save. A concurrent accepted change to that PR wins over an evaluation based on its previous state. GitHub content changes arriving during persistence are picked up by a later refresh.
- Backfill progress is stored separately from dashboard state in
backfill-state.json. The cursor is the last attempted PR number, and the next run continues after it in sorted PR-number order, wrapping when needed. Failed PR numbers are stored beside the cursor and are removed after a later successful refresh. Each run pulls forward up to 10 recorded failures from that rotation, then fills the remaining batch from the normal mixed order. This retries failures sooner without adding another cursor or letting the priority group consume the entire 50-PR batch. - The rotation is not reordered to favor PRs that are waiting on something. A wait ends with nothing changing on the PR — a check completes, a review is filed — so a missed event is only noticed when the rotation comes round again. But a full sweep takes two passes on most repositories and four on the largest, which is no longer than the gate hold that bounds the wait anyway, so refreshing waiting PRs first would buy about an hour on one repository and nothing on the rest.
- Initial-backfill completion is stored in dashboard state and becomes true in the same accepted state commit that attempts the final missing open non-draft PR. Failed PR data is not accepted into dashboard state, but a recorded failed attempt cannot block initial publication. Once set, completion remains true. New PRs do not reset bootstrap; they appear after their first successful targeted refresh or backfill.
- A selected PR failure is recorded outside dashboard state, advances the cursor, and does not stop later selected PRs. The backfill still exits nonzero while any open PR is still recorded as having failed processing, keeping scheduled failure reporting active. The publisher consumes only accepted state, so untrusted PR content cannot deny service to the rest of the repository.
- The cursor deliberately does not rely on PR
updatedAt; prior testing showedupdatedAtis not a safe freshness key for every comment, review-comment, or thread event the dashboard needs.
- Review threads are still fetched from GraphQL because the dashboard needs
thread-level fields such as
isResolved,isOutdated, and canonical thread grouping. - Top-level issue comments are fetched entirely through GraphQL because
lastEditedAtisolates content edits while RESTupdated_atalso changes for non-content activity. This avoids a second REST request and metadata join; the dashboard falls back tocreatedAtwhen a comment has never been edited. reviewThreads(first: 10)is intentionally small. The nestedcomments(first: 100)connection makes GitHub GraphQL rate-limit cost scale with the review-thread page size.- Pagination still fetches every review thread; the smaller page size reduces rate-limit spikes without dropping data.
- LLM classification cache is stored with
actions/cache. - Unchanged review threads and top-level feedback items reuse cached classifications and avoid new Copilot calls.
- Each discussion cache key includes the model, full prompt template, prompt input, replacement system prompt, and execution configuration version. Changing one classifier's domain policy invalidates its verdicts without clearing unrelated classifier or dashboard state. Changing shared inference instructions invalidates all affected classification results.
- Cache keys are scoped by target repository and by either PR number or backfill.
- Targeted PR runs restore their PR-specific cache first, then fall back to the latest backfill cache for that repository. They still save under a PR-specific key, so targeted runs do not overwrite the backfill cache namespace.
- Cache entries are immutable, so rolling keys plus restore prefixes pick up the latest usable snapshot without concurrent writers overwriting each other.
- Cache snapshots are saved even when the update job fails, preserving valid classifications produced before or alongside an isolated failed item.
- Failed classifications are not cached. A structurally invalid model response or contract-validation failure retries its model request once in the same run. Copilot SDK failures, timeouts, and raised exceptions do not use this retry. If the second response is still invalid, a later run restores valid sibling classifications and sends only the still-uncached items to the model. The original run remains failed so the item is visible for operational triage.
- Reported CI facts come from the PR's GraphQL status-check rollup, filtered by
each context's
isRequiredresult, so optional check failures do not make the dashboard report a failing PR or change its route. Paginated effective rulesets for the PR's base branch supply configured required contexts; a context that has not reported yet is shown as pending rather than passing. - Classic branch-protection required status checks are not discovered when they have not reported. This is an accepted limitation because configured OpenTelemetry repositories use rulesets for required status checks.
- The rollup is read from the PR's last commit, which can still be the previous head just after a push. That commit's checks are already complete, so they would read as a settled result for code that is no longer proposed. The rollup therefore carries its commit oid and is discarded when it does not match the PR head, leaving check facts unavailable until the rollup catches up.
- A required context is only pending while the app that owns it may still report. Check suites for the head commit are consulted, and a context is dropped from the pending set once every suite its app created has completed, because the app has then reported everything it is going to. Without this a ruleset context that no workflow produces — an obsolete or conditionally skipped job — would be reported as permanently pending. Such a PR cannot merge either way; the difference is that the dashboard stops treating it as "still running." The suites are read only when an app-owned required context has not reported, so the refreshes that cannot use the answer do not pay for it.
- A
code_scanningruleset rule holds the merge on a check that the code scanning app publishes per configured tool, named after that tool, which GitHub never marks as required. Those checks are matched by app and by the tool names in the rule, then treated as required. TheirNEUTRALconclusion means the alerts introduced by the PR could not be determined, which also holds the merge, so it is reported as failing rather than skipped. Tools with no such check are not reported, because GitHub expects results only from the tool configurations that actually ran. - That
NEUTRALis only reported as failing once every check at the head has finished, including optional ones. The code scanning app publishes the tool check asNEUTRALbefore the analysis is uploaded and then replaces it in place, so an analysis still running is reported as pending instead of pinning the PR to its author. The replacement leaves the enclosing check suite untouched, and the dashboard does not subscribe to check runs, so no webhook reports it. That only delays the dashboard when the code scanning analysis is the last thing to finish at the head, because any check suite completing after it triggers the refresh that observes the final result; when it does, the transition waits for the hourly refresh. Subscribing to check runs would close that window at roughly ten times the webhook volume of every other event combined, because GitHub emits one check run per job. - Required checks reported as commit statuses, such as EasyCLA, never belong to a check suite, so commit status events are subscribed as well.
- A failing required status check routes a human-authored PR to the author
before discussion and approval routing. The live PR status comment names the
CI failure, including when review feedback also needs author action.
Repository-configured
non_blocking_check_patternsidentify failed optional checks in a note alongside this action, without changing required-check facts or routing. An optional check inACTION_REQUIREDis not included in that failure-only note: it does not block merge and has neither failed nor been cancelled. - A merge conflict does not decide who should act. Discussion, CI, and approval routing still identify the owner, while the conflict remains visible as a separate merge blocker. This lets maintainers handle routine conflicts, such as a changelog conflict, without first sending the PR back to its author.
- Required-check and Copilot review gates still hold a handoff while a conflict exists, but their missing-result clock is paused. GitHub may not start either automation on an unmergeable head, so a missing result is expected rather than a stalled handoff. Entering the conflicted state clears any same-head hold clock. A fresh four-hour clock starts if a gate is still missing after the conflict is resolved.
- A PR does not advance toward merge while the required checks are unsettled: an author waiting on CI keeps the PR, and a PR already with approvers is not handed to maintainers to merge. Clearing the checks is the author's job, so an outstanding one is not yet a reason to spend anyone else's attention, and a push clears the failing count before the replacement checks produce a result, so the PR would otherwise move forward on evidence that does not exist yet and move back minutes later when the same check fails. Moving back toward the author is never held, because a failing check or new author-owned discussion is evidence the gates cannot undo. Unavailable check results hold the handoff for the same reason a pending one does, and resolve on a later run.
- An
ACTION_REQUIREDcheck from the GitHub Actions app with an attached workflow run is the exception to that hold. That metadata identifies a workflow approval: a final, reported result that still blocks merge because someone with repository write access must unblock the workflow. It remains unsettled and appears as 🔐 in the CI column, but it does not hold routing with the author. The PR routes to reviewers while approvals are outstanding, then to maintainers once it has enough approvals.ACTION_REQUIREDfrom another app, or without a workflow run, has unknown ownership and routes conservatively as a failure. A genuine or unknown-owner required-check failure alongside a workflow approval also routes to the author. - A held PR is presented as waiting on its author rather than on the robot it is waiting for, so a separate route would add a section that nobody is expected to act on. What it waits for is named in the columns instead: the CI column already shows running checks, and a Copilot review that is actually in flight is listed in the reviewers column with the pending icon. Copilot otherwise joins that column only once it has reviewed, so without this the Copilot gate would hold a PR with nothing on the row to explain why. The live status comment tells the author the handoff happens once both are clean.
- A held route also holds its wait age. Recomputing it would read the push as the end of the CI failure and fall back to the last approver activity, which is usually far older, so a PR the author had just pushed to would sort to the top of the waiting-on-authors section as the stalest item on the board.
- A held PR remains in the author reminder flow. Most holds clear quickly, but a hold can also mean that automation never started or that the author is stuck. After the standard seven-day wait, the reminder links to the live status and offers the reviewer-routing command as a break-glass handoff. Reviewer Slack notification still waits until the route actually reaches reviewers.
- While a PR stays on a route where someone other than the author owes it a response, its wait age only moves back, never forward. The fallback for those routes is the last author activity, so a push would otherwise restart the clock and present a review nobody has done in a week as brand new. A handoff from the author route does start a fresh wait, because that push is what put the PR in front of reviewers.
- A handoff the gates held starts its wait when the gates release it, not at the push. The push and the handoff were the same moment before the gates existed, which is why the fallback dates a reviewer's wait from the last author activity. Now the gates sit between the two, so a PR whose checks took an hour would reach reviewers already an hour old, and one released after a stalled gate would arrive older still. Either way the age blames reviewers for a wait they could not have answered, and sorts the PR above ones they really have been sitting on.
- Only the handoff from the author restarts the wait. A held PR that was already with reviewers and is released to maintainers keeps its wait, because it never left the people who owe it a response, and restarting there would present an approval a week old as a merge request that just arrived.
- PRs from unattended automation never route to their author because the automation cannot respond to a dashboard action. Copilot-delegated PRs are different. The dashboard recovers a human assignee or the first commit's human committer as the effective author, so that person can receive the author route. Pending required checks affect the CI column, and a bot PR whose handoff is held waits on reviewers instead. Known maintenance bots use a one-approval threshold; other automation uses the repository's configured threshold. Compatible cached state derives a missing author-capability fact from the effective author identity and discards a stale author-routed result for reevaluation, while an explicit stored value wins. Merge conflicts remain visible without overriding that routing.
- A hold has a time limit, and past it the PR routes anyway. Every gate waits on something outside the dashboard, and each one has been seen never to arrive: a required check with no check run on the head, a Copilot review GitHub never started, a review request GitHub accepted and dropped. The dashboard does not decide whether a PR may merge, branch protection does, so a hold that never ends protects nobody. It only keeps the PR away from the people who could look at the missing gate, and it hides the failure, because a held PR looks exactly like a PR that is waiting normally.
- The limit is four hours: longer than a slow check suite or a queued Copilot review, short enough that a gate which is never going to report costs the PR part of a day rather than the rest of its life.
- The clock starts when a gate first holds the PR back, and then runs on its own for as long as the same head still has an outstanding gate. Carrying it that way is what lets the hold give up without the stall looking resolved a moment later, and it keeps the status comment able to say which gate the dashboard stopped waiting for. Starting it only on a real hold is what keeps a slow check suite on a PR that was already with its reviewers from looking like a stalled handoff. A push clears the clock, because new code means new checks and a review that has to run again.
- Only a gate that has reported nothing on the current head runs the clock. Copilot findings that still need author action hold the PR through discussion routing, but the review is not missing: it answered. Because the clock is carried across a trip back to the author, counting those findings would make every author who takes more than four hours over review comments look like a gate GitHub lost, and the report would say a review never arrived when it is sitting on the PR. A review that is missing or that only covers older code still counts, because Copilot has said nothing about the code being reviewed.
- An expired hold routes the pull request and says so in its status comment, which is the whole of what the dashboard can do about it. The gates raise no alarm of their own. Each way a gate can go missing has its own cause and none of them can be told apart from the dashboard's side, and the pull request is already in front of the people who can look at the missing gate. Failing the repository's run instead would repeat every hour until the head moved, long after the handoff it was reporting had happened, and it would keep firing once a trip back to the author had stopped the dashboard re-requesting the review it was naming. A conflicted PR cannot expire its hold because its missing automation is expected.
require_clean_copilot_review_branchesis a final safety net applied only when a PR would otherwise route to reviewers or maintainers — that is, after the author has addressed the actionable discussions. It is not a routing input while the author still owns actions.- The setting lists the base branches to gate rather than a single on/off switch, because automatic Copilot review is itself configured per branch (often only the default branch). Gating a branch with no automatic review would hold every ready PR with its author waiting for a review that never runs, so only branches with automatic review are listed and PRs targeting other branches route normally.
- Copilot threads use the same discussion classification as human review threads. A completed author reply hands the action to reviewers; a commitment to more work keeps it with the author. Explicit resolution is not required for that handoff because a reply explains the author's response without requiring a separate administrative step. The thread remains visible as unresolved until GitHub marks it resolved or outdated.
- The gate waits only for a missing or stale review. Counting open threads independently would override discussion classification and keep completed author replies assigned to the author. Actionable findings already keep the PR with its author through ordinary discussion routing.
- A stale review means the author pushed, so a re-review can inspect new code. Findings on the current head do not trigger re-review requests: another review is not a substitute for responding to the finding. A later clean review alone does not close an existing thread or change its pending action.
- The dashboard requests a missing first review on the first eligible refresh using its GitHub App installation token. Initial reviews and re-reviews share the same delivery checks and confirmation path.
- Copilot's generic error notice is a failed attempt, not a completed review or delivery confirmation. It leaves the review missing or stale so the next eligible refresh can retry on the same head without changing the requester. Only one retry is allowed: two error notices for the current head stop automatic requests and immediately release the Copilot gate. The count comes from GitHub reviews, not refreshes, and a new head starts a fresh budget. Delivery rechecks the live count so an error arriving after evaluation cannot cause another request. Pending requests still prevent duplicates. Required checks and Copilot findings needing author action still apply; the status comment explains when routing stops waiting after repeated errors. The four-hour timeout remains a backstop for reviews that never report.
- GitHub's quota-exhausted notice is a failed attempt even though GitHub records it as a review of the current head. It does not count as a completed review or confirm delivery. Without a configured fallback token, it immediately releases the Copilot gate and disables automatic requests for that PR, including after later pushes. Required checks and actionable feedback still apply, including Copilot threads that still need author action.
- The optional
COPILOT_REVIEW_FALLBACK_TOKENchanges the requester after quota exhaustion and keeps the Copilot gate active under its normal rules. The fallback records existing quota notice IDs before requesting, so only a new notice exhausts it. An exhausted fallback permanently stops automatic requests for that PR and immediately releases only the Copilot gate. A genuine current-head review still counts if the PR also has a quota-failure notice. - A first-review request is reachable only where a re-request already is — the pull request would otherwise route to reviewers or maintainers, and Copilot is not already a pending requested reviewer.
- Running checks do not hold either request back, so the review and the checks run at once rather than in sequence. Checks that fail still block it, because they route the pull request to its author and only a reviewer route reaches here, so a review is never spent on code CI is about to reject. Waiting for pending checks only delayed the request by however long the suite took, on top of the hour the missing review had already cost.
- For the same reason the recorded request is fingerprinted without the checks. The fingerprint exists to discard a request whose routing has since changed, and a request made while CI runs would otherwise be discarded by the next check to finish, one pass at a time, for as long as the suite lasted.
- Delivery re-validates the review state against live data and discards the request when a review already covers the current head, which is what happens when one lands between the observation and the delivery.
- A request counts as delivered only once GitHub shows Copilot as a pending reviewer. The mutation answers with success even when GitHub records nothing: on one pull request it accepted the same request every hour for nineteen hours and created no review request at all, and the pull request stayed held on its author until a person requested the review by hand. Stamping the request from the mutation's own answer hides that completely, because success is silent and only discards are logged.
- The read back is retried a few times, because GitHub takes a moment to record a request it did accept. A Copilot review of the current head is accepted as the same proof, since a short review can finish and take Copilot out of the pending requests again before the read.
- A request GitHub dropped is left undelivered, so the next pass sends it again and logs the miss. It needs no alarm of its own: the pull request stays held while the review is missing, and the hold expiring is what reports it.
- The reviewers column marks Copilot pending only where the gate applies and a review is genuinely in flight — a requested re-review, or the automatic first review on a PR the Copilot gate is holding because Copilot has never reviewed it. The gate scope is part of the meaning, not just a hold: a requested human reviewer who has not responded is left off the row entirely, so Copilot earns a place only where its review is what the PR is waiting on. A hold alone is not enough either, because unsettled required checks hold a route too. Marking every outstanding gate instead puts the icon on nearly every row, because a stale review is the ordinary state between a push and the next re-review, and an icon that is always present says nothing about which PRs are actually waiting.
- Copilot appears in the reviewers column only for unresolved inline feedback or a pending review that holds the PR. It reviews nearly every PR, so completed participation alone adds noise rather than identifying work that remains. This is a display filter only; review state and routing still use all feedback.
- An effective reviewer-routing override is a break-glass handoff. It bypasses required checks, Copilot review, merge conflicts, and discussion actions across later pushes. The approval threshold determines whether it waits on reviewers or maintainers. The author may be stuck or may need a person to explain a basic problem, so no automated blocker can prevent the handoff. Top-level feedback last changed at or before the command stays retired. Review threads are not retired by the command, but their existing work does not override the handoff. Actionable human reviewer feedback on an unresolved, non-outdated thread with an effective content timestamp after the command ends the handoff, because a reviewer has answered the request for help and assigned the next action to the author. That includes a new reply or edit on an older active review thread. Praise, informational comments, bot feedback, inactive threads, and feedback last changed at or before the command do not end it.
- While a handoff is active, the dashboard classifies only human reviewer feedback with content activity after the command. Older discussions and classification failures therefore cannot block the break-glass route. Once newer feedback produces an author action, normal discussion classification and routing resume, subject to the permanent top-level feedback cutoff. The dashboard records that transition in its live status comment so an author reply or a lost state cache cannot reactivate the same command. A newer command can establish a new handoff on the same head and advance the cutoff.
- The dashboard binds a command to the head it sees when it first reads that command, and records that head in an acknowledgement marker on either the command reply or the live status comment. A persistence marker makes the cross-push behavior explicit. Legacy acknowledgements without that marker remain head-bound, as do pending bindings restored from compatible legacy state, so deployment cannot reactivate old handoffs that had already expired. A separate companion marker records the frozen timestamp used for the permanent top-level feedback cutoff. Marker lookup is scoped to the bound command. A legacy acknowledgement without that cutoff does not retire feedback, because the command's current edit timestamp cannot reconstruct the original cutoff. The first observation freezes the cutoff for that command, so editing the command later cannot retire intervening feedback; only a newer command advances it.
- Recording the observed head also avoids ordering the command against a push by
comparing the comment timestamp with the head push time from
GET /repos/{repo}/activity. Both timestamps have one-second resolution and come from different APIs, so the ordering is sometimes unknowable. Binding to the observed head removes that extra API call. A push between the command and the pass that first reads it belongs to the handoff, as does any later push after the persistence marker is recorded. - The gate does not wait for the required checks before requesting the review, so the two run at once. A route computed while checks are still running is provisional, but the only outcome that matters here is a failure, and a failure routes the PR to its author, which is not a route the request is reachable from. Waiting cost the review however long the suite took and recovered nothing.
- Delivery still re-validates the required checks against live data rather than relying on the fingerprint alone. The fingerprint only detects change, so checks that were already failing when the request was recorded would otherwise pass through unnoticed.
- "Clean" means no inline comments on the current head, counted from the review, not from the classifier's actionability judgment. Accepted limitation: if Copilot leaves comments the classifier treats as non-actionable while they stay unresolved, routing sits at reviewers but the handoff stays held with the author and re-requests until Copilot returns a comment-free review or the author pushes. The strict count is intentional — the gate is a conservative "Copilot had nothing to say about this exact code" check, and folding in classifier judgment could let a real-but-non-actionable comment slip a PR to humans.
- Feedback totals in the live comment count the canonical author-action links stored in dashboard state, not a separately persisted total of pending-action records. This keeps every counted item tied to an action the comment can present to the author.
- GitHub provides non-null ids for review threads and non-null canonical URLs for review comments and submitted reviews; issue comments likewise have id-specific URLs. Distinct author-action items should therefore produce distinct links. Missing or colliding URLs indicate malformed upstream data, not a supported state that needs a second count.
- Feedback links are deduplicated in dashboard state (when the canonical author-action links are collected) and capped at 20 in the comment. If the URL invariants need stronger enforcement, fail the affected PR refresh rather than advertise a larger item count with fewer actionable links.
last_activity_atis derived from substantive participant events rather than the PR'supdatedAt. Updating the status comment bumpsupdatedAt, so anupdatedAt-derived fact made every refresh look like new activity, which queued another comment update, which bumpedupdatedAtagain. Besides rewriting the comment indefinitely, that loop reset the inactivity clockactions/stalereads, so no PR in a dashboard repository could go stale. The dashboard app is never a PR's author, sorole_foralways classifies its comments asbotand they never count.- An inline review thread's list position comes from its last comment's
createdAt, never its edit time. Effective content activity selects the latest participant and requester and supplies the pending action timestamp, so editing an older request can reactivate it without reordering the classifier transcript or list. The same selected comment supplies praise and author-reply classification, and removing praise leaves the remaining conversation order intact. Top-level feedback items still date from their creation time, so editing a comment cannot reorder that list. - The ignored praise index is a current-evaluation projection used for reviewer
attribution. Classification caches retain the praise verdict but not its
position, so each refresh recomputes the index from current thread activity.
Pending actions are not persisted in dashboard state. Legacy projections
with only
ignored_last_commentretain creation-last behavior.
- GitHub gives inline review threads explicit replies and a resolved state, but top-level feedback has no equivalent completion signal. Top-level feedback means a standalone PR comment or submitted review summary that is not attached to an inline review thread.
- Each top-level feedback item is therefore classified independently with a stable GitHub-derived id. The LLM decides only whether the source is actionable.
- Each refresh reconstructs these independent items with a linear scan of the comments and reviews already fetched from GitHub. They are not threaded, and the reconstructed list is not stored as a second ledger. This keeps edited or deleted source comments authoritative without additional reconciliation. Cached classifications avoid repeated LLM calls, while dashboard state retains the author reply already observed for each item.
- In normal routing, an explicit author reply is the only thing that closes a top-level item. Commits, PR title edits, and PR description edits are not tied to the item they would close, so a push alone never closes feedback. The break-glass reviewer command is the deliberate bulk exception: it retires top-level items at or before its cutoff while leaving review threads open. The status comment lists the exact open discussions and explains how to give each one an outcome, which makes an explicit reply both cheap and unambiguous. An author's explicit commitment to future work in the current PR is a self-deferral, not a completed reply, so the item continues waiting on the author.
- Each model call classifies up to ten uncached top-level feedback items independently, while retaining a separate cache entry for every item. A refresh processes at most 200 such items per PR. Exceeding that cap means the items went unread, which is reported as a classification failure so the refresh is not published and the next one retries them, rather than the items being given an invented action. This bounds both call count and prompt size without allowing one long-lived PR to monopolize the workflow or model quota.
- Candidate author replies use a separate classifier with the same batch size, per-PR cap, and immutable cache behavior. That classifier also has a model-call budget that is expected to bind, so exceeding it defers the items instead of failing; every consumer already treats a deferred author reply as not yet classified, which leaves the earlier handoff in place. Its result distinguishes completed replies from author self-deferrals independently for each earlier feedback item the comment addresses. Timestamp ordering determines which items are candidates, but never applies a comment to every earlier item by itself. Candidate sets are split and model-call batches are greedily packed against the fully serialized prompt, so every Copilot SDK argument remains within the configured character limit. Partial results are merged into one cache entry per author comment. Completed reply evidence retains the source comment id as well as its timestamp, so comments created in the same second cannot be confused.
- "Unclear" remains classifier vocabulary but is not a route. It collapses onto the author when a pending action is built: when the classifier cannot tell what a discussion needs, the author is the one who can clarify it. There is no separate label for feedback blocked on a dependency, decision, or event outside this repository, because the author still has to drive it. A route for that case would name nobody, could not be nudged, and would outrank approvals, leaving blocked PRs unowned.
- A concrete reviewer report that tests still fail, CI still reproduces the defect, or the proposed fix remains broken is an author action even without request wording. It is evidence that the change is not ready, not a status summary.
- Lifecycle transitions are deterministic after feedback and author-reply classification. An ordinary new item waits on the author with 📌 visible. Once the author gives a completed reply, the item is addressed and the pin disappears. Normal approval-based routing then decides whether the PR waits on reviewers or maintainers; ordinary items do not have a separate requester-confirmation phase.
- Review summaries follow the same lifecycle as other top-level feedback, but
the classifier is told which kind of item it is reading, so the summary rule
below applies only to them. Review state stays independent of that: a
CHANGES_REQUESTEDstate affects only the reviewer's badge; it does not affect dashboard actions or routing. Formal review states are ordered by submission time, while summary edits use their content timestamp for lifecycle decisions. Empty review summaries are ignored; their inline comments, if any, define independent actions. - Who a comment opens by addressing is extracted in code and passed to the
classifier as
addressed_to, rather than left for the model to find in the body. A reviewer routinely names other people, pull requests, and prior work while still asking the author for something, so only an opening run of mentions is treated as an addressee. Naming it explicitly is what lets the prompt distinguish a design question put to a maintainer, whose next move is that maintainer's, from feedback that merely mentions one. The item still waits on the author whenever it also requests, suggests, or directs a change, so the addressee never overrides real feedback. - A review summary that only refers to the review it introduces needs nothing from the author. That covers where its comments came from, how much weight to give them, and that the author is free to disagree with them, and it equally covers a summary that counts those comments, describes their severity, or names the topics they cover. Those comments are already independent items, so treating the summary as feedback asks the author to answer a note about comments each tracked on their own, and its invitation to push back would outlive every one of them. The classifier's ambiguity fail-safe otherwise sends these to the author whenever the wording reads as a request, so the prompt names them. A summary is ordinary feedback only when its own body adds a request, question, objection, or blocker beyond referring to those comments. Accepted tradeoff: a summary stays non-blocking even when it says the review contains requests, so a reviewer whose only actionable point is in the summary should post it as its own comment.
- The author reply that closed an item is retained in the cached PR result. It is reused only when it is newer than the item's effective content timestamp. Editing an older request with new work therefore reopens the item when the edit is newer than the completed author reply. Ordinary requester-confirmation timestamps are not persisted.
- Reviewers should prefer inline comments when feedback needs explicit closure. Blocking PR-wide feedback should use GitHub's Request changes review state; ordinary top-level feedback remains a softer coordination mechanism.
- Slack notification state is PR-granular. It does not track notification history separately for each assignee.
- When notification state is first created, existing approver-routed PRs may receive initial notifications from a later publisher. Avoiding that bootstrap case would require storing separate seen-but-not-notified state.
- When a mapped assignee is added after a PR was already notified during the same waiting period, that assignee may wait until the next follow-up cadence instead of receiving an immediate initial notification.
- A targeted publisher evaluates only its triggering PR unless a backfill's full-publish generation is outstanding; it preserves unrelated entries in the sent-notification ledger. Full delivery evaluates all accepted repository state for eligible initial and follow-up notifications. The ledger and weekday 24-hour follow-up cadence bound delivery.
- Slack notifications are sent only for dashboard state that has already been accepted on the state branch. A newer dashboard update can land after the publisher checks out state, so a notification can be slightly late relative to the newest state.
- The publisher preserves just-written notification state across normal delivery-branch CAS retries. Slack delivery is explicitly at least once. A process crash after Slack accepts a webhook but before the receipt push can cause a later publisher to send the same notification again. Recording the receipt first would trade the duplicate for a lost notification, which is worse for this dashboard.
- Dashboard publishing is normally serialized per target repository. The publisher owns target-repository writes for status comments, author reminders, Copilot re-review requests, Slack notifications, and the dashboard issue.
- Each publisher pins one accepted-state snapshot. A targeted publisher limits status-comment and Slack delivery to its triggering PR unless a backfill generation is outstanding; then it makes one untargeted delivery. An untargeted publisher drains repository-wide work, with status comments bounded to 50 per run. Author reminders and Copilot requests use explicit durable ledgers; Slack eligibility is reconstructed from accepted dashboard and notification state.
- A confirmed 404 from the pull request lookup removes that status-comment target from the pending rollout. Other lookup and comment-write failures stay pending and fail delivery.
- The dashboard issue is rendered from
dashboard-state.jsonand the target repository's current open PR list after delivery. Worker advancement does not invalidate an in-progress publish or trigger a retry loop. The resulting issue can briefly show snapshot A after workers accept B. A later targeted publisher normally converges it, and the hourly untargeted publisher is the repair path if GitHub replaces that pending run.