| name | prepare-review |
| description | Rebase on the default branch, push, open the PR/MR, and draft a description that tells reviewers WHY the change exists. Use when opening an MR/PR or creating a review. |
Prepare Review
Draft a description whose job is to convey why the change exists and how it addresses the current problem. The proposed code stands on its own — the diff shows what changed; the description supplies the reason. The rest routes reviewer attention in order of criticality so they get maximum value from whatever time they can spend.
Audience assumption — ELI5 / assume unfamiliarity. Write for a competent developer who has never seen this system. Explain what it does today and why this change exists in plain language; spare a sentence or two to establish the business/system context up front — that investment is almost always worth the words. Skip the parts the diff already speaks to (which loop does what, which file moved where).
Default to terse on everything else. Justifications, hedges, asides, and "we used to / now we" framing add bytes without adding signal. Trim aggressively on the first pass; reviewers will ask for more if they want it. The shape to aim for: a Context section that earns its 30-60 seconds, then a tight Review guide. Recency-polish bullets, decisions no one was going to question, and author-todo lists all belong somewhere else — see Step 3 "What to avoid".
CR = change request: a pull request on GitHub, a merge request on GitLab. Pick the
forge tool by the origin remote.
%%{ init: { 'look': 'handDrawn' } }%%
flowchart TD
Start(["/prepare-review"]) --> CR{Open CR?}
subgraph "Step 1: Gather the changeset"
CR -->|No| OnDefault{On default branch?}
CR -->|Yes| Behind
OnDefault -->|Yes| Branch["Create branch + commit, re-gather"]
OnDefault -->|No| Ahead{Commits ahead?}
Ahead -->|No| Commit["/anchor:commit, then re-gather"]
Ahead -->|Yes| Review["Review gate: branch vs default"]
Review -->|Clean| MakeCR["Open draft CR"]
Review -->|fix-now| Commit
Branch --> Review
Commit --> MakeCR
MakeCR --> Behind{Behind main?}
Behind -->|Yes| DoRebase["Rebase + force-with-lease"]
Behind -->|No| StateCheck
DoRebase --> StateCheck["Sanity-check vs CR head"]
end
subgraph "Step 2: Resolve questions"
StateCheck --> Why["Ask the WHY + open decisions"]
end
subgraph "Step 3-4: Draft & write"
Why --> Recency["Anti-recency check"]
Recency --> Draft["Draft Context + Review guide"]
Draft --> Out{Disposition?}
Out -->|Write| Forge(["Push to CR"])
Out -->|Copy| CopyOnly(["Print for paste"])
Out -->|Edit| EditCh{moor?}
EditCh -->|Yes| Moor["One-off moor review"]
EditCh -->|No| Recency
Moor --> Recency
end
Execute quietly — do the thinking, don't show it
Follow the execute-quietly discipline: ${CLAUDE_PLUGIN_ROOT}/guides/execute-quietly.md. It bites hardest here because the reviewer reviews A → B — the net change from base to final state — not the path you took to get there. The session's pivots, dead ends, and intermediate iterations are development process, not the change under review; narrating that process — to the user, or into the description — is this skill's recurring failure. Step 1's recon and Step 4's diff/moor review each fold into one call precisely so there is nothing to narrate between them: run the call, read the result, move on.
The entire visible output of a run is:
- a decision the script flagged that needs the user (
BEHIND, a STATE mismatch, a CR_CREATE_ERROR);
- the resolved CR URL, once;
- the Step 2 questions;
- the drafted description, the Step 4 options, and the final verdict.
Everything else is internal: the per-step recon plumbing ("origin is GitLab, 1 ahead, no template, tree clean" — the script already ran it), the Step 3 anti-recency disposition (Centerpiece / Footnote / Cut scratch that shapes the draft, never output), and session-internal A → B history (which also gets cut from the description as a "Drift artifact" — see Step 3). Reserve prose for the steps that need your judgment or the user's input — the Step 2 prompts, drafting in Step 3, presenting options in Step 4.
Step 1: Gather the changeset
Run the gather script once. It performs Step 1's deterministic recon and the safe default-path setup — detect the forge, resolve or auto-open the draft CR, count the gap to the default branch, capture the current description as the Step 4 diff baseline, check local state against the CR head, read the project template and anchor.* config — then prints one KEY=value block on stdout:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/prepare-review.sh"
Read the block and act only on what it surfaces; don't re-run the individual probes. The keys:
| Key | What to do with it |
|---|
RESOLVED_VIA | cwd (inferred from the working directory) or repo (an explicit --repo was honored) — see "Operating against a non-cwd repo" |
FORGE | github / gitlab picks the CLI for the rest of the skill; none → the URL-free skip-deep-links path |
DEFAULT_BRANCH | substitute for main in the diff/log commands below |
ON_DEFAULT_BRANCH=1 | HEAD is the default branch — there's no branch to open a CR from. With work to review, NEEDS_BRANCH=1 routes through branch creation first; clean with nothing ahead → nothing to review, stop |
AHEAD=0 | nothing ahead of the default branch — NEEDS_COMMIT=1 chains to /anchor:commit (see below); otherwise say so and stop |
NEEDS_BRANCH=1 | on the default branch with work to review — a feature branch must exist before a CR can be opened (see "Get to a reviewable commit") |
NEEDS_COMMIT=1 | no reviewable commit yet — chain into /anchor:commit before continuing (see "Get to a reviewable commit") |
NEEDS_REVIEW=1 | commits are ahead, no CR yet, and unreviewed — the script stopped short of pushing. Run the pre-push review gate, then re-invoke with --reviewed on a clean verdict (see "Review before the first push") |
BEHIND=<n> | >0 → run the rebase dialog below |
CR_URL / CR_IID | the resolved or freshly-opened draft — deep-link target and write target (empty on skip-deep-links) |
CR_DRAFT | gates the post-rebase force-push (see below) |
STATE | match → proceed; anything else → surface and stop (see "Act on STATE") |
CURRENT_DESC_PATH | baseline the Step 4 diff reads (empty on skip-deep-links) |
TEMPLATE_PATH | project CR template to compose into (Step 3) |
ANCHOR_CONFIG | anchor.* keys to apply (Step 3), as JSON |
FILE_ANCHORS | precomputed sha1(path) per changed file for GitLab deep links (Step 3), as JSON |
If the block carries a CR_CREATE_ERROR=… line, the draft-open hit an auth or push failure — surface it and ask the user to refresh credentials; do not fall back to the URL-free path (the fail-fast-on-auth rule).
Operating against a non-cwd repo (worktree isolation)
When the CR lives in a repo other than the session's cwd (you're in repo A, the CR is in repo B), don't drive B off cwd: resolve the target, decide direct-vs-worktree once up front with bash "${CLAUDE_PLUGIN_ROOT}/scripts/worktree.sh" setup <path-to-B-checkout>, thread the resolved <CHECKOUT> through every later command (the harness resets cwd between Bash calls), and tear the worktree down when the flow ends. The full procedure — name resolution via resolve-target.sh, the worktree.sh setup/teardown lifecycle, and the git -C / -R / glab api threading rules — is in ${CLAUDE_PLUGIN_ROOT}/guides/worktree-isolation.md; consult it whenever B ≠ cwd.
When the target is just the session cwd (no non-cwd repo in play), skip all of this — everything below is plain git / gh / glab against the working directory.
Get to a reviewable commit (NEEDS_BRANCH / NEEDS_COMMIT)
prepare-review is meant to run from any state. A CR needs a commit on a feature branch ahead of the default branch; when that doesn't exist yet, the script says so (instead of letting glab mr create / gh pr create dead-end on a raw "Could not find any commits between origin/<default> and <branch>") and the skill delegates to get there. Three cases, keyed off the block:
-
NEEDS_COMMIT=1, NEEDS_BRANCH=0 — on a feature branch, work uncommitted. Chain into /anchor:commit: let it run its flow (tests, staging, message, the visual diff review). Once the commit lands, re-gather (below).
-
NEEDS_BRANCH=1, NEEDS_COMMIT=1 — on the default branch, work uncommitted. Still chain into /anchor:commit — it detects the default branch and offers to create the feature branch (named from the subject it drafts), committing onto it. Take the recommended branch option; the commit lands on the new feature branch, not the default branch. Then re-gather.
-
NEEDS_BRANCH=1, NEEDS_COMMIT=0 — on the default branch with commits that exist locally but only on the default branch (e.g. committed to main by habit, never pushed). Nothing to commit; the commits just need their own branch. Slug the latest subject (git log -1 --format=%s, same convention /anchor:commit uses), confirm the name with the user, then move them off the default branch:
git branch <slug>
git reset --hard origin/<default>
git switch <slug>
This is safe because the local default branch was only ahead of origin/<default> — the reset drops those commits from the default branch, but they're preserved on <slug>. Then re-gather.
After the branch/commit lands, re-run the gather script so it resolves the now-creatable CR. Which form depends on how you got here:
-
You chained /anchor:commit (NEEDS_COMMIT, either case) — its Step 4 already ran the visual review, so pass --reviewed; the second run opens the draft without a redundant review:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/prepare-review.sh" --reviewed
-
You moved commits to a branch (NEEDS_BRANCH=1, NEEDS_COMMIT=0) — those commits were never reviewed, so re-gather without --reviewed; the script reports NEEDS_REVIEW=1 and the gate runs before the push (see "Review before the first push"):
bash "${CLAUDE_PLUGIN_ROOT}/scripts/prepare-review.sh"
The second run is on a feature branch with a commit ahead; with --reviewed it auto-opens the draft CR and returns a normal block (NEEDS_BRANCH=0, NEEDS_COMMIT=0, a resolved CR_URL). Proceed from there into the rebase / drafting flow as usual. If the second run still reports NEEDS_COMMIT=1 — the user declined the commit, or it produced nothing ahead — say so and stop; don't loop.
Review before the first push (NEEDS_REVIEW)
NEEDS_REVIEW=1 means commit(s) are ahead of the default branch, no CR is open yet, and the changeset hasn't cleared the review gate — so the script stopped short of pushing. The first push is what opens the draft CR, and that push is the moment the code leaves the machine: it must be reviewed first. (/commit's Step 4 gates the local commit, but a commit made with raw git — or one whose Step 4 was skipped — would otherwise reach the remote unreviewed the instant the draft auto-opens.)
Run the same branch-vs-default visual review /commit --preview cr uses, as a background Bash call (run_in_background: true) — the wrapper blocks until the difftool closes:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/review-diff.sh" --full
Read the verdict with the BashOutput tool — not tail / $(...), which trips the command-substitution gate. The REVIEW_VERDICT / REVIEW_OUTPUT contract is identical to /commit Step 4:
-
0 (clean) — re-invoke the gather script with --reviewed to push and open the draft CR, then proceed into the rebase / drafting flow:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/prepare-review.sh" --reviewed
-
1 (fix-now) — list the fix-now comments (REVIEW_OUTPUT's .comments where action == "fix-now") and fix them via /anchor:commit, which amends the still-unpushed commit and re-reviews. Then start Step 1 over. Do not push.
-
2 / 3 / absent — no clean verdict, so this is not approval: don't push on it. With moor on PATH the review didn't complete (unreviewed hunks, closed early, or no verdict written) — surface what happened. With moor not on PATH the wrapper showed the diff in your git-configured difftool, which writes no verdict, so absent here means "shown, but unconfirmed" — ask the user to confirm it's good before re-running with --reviewed, or what to change. Either way, don't read silence as approval.
This gate fires only for the first push of an as-yet-unreviewed branch — once the CR exists, subsequent pushes (a rebase force-push) are gated separately by CR_DRAFT. When prepare-review chained through /anchor:commit above, Step 4 already reviewed the commit, so that path passes --reviewed and never sees NEEDS_REVIEW (see "Get to a reviewable commit").
Why auto-open is the default. A draft CR is cheap and reversible: it requests no review, the push already triggered any branch-level CI, and self-assign notifies only you. The deep links are the load-bearing part of the description, and a placeholder-only draft is broken on arrival — opening the real CR first is what makes the description useful. Auto-open still fires only after the review gate clears: the script pushes on --reviewed, which the skill passes once the branch-vs-default review returns clean (see "Review before the first push"). The script does not sniff for a "merges direct to main, never opens CRs" convention, because there's no reliable signal for it. One case gives way to the skip-deep-links path:
- User asks not to open one — the repo merges direct to
main without CRs, or the CLI's default forge instance is wrong for this repo. Re-run with --no-open to proceed URL-free; or, if they'd rather open the draft themselves, pause until they confirm one is open, then re-run so the script resolves its URL.
(When ON_DEFAULT_BRANCH=1, the script doesn't auto-open either — but that routes through branch creation, not skip-deep-links; see "Get to a reviewable commit" above.)
Rebase on main when BEHIND > 0
BEHIND=0 → skip this section. Otherwise the branch needs origin/<default> before it can merge — every conflict with intervening commits has to be resolved before the CR can land, and doing it now (while the change is fresh) is cheaper than after review when context has gone cold. Secondary: deep links anchor to lines in the current diff, so a behind-default branch points at content that won't compose cleanly at merge time. Ask:
Branch is <BEHIND> commits behind origin/<default>. Rebase now? [yes / skip]
yes — run git rebase origin/<default>. On conflict, resolve in place: read both sides of each conflicted region, pick the resolution that preserves the intent of both changes (not just one side), git add the resolved files, then git rebase --continue. Loop until the rebase completes. Surface to the user when intent is genuinely ambiguous — two competing changes to the same logic, semantic conflicts the textual markers don't show, a rename colliding with an edit. Don't guess in those cases; show the conflict and ask. If a hook fails mid-rebase, surface the failure rather than retrying with --no-verify.
skip — proceed with the current branch state. Note that deep links may render against lines that have shifted by merge time.
A rebase rewrites history, so the push that follows is a force-push. Gate it on CR_DRAFT — the author's declared review state, which is reliable in a way that inferred engagement signals (note counts, reviewer lists) are not:
CR_DRAFT=true — mutable history is the norm (anchor opens CRs as drafts for exactly this reason). Rebase and force-push with lease without further ceremony.
CR_DRAFT=false (ready) — a reviewer may already be looking, and there's no reliable signal for whether they have. Force-pushing over commits they've seen destroys their "changes since you last looked" diff and marks inline threads outdated. Engagement signals are advisory context for the prompt (reviewers / discussion count via glab api projects/:fullpath/merge_requests/<CR_IID> | jq '{reviewers, user_notes_count}' or gh pr view --json reviews,reviewRequests,comments), but the decision is the user's — ask before proceeding:
This CR is marked ready. Rebasing now force-pushes over commits a reviewer may have seen, which resets their incremental diff. Rebase anyway? [yes / skip]
After a successful rebase (and the review-activity check above), force-push with lease so the open CR updates to the rewritten history:
git push --force-with-lease
--force-with-lease rejects the push if anyone else has pushed to the branch since you last fetched — that's the safety against clobbering a coworker's commit. If it rejects, fetch, inspect, and ask the user before escalating. If the rebase itself aborts (uncommitted changes blocking it, a rebase already in progress, missing remote), surface the error and stop.
Read the diff and commit history
Substitute DEFAULT_BRANCH from the block for main:
git log main..HEAD --oneline
git diff main...HEAD --stat
git diff main...HEAD
(AHEAD=0 already routed you — chained to /anchor:commit on NEEDS_COMMIT=1, or stopped otherwise — so a run that reaches here is ahead of the default branch.)
Act on STATE
The deep links you'll generate point at specific lines of the current CR diff, so drafting against stale state ships a description that renders against content the reviewer can't see. STATE=match → proceed. Otherwise stop and surface — do not draft:
dirty — uncommitted changes in the working tree. Common cause: a multi-step cleanup whose steps were each confirmed in conversation but never committed. Ask the user whether to amend (or new-commit) and push before drafting.
head-mismatch — local HEAD ≠ CR head: the user's expected push hasn't landed, or you're on the wrong branch. Common cause: a force-push blocked by a hook, or a no-op push because the working tree was never committed. Surface the SHA mismatch (LOCAL_HEAD_SHA vs CR_HEAD_SHA) and ask.
dirty+head-mismatch — both of the above.
State drift between conversation belief and repo reality is silent and expensive. The script's read-only state check catches it; missing it ships a broken description.
Step 2: Resolve open questions before drafting
The description needs the author's motivation and any decisions still in flux. If something would otherwise come out of the description as a hedge or an open offer ("happy to bump version if you'd like", "open to adding a test", "could split this into two CRs"), it's a question — ask it now, then draft. The CR description is not a place to negotiate. Any ambiguity is a reason to defer drafting, not park inside it.
Before drafting, scan for these common ambiguities and ask the user about each one that applies:
- Why — what problem this solves and why it matters (the prompt below)
- Audience / threat model — for security or visibility changes, who is the affected population? "Anyone who can read X" is too vague. Name the population concretely: anyone inside the network, anyone with read access to the source, the on-call team, etc.
- Scope decisions — should this be split? Squashed? Feature-gated? Released alongside something else?
- Ordering dependency — must this CR land after another one (a shared library before its consumer, a config that points at the consumer)? Don't infer this aggressively — take it when the user says so, or when they've had you open the CRs as an ordered chain this session. If so, capture the predecessor CR (its iid/number, and project when cross-repo); Step 3 records it in the description and Step 4 sets the forge dependency.
- Surface decisions — version bump, deprecation timeline, migration guidance for downstream callers
- Verification gaps — anything you can't actually test from the working environment (UI, downstream consumers, prod-only behaviors). Surface these to the author so they can plan how/when to verify before merge. These are author homework — they do not become checklist items in the description.
Wait for answers to all of them before drafting. A description shipped with parked questions is worse than one shipped a turn later.
If the only open item is the WHY, ask:
What problem does this solve, and why does it matter?
The diff shows what changed — I need you to tell me why. A sentence or two is enough. For example:
- "Users were getting 500 errors when their session expired mid-checkout"
- "We need to support the new billing API before the March deadline"
- "The old approach couldn't scale past 10k concurrent connections"
Draft the WHY only from what you were actually given — the author's answer, the diff, or a cited doc. A correct-but-narrow WHY always beats a speculative-but-broad one. When the WHY comes in thin, that thinness is the signal to ask (or confirm your reading) — not to fill. Don't elaborate the motivation past the source, and don't invent supporting detail to prop it up: not a surrounding narrative (a single named artifact — a script, a job — is not evidence of a category or a recurring practice), and not technical mechanics the author never stated, however plausible. A thin, sourced WHY ships; a rich, invented one is the "invented current state" failure (Step 3, "What to avoid").
Step 3: Draft the description
Honor an existing forge template
TEMPLATE_PATH from Step 1's block names the project's CR template (.gitlab/merge_request_templates/*.md or .github/pull_request_template.md); empty means none. When set, it's the team's required scaffolding — compose into it, don't replace it. Fill the sections it defines; preserve the reviewer-facing structure (headings, approval checklists) verbatim while stripping author-facing scaffolding (a section's placeholder / helper text, dev-time reminder links); answer any justification checkbox with fact, not meta-commentary; and supply anchor's prose where it leaves prose to the author. On a structure conflict the team template wins. The composition rules are documented in the "Honoring a project's forge template" section of templates/cr-description.md.
Honor anchor.* config
ANCHOR_CONFIG from Step 1's block holds the project + global anchor.* keys as JSON ({} when none). The keys come back lowercased (anchor.reviewbudgetmins); match them case-insensitively. Apply the keys relevant to a CR description; absent keys keep anchor's defaults — never invent a value:
anchor.reviewBudgetMins — the minutes of focused review you expect this CR to get (an input, not a length cap; unset behaves like ≈10). A tight budget (≈5) leads with the essentials and cuts asides hard; a generous one (≈30) keeps more supporting context and depth. This steers how aggressively the anti-recency and "What to avoid" passes cut — it steers what to include, never the register; a tight budget is not license for punchy or marketing tone (see Tone).
anchor.workTrackerBaseUri — when the user mentions a ticket (a full tracker URL, or a bare id), link it in the description: use a full URL as-is, or build <base-uri><id> from a bare id. No mention, no link — don't scrape the branch or prompt.
anchor.crRules, with forge overrides anchor.mrRules (GitLab) / anchor.prRules (GitHub) — an extra rule layered onto the default CR-description rules. Pick the forge by the origin remote: use mrRules / prRules when set, else fall back to crRules.
See ${CLAUDE_PLUGIN_ROOT}/guides/configuring.md for the full key set.
Anti-recency-bias check (do this before drafting Context)
Recency bias is the dominant failure mode here: detail you spent the last hour polishing carries disproportionate weight in working memory and anchors the Context section even when the CR is about something much larger. The headline is what the branch was for — usually the first commit, not the last. Mechanical fix:
- List the 3-5 things you most recently iterated on (in this session, or in the last few commits). Be concrete: "polished two chip labels", "rewrote cache-key construction".
- Write a disposition for each against would a fresh reviewer consider this central? — Centerpiece (lead Context), Footnote (one bullet in Review guide), or Cut.
- If everything came out "centerpiece", redo it. Follow-up commits are footnotes. If a follow-up deserves co-headline status, it's actually a separate CR.
Run this check internally — the disposition list is scratch that shapes the draft, not output. The only thing the user sees from this step is the resulting draft (see Execute quietly at the top).
Title
A concise imperative phrase (under 72 characters) that captures the change. Same rules as a good commit subject line.
Body structure
Draft the description following the section template in templates/cr-description.md: Context, Review guide, Approach & trade-offs (rare), Testing (rare), and Validation (when correctness is best shown by real-world use). The template owns the shape; the guidance below owns the technique for realizing it.
Use these heading names verbatim — Context / Review guide / Approach & trade-offs / Testing / Validation are canonical, not paraphrasable; reviewers scan for them. Omit a section that doesn't apply; never rename one. (The template spells out why.)
Ordering dependency (when Step 2 captured one). Near the top of Context, add a bare, autolinking reference — Depends on !<iid> (GitLab) / Depends on #<num> (GitHub) — and a line that it must merge first. On GitHub, and on any GitLab fall-back (see Step 4), this prose is the only ordering signal, so say plainly that the forge won't enforce it.
Deep-link construction (Review guide). Always deep-link to the actual line, not just the file — reviewers should be one click away from the hunk. The forge-specific anchor construction (GitLab sha1 path-hashes from FILE_ANCHORS, GitHub sha256) lives in ${CLAUDE_PLUGIN_ROOT}/guides/cr-formatting.md.
Pipeline artifacts — fetch, reason, include. When the CR or its commit's pipeline produces an artifact that bears on review, fetch it, reason about what it shows, and include the pertinent excerpt (collapsed if long; see ${CLAUDE_PLUGIN_ROOT}/guides/cr-formatting.md). Don't describe a change whose effect the pipeline already rendered without showing it.
Validation — ask, don't guess. The Validation section records evidence of real-world use, and applies only when the diff plus the rendered artifact don't settle correctness on their own — a shared component consumed by other repos, or a tool/automation whose value is the work it drives. When those signals fire, ask the author what validation looks like rather than guessing a checklist row; skip the section entirely when the diff plus CI already settle it. The detection signals, the prompt, and the evidence-row format live in the template's Validation section (templates/cr-description.md).
Tone
Conversational and informal. Reviewers are colleagues, not stakeholders — write like you'd talk through the change at a desk, not like a status report. Sentence fragments are fine. Mid-thought asides in parens are fine. Don't sweat capitalization on tier labels and short bullets, and don't sweat trailing punctuation on fragments — core change, lives here reads as well as Core change, lives here. and a closing period on a one-line bullet adds nothing. Save the more formal register for the Why paragraph where context actually matters; everywhere else, default low-friction.
A tight review budget is not license for marketing punch. A low anchor.reviewBudgetMins (≈5) steers what you include — lead with essentials, cut asides — it does not loosen the register into hype. Buzzwords, reviewer flattery ("you know this system cold"), and punchy taglines cost attention without earning it. Terse means fewer words, not louder ones; the no-hyperbole discipline in "What to avoid" governs at every budget.
Formatting
Presentation is a primary concern, not a finishing pass. Before drafting, ask: what shape is this data, and what visualization fits it? — then pick deliberately; a diagram that doesn't match the data shape is worse than none. The full technique lives in the bundled ${CLAUDE_PLUGIN_ROOT}/guides/cr-formatting.md: the data-shape → visualization menu, the prose bold/italic/backtick conventions (with the forge-autolink bare-token exception), collapsible <details>, mermaid diagram and before/after recipes, and the screenshot-capture workflow. Consult it while drafting. The render-time traps that break any forge markdown — character escaping, nested fences, mermaid-fence placement, the <details> blank-line rule — stay in ${CLAUDE_PLUGIN_ROOT}/guides/markdown-gotchas.md.
What to avoid
Categories of cruft. If something fits one of these, it doesn't belong in the description.
- Drift artifacts — recency-polish bullets (run the anti-recency check above; cut anything dispositioned "Footnote" or "Cut"); implementation-history phrasing that frames the change by what it replaces (
now-deprecated, previously, formerly, the old, we used to, used to be); past-or-future speculation ("this was originally X", "will eventually become Y", "could one day be extended to Z"); invented incidents, audiences, or current state — generalizing one named artifact into a category, or inventing technical mechanics to justify a claim, both count. Every factual claim about prior workflow or current state needs a citable source — something the user said, the diff shows, or a doc establishes. A claim carried over from an existing description or a prior draft is not pre-sourced — re-verify it against the diff before repeating it; a plausible-sounding inherited claim is often the one the diff contradicts. (Exception: a deprecation CR whose entire purpose is announcing the deprecation — there, naming the deprecated thing is the point.)
- Loaded framing — temporal blame, size-minimizers, self-congratulatory adverbs, defensive softeners; the full discipline with examples lives in
${CLAUDE_PLUGIN_ROOT}/guides/loaded-framing.md. The factual claim almost always survives the trim.
- Things the diff already shows — flat lists of files changed without criticality ordering; re-stated commit messages; implementation details obvious from reading the code; dead-end approaches you tried and abandoned; anything a reviewer could derive from one click on a deep link; Review-guide bullets that narrate a hunk in prose instead of pointing at it (the point-of-generation rule lives in the Review guide section of
templates/cr-description.md).
- The "stop before a code block" trigger. When you're about to put a multi-line code block in the description, stop and ask: does this duplicate what the diff already shows? It almost always does — and it drifts from the diff the moment the code changes. Drop the block; use inline single-token backticks (
`SomeType`, `--some-flag`) plus a deep link to the lines. Name the params, flags, and internal types the diff already carries in inline backticks, not in prose that re-describes them. (The exceptions stay as documented under Formatting: sample output, a created-file tree, a terraform plan — content the diff does not carry.)
- Things that belong elsewhere — author-only checklists (eyeball staging, fill a spot-check matrix, confirm rendering, drive a fixture table — these live in a personal task list, a self-review pass, or CR comments, not the description body); changelog content the CR already ships; decisions a reviewer wouldn't have questioned (Approach & trade-offs is for contested choices); testing claims CI already provides; reference-grade explanation of standing behavior that grew while drafting — with the author's sign-off, split it into the repo docs and link it from the description (high bar; see the bundled
${CLAUDE_PLUGIN_ROOT}/guides/description-vs-docs.md).
- Step-2 leftovers — hedges, offers, or open questions ("happy to / open to / let me know if"); unsubstantiated verification claims ("verified" / "tested" / "confirmed" for things you didn't actually exercise). If ambiguity is still in flux, defer drafting; don't park it in the description.
- Boilerplate — generic openings ("This change updates…"); assuming domain knowledge the reviewer doesn't have.
The single exception to "no verification content in the description body" is the Validation evidence row — see the Validation section in templates/cr-description.md. That row records evidence of real-world use, not a todo.
Step 4: Output
Write the drafted description to a temp file ($(mktemp -u "${TMPDIR:-/tmp}/cr-desc-draft.XXXXXX").md) — both the diff presentation here and the moor edit loop below read it.
Present the change. When CURRENT_DESC_PATH from Step 1's block is non-empty (any CR exists — including a freshly-opened draft, whose --fill baseline makes the draft render as all-additions), show what changed rather than the whole body — diff the draft against that baseline and present it in a fenced diff block:
git --no-pager diff --no-index <CURRENT_DESC_PATH> <draft-path>
When CURRENT_DESC_PATH is empty (the skip-deep-links path, where no CR exists), display the full description in a fenced code block instead. Use markdown formatting appropriate for the platform (GitHub, GitLab, etc.). After presenting, offer to write the description back to the forge — see the final prompt below.
Output checklist (verify before presenting)
The description gets pasted into a markdown renderer, so rendering bugs are user-visible. Walk the general rendering gotchas in the bundled ${CLAUDE_PLUGIN_ROOT}/guides/markdown-gotchas.md — character escaping (~/$/_/*), nested code fences, mermaid blocks, collapsible <details>, tables in lists — then these CR-description-specific checks:
- Backtick coverage is generous — except for forge-autolink tokens. Re-scan the description for grep-bait: env vars (
$FAMILY, $CI_PIPELINE_CREATED_AT), config keywords (extends:, needs:, on_success, manual, allow_failure), job/product/feature suffixes that match identifiers in the diff, CLI flags, file paths. The "if a reader might paste it into a terminal" test is more permissive than "code identifier only" — err generous. But scan separately for CR/issue refs (!148, #42), commit SHAs, and user @mentions — these must be bare text to autolink; backticks render them as inert code spans.
- Inline single quotes around
'all' / 'true' style values read fine in prose but lose their distinguishing weight in scan-mode. Convert literal dropdown/enum values to backticks.
Then ask the user how to proceed using the AskUserQuestion tool — an arrow-key-selectable prompt beats a free-text [write / edit / copy] prompt the user has to type out. Use header Disposition and these three options (in order, so the default lands on Yes):
- Yes (write) — push the description to the open CR.
- No (copy only) — print the description for the user to paste themselves.
- Edit — mark up the change inline (moor) or in chat, then re-present.
Map the user's selection to the actions below:
-
Yes (write) (default) — push the description to the open CR. Editing a description is reversible, so this is the low-friction default. On 401/403 or similar auth failure, surface the error and ask the user to refresh credentials — do not silently fall back to copy-only. The <draft-path> is the temp file you wrote at the top of Step 4.
- GitHub:
gh pr edit --body-file <draft-path>.
- GitLab: use the API form
glab api -X PUT projects/:fullpath/merge_requests/<CR_IID> -F "description=@<draft-path>" — glab mr update -d doesn't accept a file. See the bundled forge cookbook (${CLAUDE_PLUGIN_ROOT}/guides/forge-cookbook.md) for the full canonical invocation.
When operating against a non-cwd repo these are the write path, so retarget them per "Operating against a non-cwd repo": add -R <owner/name> to gh pr edit, and substitute the URL-encoded project for :fullpath in the glab api PUT (plus --hostname for self-hosted).
-
No (copy only) — print the description for the user to paste into the web UI themselves. Useful when the user wants to hand-edit before pasting, or when the CLI's default forge instance is wrong for this repo.
-
Edit — the user wants to adjust something. moor is the preferred surface for this but optional — check it's installed:
command -v moor
If moor is present, open a one-off review of the current description vs. the draft so the user can comment on specific lines with a reason — directed, line-anchored feedback instead of a prose back-and-forth. Launch via the wrapper (not moor directly — the wrapper writes the MOOR_CONTEXT input header and prints the verdict on its stdout). The viewer blocks until closed, so launch as a background Bash call (run_in_background: true); a foreground call holds the turn open until the Bash timeout:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/review-diff.sh" --files \
<CURRENT_DESC_PATH> <draft-path> \
--title 'CR description — proposed edits' \
--detail branch=<BRANCH> --detail CR=<CR_URL>
When the background command completes, read its stdout with the BashOutput tool — not tail / $(...), which trips the command-substitution gate. The last lines carry the verdict: REVIEW_VERDICT (0/1/2/3/absent) and REVIEW_OUTPUT (compact JSON, with .comments when the verdict is 1). Don't read silence as success — only REVIEW_VERDICT 0 is approval; every other outcome either carries feedback to fold in or means the review never happened, so never write the description off the back of one.
0 — reviewed clean, nothing blocking: treat as approval and write the draft to the CR (the Yes action above).
1 — fix-now comments exist: each entry in REVIEW_OUTPUT's .comments is {body, action, file?, startLine?, endLine?}, where body is the user's inline feedback and action is fix-now (blocker), fix-later, or consider. Fold every fix-now comment into a revised draft — along with any advisory fix-later / consider comments worth incorporating — then loop back to Present the change.
2 — the user closed with hunks still unreviewed: a partial pass, not approval. Ask what they want to change, then re-present.
3 or absent, or no REVIEW_VERDICT line appeared — the review did not complete: moor closed before counting any hunks, crashed, or failed to launch. Do not treat this as approval and do not write the description. Surface what happened to the user — name the verdict, or that none was written — then fall back to a path that works: re-present the chat diff from Present the change and ask what to change (the moor-absent path below). If the user has a non-moor difftool configured, offer to open the two files in it (git difftool --no-index <CURRENT_DESC_PATH> <draft-path>) as an alternative; don't silently retry moor, since the same failure will recur.
The wrapper leaves the context file in place; moor recycles it.
If moor isn't on PATH, fall back to chat: ask what to change, revise the draft, and re-present (loop back to Present the change).
Set the ordering dependency (when Step 2 captured one)
If this CR must land after a predecessor CR, record the ordering on the forge once the description is written — not just in the prose line from Step 3. The full invocation and the degrade ladder live in the cookbook's "Linking an ordering dependency between CRs"; in short:
- GitLab — set the enforced dependency:
glab api -X POST "projects/:fullpath/merge_requests/<CR_IID>/blocks" -F blocking_merge_request_iid=<predecessor-iid> (add -F blocking_project_id=<id> when the predecessor is in another project). Detect by attempt, don't pre-probe: 201 linked · 409 already linked (fine) · 404 the instance predates the API (< 17.5) · 403 not Premium/Ultimate or no permission. On 404/403, fall back to prose — confirm the Step 3 Depends on !<iid> line is present and tell the user the ordering isn't enforced (they can set it in the UI if the instance supports it).
- GitHub — no native cross-PR dependency exists; the Step 3
Depends on #<num> line is the only signal. State that GitHub won't block the merge on it.
No predecessor captured (a single CR, or an independent one) → skip this entirely.
One web-UI step remains regardless of choice: screenshots embedded in the description must be dragged into the forge editor (gh / glab don't expose a clean upload path). After Yes (write) lands the body, open the CR in the browser, drop each PNG, and re-save — the forge rewrites the local paths to hosted URLs.