| name | reviewer-finder |
| description | Recommend (or, on explicit request, assign) the best GitLab reviewers for a merge request, optimizing for outstanding review load, availability, OOO/PTO (current and upcoming), timezone fit, and the SME-then-maintainer review convention. Use when the user asks who should review an MR, to find/suggest reviewers, or to assign reviewers while creating MRs. |
| license | MIT |
| compatibility | opencode |
| metadata | {"audience":"developers","author":"pedropombeiro","keywords":"reviewer, review, merge request, codeowners, sme, maintainer, assign, pto, ooo","workflow":"gitlab"} |
Reviewer finder
Recommend the best reviewers for a GitLab merge request. The MR is inferred from
the current branch or given explicitly (URL or IID). Optimizes for review load,
availability, OOO/PTO, timezone, and the SME → maintainer two-stage convention.
When to use
The user asks any of:
- "who should review this?", "find reviewers for !N", "best reviewer for this MR"
- "suggest a reviewer for
<branch>"
- "assign reviewers" / "create the MR and assign a reviewer" (the assign
path — see "Assigning" below)
When NOT to use
- The MR is a draft the user isn't ready to route — confirm intent first.
- There is no MR yet and the user only wants general team-ownership info — that's
a CODEOWNERS lookup, not reviewer selection.
Fast path for trivial, single-area MRs
The full four-script pipeline is overkill for a tiny, single-file change (a
test-only fix, a typo, a one-line maintenance change). For these, the best
reviewer is almost always the author/owner of the changed file, confirmed
against the MR's own area labels — which is also the reliable fallback when
the pipeline has trouble.
When the MR touches one file (or one tight area) and is low-risk:
- Find the file's recent owners:
git log --format='%an' -8 -- <path> | sort | uniq -c | sort -rn
- Take the dominant author and read their current signals directly:
glab api graphql -f query='{ user(username: "<u>") {
location status { availability message emoji }
reviewRequestedMergeRequests(state: opened) { count } } }'
- Sanity-check they match the MR's scoped area labels (e.g.
group::…,
devops::…), are not OOO, and have reasonable load.
For a trivial single-area MR, one reviewer covers both the SME and maintainer
stages — no need to split. Escalate to the full pipeline only when the change
spans multiple areas or is non-trivial.
How it works
Four scripts under scripts/ form a pipeline (all glab + jq, read-only):
resolve-mr.sh — current branch | URL | IID → {project, iid, author, files}.
candidate-reviewers.sh — queries the MR's approvalState.rules (the
SSOT: the exact eligible approvers GitLab shows in the MR Review widget, e.g.
"153 code owners for /spec/"). Each CODE_OWNER rule becomes a section (the
Maintainers fallback → maintainer pool; others → per-area SME). This avoids
CODEOWNERS parsing and group-membership expansion entirely — so non-approvers
(e.g. a UX designer with an inherited gitlab-org role) never appear. SME
sections are augmented with recent authors of the files. The query is
lightweight by design — it fetches only approver identities
(username bot state), not per-approver signals. Inlining signal subqueries
for a large rule (the cited "153 code owners") made GitLab's GraphQL resolver
partially time out and return invalid JSON; signals are instead gathered in
bulk by step 3.
reviewer-signals.sh — per candidate: outstanding review load, current
OOO/PTO status, role (jobTitle), location, and days-idle. Reuses signals
pre-gathered during group expansion; fills gaps for handle/recent-author
candidates.
rank-reviewers.sh — ranks candidates per section + the maintainer pool,
including an advisory role-fit penalty when a candidate's jobTitle clearly
mismatches a domain section.
Run them as a pipe:
SK=~/.config/dotfiles/gitlab/.opencode/skills/reviewer-finder/scripts
"$SK/resolve-mr.sh" \
| "$SK/candidate-reviewers.sh" \
| "$SK/reviewer-signals.sh" \
| "$SK/rank-reviewers.sh" --author-tz <author_utc_offset> > /tmp/reviewers.json
Pass the IID explicitly for a just-created MR. Branch inference
(resolve-mr.sh with no args) calls glab mr view, which can fail to find an
MR immediately after gpsup pushes and creates it (branch-tracking/propagation
lag). When you just created the MR, call resolve-mr.sh <IID> — the repo is
auto-derived from the git origin remote, so -R is not needed from inside the
repo.
Running the pipeline robustly
A broad CODE_OWNER rule can yield a 150+ approver pool. The scripts handle this,
but to keep failures debuggable and avoid dumping huge GraphQL blobs into
context:
- Run stages to temp files and validate JSON between them rather than one
long pipe, e.g.
resolve-mr.sh <IID> > /tmp/mr.json, then
candidate-reviewers.sh < /tmp/mr.json > /tmp/cand.json, checking each is
valid JSON before continuing.
- Never pipe a stage whose exit code was non-zero — a partial blob breaks
the next stage's
--argjson with invalid JSON text.
- If a stage prints a giant payload on error, capture stderr to a file and read
the tail; do not echo the whole thing.
reviewer-signals.sh gathers all signals (load, status, location, contribution
recency) via batched GraphQL in small chunks fetched concurrently, so it
evaluates the entire candidate pool (even 100+ maintainers) in ~15-25s. No
sampling or per-candidate REST fan-out.
days_idle is computed from the most recent contribution (latest authored or
reviewer-requested MR update), not lastActivityOn — the latter ticks on bare
sign-ins and falsely reports "active today" for someone idle for weeks.
Pass --author-tz <hours> (e.g. 1, -5) so timezone scoring applies — without
it, far-away reviewers can rank as high as near ones. tz_offset is derived from
each candidate's profile location. Timezone modes (--tz-mode):
day-ahead (default) — prefer a reviewer who has their full workday
ahead right now (uses the current UTC hour; override with --now-utc-hour).
overlap — closest timezone, for heavy back-and-forth.
handoff — a reviewer ~8h ahead (classic end-of-day handoff).
See references/scoring.md for the full scoring model,
API field reference, and tunable constants.
Scoring summary
Lower score = better. Per candidate:
- Current OOO/PTO → hard-excluded (status
busy, PTO keywords, or vacation
emoji). Listed under excluded, never ranked.
- Outstanding load → counts open MRs where the reviewer's state is not
approved (i.e. unreviewed/reviewed/requested_changes). Lower is better.
- SME bonus → recent authors of the changed files rank higher in their section.
- Inferred inactivity → idle ≥ 3 days (default) is flagged advisory, not
excluded.
- Timezone fit →
day-ahead by default (favor a reviewer whose full workday
is still ahead at request time); --tz-mode overlap|handoff for other
strategies. Derived from location; applied only when the timezone is known.
Upcoming-PTO check (Glean)
The scripts cannot see calendars or Slack. After ranking, you (the agent) must
check whether the top candidates are about to go on PTO, because we want to avoid
routing a review to someone leaving imminently.
- Take the top 3 per required section + top 3 maintainers from the ranked
output.
- Run a single batched Glean
chat over that union. Prefer chat (cheap,
summarized) per CLAUDE.local.md; fall back to search with
app=googlecalendar or app=slack only if chat is inconclusive.
Example message:
Are any of these GitLab team members going on PTO, vacation, or out of
office in the next 5 business days: userA, userB, userC, ...? Check
calendars and recent Slack.
- For anyone confirmed leaving within 5 business days: demote them below
available candidates, cite the evidence and their return date, and surface the
next-best candidate in that section.
- If Glean is unavailable, note that upcoming PTO could not be verified —
do not fail or silently skip; say so.
Presenting results
Show a per-section matrix:
- One ranked block per required SME section, then the maintainer block.
Surface optional (
^[...]) sections as lower priority.
- For each candidate, give the rationale: outstanding load, local time/location,
availability/status, SME evidence (recent author), and any PTO note.
- Cross-section overlap: if one available candidate covers multiple required
sections (in the
overlap map), highlight them to minimize the number of
distinct reviewers — while still listing each section's full options.
- Wrap every username in backticks (
`username`) so nobody is pinged.
Maintainer-only MRs
If the changed files map only to the Maintainers section (no area-specific
named SME section claims them — common for top-of-file rules like *.rb), there
is no separate SME shortlist. Present a single maintainer ranking and state that
no specific SME section owns the files, so the same reviewer covers both stages.
Score ties
tz_offset is derived from location, so timezone now differentiates most
candidates. Remaining ties (e.g. several same-timezone, zero-load reviewers, or
candidates with no location and thus no timezone score) should be broken with
the Glean upcoming-PTO check and by spreading load across a batch. Never prefer a
no-location candidate on timezone grounds — their tz_offset is null, so they
were not timezone-scored at all.
Assigning (opt-in)
Default behavior is recommend-only. Only assign reviewers when the user has
given an explicit instruction — either in the current request ("assign a
reviewer") or a standing instruction while working through a stream of MRs
("assign reviewers as you create each MR").
When assigning:
- Pick the top available candidate per required section + the top maintainer,
applying cross-section minimization (one person can satisfy multiple sections).
- Never assign anyone hard-excluded (current OOO/PTO) or flagged by the Glean
check as about to be out; use the next-best instead.
- Spread load across a batch. When assigning reviewers to several MRs in one
go, prefer distinct reviewers rather than piling the same top pick onto every
MR. After each assignment that person's outstanding load effectively goes up by
one — account for it so the batch is balanced.
- Assign directly, then report who was set per section and why. Do not ask
for a second confirmation when an explicit/standing assign instruction already
exists.
How to request the review
Assign the reviewer (set the MR's reviewer via glab mr update --reviewer or the
/request_review quick action), then post a short friendly ping comment. The
comment is the human-facing nudge — keep it terse, in this exact shape:
Hey @<reviewer> :waves:, mind doing the <~labels> <stage> review?
Real examples (from gitlab-org/gitlab!240710):
Hey @abime :waves:, mind doing the ~backend / ~authorization maintainer review?
Hey @mfanGitLab :waves:, mind doing the ~"devops::verify" maintainer review?
Rules of the pattern:
- Greeting:
Hey @<reviewer> :waves:, — note the emoji is :waves:
(plural), not :wave:.
<~labels> name the MR's area/domain, using the project's scoped
labels — e.g. ~backend, ~authorization, ~"devops::verify" (quote labels
containing :: or spaces). When several areas apply, join them with /
(e.g. ~backend / ~authorization). Derive these from the MR's own labels and
the CODE_OWNER section(s) that apply.
<stage> is the review stage: typically maintainer (final approval).
For a distinct first/SME pass use the team's wording (e.g. "initial").
- No
/request_review in the comment body, and no context sentence — the
request is terse. The actual reviewer assignment is done separately (reviewer
field / quick action), immediately before the comment.
- One comment per reviewer.
- Posting these ping comments is covered by the explicit/standing assign
instruction. Respect the no-other-comments rule: only the ping, nothing else.
- Escape
::/quotes safely by writing the body to a file first, then
glab mr note <iid> -m "$(cat "$FILE")" (see the glab skill's
message-escaping guidance).
Pre-warm the merge pipeline for the final reviewer
When you assign the last required reviewer (i.e. their approval will satisfy
all remaining approval rules — approvalsLeft will drop to 0) and you expect
them to complete the review within ~16 hours, you can save end-to-end time by
running the full merge pipeline ahead of approval. Once they approve, the MR can
merge immediately instead of waiting for a fresh pipeline.
Conditions (all must hold):
- The reviewer being assigned is the last outstanding required approval —
check the MR's
approvalState: after their approval approvalsLeft becomes 0
(e.g. a maintainer-only MR where this is the sole required rule).
- They are likely to review within ~16 hours — they are available now (not
OOO/PTO), low load, and ideally in a timezone where the window covers their
workday. Use the same signals the ranking already gathered.
Then:
glab mr update <iid> --label "pipeline::tier-3" -R <owner/repo>
glab api --method POST "projects/<id>/merge_requests/<iid>/pipelines"
Notes:
pipeline::tier-3 is the full pipeline tier; tier-1/tier-2 run reduced job
sets that may not be sufficient for merge. Only escalate to tier-3 when you
intend the run to be merge-ready.
- This is an optimization, not a default — only do it when the last-reviewer
- 16-hour conditions hold, since a full pipeline is expensive. If the review is
likely to take longer, skip it (the pipeline would go stale).
- This is covered by the explicit/standing assign instruction; report that you
set the label and started the pipeline, alongside the review request.
A plan-mode approval or answering a clarifying question does not count as an
assign instruction. Absent an explicit ask, stop at the recommendation.
Agent guidelines
- Recommend by default; assign only on an explicit/standing instruction,
then report. This is the one narrow case where assigning reviewers is allowed.
Assign the reviewer, then post a terse ping:
Hey @user :waves:, mind doing the ~area maintainer review? — scoped area
labels, :waves:, no /request_review or context sentence. See "How to
request the review".
- Backtick usernames in all chat output.
- OOO inference and Glean PTO findings are advisory — cite the evidence so
the user can override; never present them as certainty.
- Whole-pool evaluation — signals come from batched GraphQL, so even large
maintainer pools are fully evaluated in seconds; no sampling caveat needed.
- Author timezone — derive it from the MR author's
location and always
pass --author-tz; without it, timezone is ignored and a far reviewer can
tie a near one. Default --tz-mode day-ahead favors a reviewer whose full
workday is ahead at request time — usually what you want for a review request.
- Degrade gracefully — if a signal source (status, last activity, Glean) is
unavailable for a candidate, treat it as unknown and say so, rather than
excluding the candidate.
- Pre-warm the merge pipeline when assigning the last required reviewer
who is likely to approve within ~16 hours: set
pipeline::tier-3 and trigger
a fresh pipeline so merge can proceed immediately on approval — see
"Pre-warm the merge pipeline for the final reviewer". Optimization only, not a
default.
- Use the fast path for trivial single-area MRs — for a one-file/one-area
low-risk change (e.g. a test-only fix), prefer git authorship of the changed
file plus the MR's area labels over the full pipeline. See "Fast path for
trivial, single-area MRs". It is also the correct fallback if the pipeline
errors.
- Just-created MRs: pass the IID explicitly to
resolve-mr.sh — branch
inference can miss an MR right after gpsup creates it.