name: review-epic
description: Linear · Review-only counterpart to /abc:ship-epic. Self-arming /loop that watches a Linear parent issue's sub-issues, reviews each child's PR/MR (GitHub or GitLab, routed via the repo: label) as it surfaces against the FULL epic context (parent spec + merged-sibling decisions + pending children's criteria), posts inline + spec-cross-referenced summary comments via the abc:reviewer subagent, and exits when the parent reaches Done. Never merges. TRIGGER when the user says "/abc:review-epic PARENT-ID", asks to "review this epic as it ships" against a Linear parent, or wants a standing reviewer session running parallel to /abc:ship-epic.
argument-hint: "PARENT-ID | https://linear.app/.../PARENT-ID [--no-compact]"
model: opus
allowed-tools:
- Skill
- CronList
- CronDelete
- Agent
- Read
- Grep
- Glob
- AskUserQuestion
- Bash(git -C * remote get-url )
- Bash(gh auth status:)
- Bash(gh pr view:)
- Bash(gh pr list:)
- Bash(gh pr diff:)
- Bash(gh pr comment:)
- Bash(gh api:)
- Bash(glab auth status:)
- Bash(glab mr view:)
- Bash(glab mr list:)
- Bash(glab mr diff:)
- Bash(glab mr note:)
- Bash(glab api:*)
- mcp__claude_ai_Linear__get_issue
- mcp__claude_ai_Linear__list_issues
/abc:review-epic — epic-context PR/MR reviewer (Linear)
Watch a Linear parent issue with sub-issues and review each child's PR/MR against the full epic context — the parent spec, the design decisions already taken in merged sibling PRs, and the acceptance criteria of children still pending. This is the reviewer half of the two-session epic-shipping pattern: /abc:ship-epic (or /abc:ship-issue on the parent) implements in one session; this skill reviews in another, holding the top-down spec context the per-PR view can't see.
Review-only. This skill never merges, never pushes, never closes issues, never transitions Linear states. Its only writes are PR/MR review comments and its own dedup markers — and those live on the PR/MR, never on the Linear issue (see ./linear-conventions.md), so the dedup convention is byte-identical with ../review-epic-gh/SKILL.md.
The per-PR review logic is shared with the GitHub variant via the abc:reviewer subagent; what differs here is plumbing — Linear MCP resolution, per-child platform routing, and the GitLab posting path. Linear↔VCS mapping lives in ./linear-conventions.md.
Hard rules
Never merge, approve-with-merge, push, close, label, or transition Linear state. Posting PR/MR review comments and
<!-- review-epic:* -->markers is the entire write surface. No Linear write tools are granted — by construction.Never review a PR/MR twice at the same HEAD SHA. The dedup marker (Phase 2) is load-bearing — without it every tick re-reviews everything.
Never post markers on the Linear issue. Markers live on the PR/MR so dedup is platform-agnostic and shared with
review-epic-gh— a future migration of an epic between trackers keeps its review history.Do not run this skill in the same Claude Code session as
ship-issue/ship-epicworkers. The dual-context perspective is the whole point: the implementer session holds per-PR context, this session holds the epic-wide spec. One session holding both collapses the benefit (and bloats context twice as fast).Always self-cancel the cron on termination (Phase 5), mirroring the
ship-*family contract.Per-tick post gate (conservative by design). The repo convention gates posting reviewer comments behind
AskUserQuestion. Each tick re-derives state from Linear + the VCS alone and no consent marker is stored anywhere, so consent cannot outlive a tick: the gate fires on the first review pass of each tick that has a review to post (Phase 3 step 3), and approval covers every post in that tick. Declining halts the loop and self-cancels the cron. No-op ticks never ask. Stated trade-off: a tick holding a pending review blocks until a human answers — the reviewer session is walk-away between reviews, not during them. (The alternative — dropping the gate and posting unattended with an explicit Hard-Rule exception, mirroring howship-*post status comments unattended — was considered and deferred; see the two-session workflow section of the top-level README.)Concurrency while a gate is open. A blocked
AskUserQuestionholds the Claude Code turn, and/loopruns one turn per session serially — the next 12m cron fire does not spawn a second concurrent reviewer process against the same targets. The pending prompt holds the turn until answered; the cron's interim fire is absorbed by the single-session model rather than racing it. So "walk away during a gate" is non-destructive but stalling: no double-review, no marker race — the loop simply makes no further progress until the human answers, then resumes from the next tick's fresh Phase 2 derivation (any HEAD that advanced meanwhile is naturally re-targeted). This is why the dedup marker (Phase 2) only needs to guard posted reviews, not in-flight ones — there is never more than one in-flight review per session.
Phase 0: Parse input and self-arm
Normalize the arg
Flag extraction (before shape detection): detect and strip a trailing --no-compact flag from $ARGUMENTS. When present, set no-compact mode for this invocation — the compact-between-reviews prompt (Phase 4) is skipped. The flag stays in the raw arg string used for cron arming/matching, so the opt-out survives every subsequent tick. Contract: ../_shared/compact-on-merge.md.
$ARGUMENTS is one of:
- Bare Linear ID (e.g.
PROJ-100) — the parent issue. - Linear issue URL (
https://linear.app/<org>/issue/<id>) → strip prefix, extractTEAM-N.
Anything else (GitHub <owner>/<repo>#<n> refs, comma-lists, milestone: refs, project URLs) → reject with the two supported shapes. This skill requires an explicit Linear parent issue; GitHub parents go through /abc:review-epic-gh.
Fetch parent and validate
mcp__claude_ai_Linear__get_issuewithid: PARENT-ID,includeRelations: true. Read description, labels,statusType.- If
statusTypeiscompleted(Done) orcanceled→ the epic is done, but how to exit depends on whether a cron is armed. Run the cron-entry match rule (below): if a matching entry exists (this is a loop tick), terminate via Phase 5 — emit the reviewed-PRs summary andCronDeletethe entry — so the loop self-cancels instead of zombie-firing "epic done" every 12 minutes. Only when no matching cron exists (a fresh invocation against an already-done epic) print a one-line "epic done — nothing to review" and exit without arming. - Resolve sub-issues:
mcp__claude_ai_Linear__list_issueswithparentId: PARENT-ID, no status filter — Linear's native parent/child relation replaces the-ghfamily's managed task-list fence. If the list is empty → reject: "PARENT-ID has no sub-issues. Run/abc:scaffold-sub-issuesfirst."
If any of these reads fail (MCP error, timeout) → halt with the error verbatim; an unknown epic state is not "nothing to review."
Self-arm the loop
Mirror ship-epic's cron-entry match rule with this skill's name:
A
CronListentry matches when its command string contains<command-name> <raw-arg>followed by a word boundary — the next character (if any) must NOT be alphanumeric,-, or,.<command-name>is the literal slash-command name Claude Code injects (e.g./abc:review-epicvia plugin namespace) — read it from the<command-name>tag, never hardcode. Fallback regex:(?:^|[^A-Za-z0-9])(?:[A-Za-z][A-Za-z0-9_-]*:)?review-epic <raw-arg>(?![A-Za-z0-9_,-]).Note this skill's name is a prefix of
review-epic-gh— the required space between<command-name>and<raw-arg>is what disambiguates: an entry for/abc:review-epic-gh <owner>/<repo>#<n>never containsreview-epicfollowed by a Linear ID, so cross-matching is impossible.
- Match found → no-op (the common loop-tick path), proceed to Phase 1.
- No match →
Skill(skill: "loop", args: "12m <command-name> <raw-arg>"), then proceed to Phase 1 — the first tick also does the first iteration's work.
12-minute cadence sits in the ~10–15 min target: slower than the 6m workers (a review is only actionable once a PR exists or gains commits), fast enough that a worker's pr-open window usually gets its review before the human merges.
Phase 0.7: Resolve platform per child (repo routing)
For each open sub-issue, resolve where its PR/MR lives — same repo: label convention as ship-issue Phase 1:
- Collect label names starting with
repo:. Exactly one expected;0→ skip the child this tick with a[no-repo-label]line in the output;2+→ same skip, noting the ambiguity. - Extract
<name>fromrepo:<name>→ workdir is the<cwd>/<name>/subdirectory. Missing subdir → skip with a[no-workdir]line. - Detect the platform from
git -C <workdir> remote get-url origin: containsgithub.com→gh; containsgitlab.<host>→glab; anything else → skip with[unknown-platform]. Capture the repo identity from the same remote URL: GitHub →<owner>/<repo>; GitLab → the project path (e.g.group/subgroup/project). - Confirm CLI auth (
gh auth status/glab auth status) once per platform per tick. Not authed → halt with the auth command — a reviewer that can read but not post would burn the dedup-free review work every tick.
Skipped children are not errors — this skill is review-only, so it degrades by narrowing coverage and saying so, rather than halting the whole loop the way a worker must. Cache the {workdir, platform, cli, repo} tuple per child for this tick; re-resolve fresh next tick.
Repo routing on every VCS call (load-bearing in multi-repo epics). The loop's cwd is not the child's workdir, so neither CLI may rely on cwd-based repo resolution: every gh call passes --repo <owner>/<repo> explicitly, every glab mr call passes -R <project-path>, and every glab api call substitutes the URL-encoded project path for the project segment (projects/<group%2Fproject>/merge_requests/...) instead of the cwd-resolved :id placeholder — :id would silently resolve against the wrong project (or none) from the loop's cwd.
Phase 1: Bootstrap context (per tick, ≤30KB)
Load fresh each tick (the tick interval keeps the prompt cache warm; re-fetching also picks up mid-epic spec edits):
Parent issue description verbatim — the source of truth.
Per child (from the
parentIdlisting): title,statusType, dependency relations, and the acceptance-criteria section of its description. Relations come from a per-childget_issuewithincludeRelations: true— theparentIdlisting doesn't carry them (same pattern asship-epicPhase 1). They're bootstrap context, not load-bearing; skip the per-child fetches when the tick is already over budget. The acceptance criteria live under a## Acceptance criteriaheading or an- **acceptance:**block, whichever convention the scaffold used. Skip scope / out-of-scope prose unless a review needs it.Dedup against the parent (load-bearing for the budget). Scaffolded child descriptions are usually verbatim ST-sections of the parent PLAN, so naive parent+children assembly roughly doubles the spec bytes. When a child's spec text already appears in the parent description, do not re-include it — cite its location ("ST-4 section of the parent"). Include a child's own description only where it diverges from the parent's section (edited mid-epic). Same rule and reference measurement as
review-epic-ghPhase 1.Merged sibling PRs/MRs: per PR/MR, the summary review comment this skill previously posted (if any) plus a per-file change summary: GitHub —
gh api /repos/<owner>/<repo>/pulls/<n>/files --jq '.[] | "\(.filename) +\(.additions) -\(.deletions)"'(gh pr diffhas no--stat); GitLab —glab api "projects/<encoded-project-path>/merge_requests/<iid>/diffs" --paginatefor the file list (new_pathper entry; the API doesn't expose per-file +/− counts — fall back toglab mr diff <iid> -R <project-path>only when a review needs a specific decision).Pending children's acceptance criteria — the forward-compat lens: what will later sub-issues exercise? (Subject to the same parent-dedup rule.)
Budget: keep the assembled context under ~30KB. When over, trim in this order: (1) merged-sibling full diffs → per-file change summary only, (2) per-file summaries → PR title + summary-comment only, (3) pending children's criteria → titles only. Never trim the parent description or the under-review child's acceptance criteria.
Phase 2: Enumerate review targets (dedup)
List candidate PRs/MRs: for each open child that survived Phase 0.7, take the child's
gitBranchName(Linear provides this on the issue object) and the issue's attachments/links, then: GitHub —gh pr list --repo <owner>/<repo> --state open --head <gitBranchName> --json number,headRefOid,url; GitLab —glab mr list --source-branch <gitBranchName> -R <project-path> --output json.0 matches —
gitBranchNameis Linear's suggested slug; the developer may have named the real branch differently. Fall back to the child's Linear attachments/links: extract a PR/MR URL and resolve it directly (gh pr view <url> --json number,state,headRefOid,url/glab mr view <url> --output json). Only when both the branch lookup and the attachments miss is the child[no-pr-yet]. 2+ matches (branch reuse, closed-and-reopened) — take the first open PR/MR and flag the ambiguity in the tick output (e.g.[reviewed, 2 PRs on branch — picked #N]). The same 0/2+ rule applies to URLs from the attachments fallback.For each candidate, read its HEAD SHA (GitHub:
headRefOid; GitLab:diff_refs.head_shafromglab mr view <iid> -R <project-path> --output json) and fetch its top-level comments (GitHub:gh api /repos/<owner>/<repo>/issues/<pr>/comments; GitLab:glab api "projects/<encoded-project-path>/merge_requests/<iid>/notes" --paginate). If a<!-- review-epic:reviewed-at:<sha> -->marker matching the current HEAD SHA exists → skip this PR/MR with no further API calls. This dedup check is the only cost for unchanged PRs.A PR/MR whose markers all reference older SHAs has new commits → it's a review target (the stale marker stays; history is the audit trail).
No targets this tick → print the one-line no-op summary (Phase 6) and return.
Phase 3: Review each target
For each target, in sub-issue order:
Fetch the diff:
gh pr diff <n> --repo <owner>/<repo>orglab mr diff <iid> -R <project-path>.Spawn the existing
abc:reviewersubagent (Agenttool,subagent_type: reviewer) — do not editagents/reviewer.md; extend its input via the prompt. Pass:- The unified diff (its standard input contract), plus
- Cross-cutting epic context from Phase 1: the parent spec, this child's acceptance criteria verbatim with their sub-issue ID, merged-sibling decisions, and pending children's criteria — with the instruction to additionally evaluate (a) which acceptance bullets this diff satisfies/misses, citing them by sub-issue ID and bullet, and (b) forward-looking flags where a pending sub-issue will exercise this code differently.
Per-tick post gate (first review pass of this tick only): show the assembled review — inline comments plus summary — via
AskUserQuestionfor a single go/no-go. Approval covers this and every subsequent post in this tick (see Hard Rules — consent can't persist across ticks because no consent marker is stored); decline → halt the loop andCronDeletevia the Phase 0 match rule. Later passes in the same tick skip this step entirely.Post the review:
- GitHub — one call:
POST /repos/<owner>/<repo>/pulls/<pr>/reviewswithevent: COMMENT, the reviewer's inline comments as thecommentsarray, and the summary as the review body. - GitLab — no batch review API: post each inline comment as a positioned discussion via
glab api "projects/<encoded-project-path>/merge_requests/<iid>/discussions" -f body=<text>with the full six-fieldpositionobject (API doc):position[base_sha]/position[start_sha]/position[head_sha]from the MR'sdiff_refs,position[position_type]=text(literal), andposition[new_path]+position[new_line]for the new side of the diff (useold_path/old_linefor deletion and context-only lines). Omitting any of the last three returns a generic 400 "the position is invalid" that's easy to mis-attribute todiff_refs. Then the summary as oneglab mr note <iid> -R <project-path> --message <body>.
Post-failure guard (both platforms): if any posting call returns 4xx, halt this PR/MR's pass without dropping the step-5 dedup marker and surface the response body in the tick output. The marker is only written after every post for that target succeeds — otherwise a malformed
positionwould burn the review and mark the HEAD as reviewed, and the loop would never retry it.The summary body has explicit structure either way:
- (a) Inline comments — one-line index of what was flagged.
- (b) Spec cross-reference — "satisfies ST-N bullet X … misses ST-N bullet Y", citing specific acceptance bullets by sub-issue ID, never free-text paraphrase.
- (c) Forward-looking flags — "ST-N+1 will exercise this path differently; current shape will need rework", citing the pending child.
- GitHub — one call:
Drop the dedup marker as a marker-only top-level comment on the PR/MR (the marker is the entire body, matching the
<!-- ship-issue:* -->marker-only convention):gh pr comment <n> --repo <owner>/<repo> --body '<!-- review-epic:reviewed-at:<sha> -->'orglab mr note <iid> -R <project-path> --message '<!-- review-epic:reviewed-at:<sha> -->'where<sha>is the HEAD SHA the review was produced against. Never on the Linear issue.Compact-between-reviews boundary — see Phase 4 before starting the next target.
Phase 4: Compact between reviews
Consumer of ../_shared/compact-on-merge.md at the "between two PR reviews" boundary: after a review pass completes (marker dropped) and one or more un-reviewed targets remain in this tick's queue, print — as the last output of the tick —
🗜 Review of <pr-or-mr-url> posted. Run /compact now to free context before reviewing <next-url>.
then end the tick (same end-the-wake rule as the workers — the dedup markers persist on the PRs/MRs, so the next tick's Phase 2 picks up exactly the remaining targets). Skip in no-compact mode, and when the just-reviewed PR/MR was the only/last target. At most once per tick.
Phase 5: Termination
On every tick, before Phase 1, re-check the parent:
- Parent
statusTypeiscompleted(Done) orcanceled→ terminal. Print a summary of all PRs/MRs reviewed by this loop (scan for this skill's<!-- review-epic:reviewed-at:* -->markers across the children's PRs/MRs) with thread links,CronDeletethe loop's own cron entry via the Phase 0 match rule, and exit cleanly. This lands within one tick of the parent transitioning to Done. - User-invoked
Ctrl-C/ loop cancellation needs no cleanup — every tick re-derives from Linear + the VCS; markers already posted keep dedup correct on any future re-arm.
If CronDelete fails, print a note ("couldn't auto-cancel; run /loop cancel") and continue — the summary is the authoritative surface.
Phase 6: Output contract (every tick)
/review-epic tick <timestamp>
Parent: PROJ-100 "<title>" (open, 3 of 6 children merged)
[reviewed] PR #43 (PROJ-103) 5 inline, 2 spec-refs, 1 forward flag
[skipped] MR !12 (PROJ-104) marker matches HEAD abc1234
[no-pr-yet] PROJ-105, PROJ-106
[no-repo-label] PROJ-107
Next tick: /loop 12m /abc:review-epic <raw-arg>
One line on no-op ticks: no-op tick — no new commits on any child PR/MR.
Notes on persistence
Stateless across sessions — Linear and the VCS are the sources of truth. The <!-- review-epic:reviewed-at:<sha> --> markers on the PRs/MRs are the entire dedup store; closing the terminal mid-loop is safe, and a force-push that discards a marker simply triggers a benign re-review. Append markers, never edit them. Because the markers live on the PR/MR rather than the tracker, the dedup state is identical regardless of which review-epic variant posted it.