The Code-Review Workflow

What this guide covers

This is the workflow around multi-model review: when to review, which entry point to reach for, which input mode matches your target, how to read the verdict, and how the fix loop is bounded. It does not re-explain the review engine itself — channels, reconciliation, the finding_key, the verdict algorithm, and .mmr.yaml all live in the MMR reference. Read that guide for "what is a channel"; read this one for "I have a change, now what do I run."

Two layers. mmr review is the engine. scaffold run review-pr and review-code are workflow wrappers on top — they pick the input mode, add the Superpowers code-reviewer agent channel via mmr reconcile, handle auth recovery, and bound the fix loop. post-implementation-review is a separate, release-time full-codebase review with its own flow (see Step 1). This guide is about driving those entry points; the engine details are in the MMR reference.

Step 1 — Pick the entry point

Two of these are wrappers over mmr review — review-pr and review-code — sharing the same engine and verdict semantics, differing in scope and what they synthesize. post-implementation-review is not a mmr review wrapper: it's an independent release-time review of the whole codebase that runs its own channel flow and writes its own report under docs/reviews/, with MMR reconciliation only as an optional add-on. Consult its own doc for specifics.

#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#000000;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#my-svg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#my-svg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#my-svg .error-icon{fill:#552222;}#my-svg .error-text{fill:#552222;stroke:#552222;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:#666;stroke:#666;}#my-svg .marker.cross{stroke:#666;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#000000;}#my-svg .cluster-label text{fill:#333;}#my-svg .cluster-label span{color:#333;}#my-svg .cluster-label span p{background-color:transparent;}#my-svg .label text,#my-svg span{fill:#000000;color:#000000;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#eee;stroke:#999;stroke-width:1px;}#my-svg .rough-node .label text,#my-svg .node .label text,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-anchor:middle;}#my-svg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#my-svg .rough-node .label,#my-svg .node .label,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-align:center;}#my-svg .node.clickable{cursor:pointer;}#my-svg .root .anchor path{fill:#666!important;stroke-width:0;stroke:#666;}#my-svg .arrowheadPath{fill:#333333;}#my-svg .edgePath .path{stroke:#666;stroke-width:1px;}#my-svg .flowchart-link{stroke:#666;fill:none;}#my-svg .edgeLabel{background-color:white;text-align:center;}#my-svg .edgeLabel p{background-color:white;}#my-svg .edgeLabel rect{opacity:0.5;background-color:white;fill:white;}#my-svg .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#my-svg .cluster rect{fill:hsl(0, 0%, 98.9215686275%);stroke:#707070;stroke-width:1px;}#my-svg .cluster text{fill:#333;}#my-svg .cluster span{color:#333;}#my-svg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(-160, 0%, 93.3333333333%);border:1px solid #707070;border-radius:2px;pointer-events:none;z-index:100;}#my-svg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#000000;}#my-svg rect.text{fill:none;stroke-width:0;}#my-svg .icon-shape,#my-svg .image-shape{background-color:white;text-align:center;}#my-svg .icon-shape p,#my-svg .image-shape p{background-color:white;padding:2px;}#my-svg .icon-shape .label rect,#my-svg .image-shape .label rect{opacity:0.5;background-color:white;fill:white;}#my-svg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#my-svg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#my-svg .node .neo-node{stroke:#999;}#my-svg [data-look="neo"].node rect,#my-svg [data-look="neo"].cluster rect,#my-svg [data-look="neo"].node polygon{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node path{stroke:url(#my-svg-gradient);stroke-width:1px;}#my-svg [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node .neo-line path{stroke:#999;filter:none;}#my-svg [data-look="neo"].node circle{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node circle .state-start{fill:#000000;}#my-svg [data-look="neo"].icon-shape .icon{fill:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].icon-shape .icon-neo path{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}What are you reviewing?An open PRLocal changesbefore commit/pushThe whole codebaseafter all tasks landscaffold run review-pr(mmr review --pr N)scaffold run review-code(synthesized deliverycandidate)scaffold runpost-implementation-review(two-phase: systemic +per-story)
Entry pointTargetNotes
scaffold run review-prOne PR (--pr N, auto-detected from the branch)MMR wrapper. Adds the Superpowers agent channel and native round-bounding (--session/--round/--max-rounds) over a bare mmr review.
scaffold run review-codeLocal pre-push: committed branch diff + staged + unstaged, as one synthesized "delivery candidate"MMR wrapper. Adds the same agent channel and native round-bounding, scoped to the local delivery candidate. Untracked files are not covered.
scaffold run post-implementation-reviewThe full implemented codebase against stories + standardsIndependent, not an MMR wrapper. A release-time review (systemic sweep + per-story functional review via parallel agents) with its own report under docs/reviews/; MMR injection is optional. Consult its own doc.

The mandatory one. After gh pr create, running review-pr is mandatory before moving to the next task (a PostToolUse hook reminds you). review-code is the recommended preflight before a push but isn't gated. post-implementation-review is a release-time sweep, not a per-change gate.

When no wrapper fits

The wrappers are conveniences, not gates on the engine. For any target a wrapper doesn't cover — a branch range, an existing patch file, a single doc — call mmr review directly with the matching input flag. The MMR reference lists every flag; the next step is the workflow view of the same choice.

Step 2 — Choose the input mode

mmr review resolves a diff from exactly one input source. Pick the one that describes your target. The --diff - (stdin) form is the universal escape hatch: anything you can express as a unified diff, you can pipe in.

#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#000000;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#my-svg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#my-svg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#my-svg .error-icon{fill:#552222;}#my-svg .error-text{fill:#552222;stroke:#552222;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:#666;stroke:#666;}#my-svg .marker.cross{stroke:#666;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#000000;}#my-svg .cluster-label text{fill:#333;}#my-svg .cluster-label span{color:#333;}#my-svg .cluster-label span p{background-color:transparent;}#my-svg .label text,#my-svg span{fill:#000000;color:#000000;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#eee;stroke:#999;stroke-width:1px;}#my-svg .rough-node .label text,#my-svg .node .label text,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-anchor:middle;}#my-svg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#my-svg .rough-node .label,#my-svg .node .label,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-align:center;}#my-svg .node.clickable{cursor:pointer;}#my-svg .root .anchor path{fill:#666!important;stroke-width:0;stroke:#666;}#my-svg .arrowheadPath{fill:#333333;}#my-svg .edgePath .path{stroke:#666;stroke-width:1px;}#my-svg .flowchart-link{stroke:#666;fill:none;}#my-svg .edgeLabel{background-color:white;text-align:center;}#my-svg .edgeLabel p{background-color:white;}#my-svg .edgeLabel rect{opacity:0.5;background-color:white;fill:white;}#my-svg .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#my-svg .cluster rect{fill:hsl(0, 0%, 98.9215686275%);stroke:#707070;stroke-width:1px;}#my-svg .cluster text{fill:#333;}#my-svg .cluster span{color:#333;}#my-svg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(-160, 0%, 93.3333333333%);border:1px solid #707070;border-radius:2px;pointer-events:none;z-index:100;}#my-svg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#000000;}#my-svg rect.text{fill:none;stroke-width:0;}#my-svg .icon-shape,#my-svg .image-shape{background-color:white;text-align:center;}#my-svg .icon-shape p,#my-svg .image-shape p{background-color:white;padding:2px;}#my-svg .icon-shape .label rect,#my-svg .image-shape .label rect{opacity:0.5;background-color:white;fill:white;}#my-svg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#my-svg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#my-svg .node .neo-node{stroke:#999;}#my-svg [data-look="neo"].node rect,#my-svg [data-look="neo"].cluster rect,#my-svg [data-look="neo"].node polygon{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node path{stroke:url(#my-svg-gradient);stroke-width:1px;}#my-svg [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node .neo-line path{stroke:#999;filter:none;}#my-svg [data-look="neo"].node circle{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node circle .state-start{fill:#000000;}#my-svg [data-look="neo"].icon-shape .icon{fill:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].icon-shape .icon-neo path{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}Where is the change?On a GitHub PRStaged in the indexA branch, vs a baseLocal working tree:committed + staged +unstagedA brand-new filegit doesn't track yetmmr review --pr Nmmr review --stagedmmr review --base main--head BRANCHgit diff MERGE_BASE | mmrreview --diff -(this is what review-codesynthesizes)(diff -u /dev/null FILE ||true) | mmr review --diff -
TargetCommandNotes
A PRmmr review --pr 123Fetches the diff via gh pr diff. This is review-pr's mode.
Staged changesmmr review --stagedJust the index (git diff --cached) — the pre-commit slice.
A branch rangemmr review --base main --head "$BRANCH"Committed work only; no staged/unstaged.
Full delivery candidategit diff "$MERGE_BASE" | mmr review --diff -Committed branch diff + staged + unstaged in one patch. review-code synthesizes this for you.
A single tracked file's pending editsgit diff HEAD -- path/file.ts | mmr review --diff -Fails with "no diff content" if the file has no local changes — use the next row.
A brand-new / untracked file(diff -u /dev/null path/file.ts || true) | mmr review --diff -Synthesizes an "all-added" diff from current contents.
An existing patchmmr review --diff path/changes.patchReads diff-format content directly.

Untracked files are silently skipped — this is the trap. review-code reviews the committed branch diff plus staged and unstaged changes to tracked files. A brand-new file you've never git add-ed is not in any of those diffs, so it sails through review with zero findings — not because it's clean, but because no channel ever saw it. To review a new file, pipe it in explicitly: (diff -u /dev/null path/to/new-file.ts || true) | mmr review --diff -. The || true guard is required: diff exits non-zero whenever files differ, which would otherwise kill the pipeline under set -o pipefail. The wrappers cannot guess which untracked files you meant to include content/tools/review-code.md:37.

The --diff flag expects diff-format content — a path to a .patch/.diff file, or - for stdin. It does not accept raw document text; wrap the target in a diff first.

Step 3 — Read the verdict, then act

Every review collapses to exactly one of four verdicts. The verdict, not the raw findings, is what decides whether you proceed. The MMR reference defines the gate, the severity tiers, and the exact derivation algorithm; this table is the action you take for each outcome.

VerdictExitDo
pass0Proceed — merge / push / next task.
degraded-pass0Proceed, noting reduced coverage. The max achievable verdict once any channel was compensated.
blocked2Stop. Fix the blocking findings (Step 4), then re-review. Do not merge.
needs-user-decision3Stop and surface to the user. Automated iteration can't resolve this.

A review is blocked when any unacknowledged finding sits at or above the fix threshold (P2 by default; override per-run with --fix-threshold). See the MMR reference for how the verdict is derived from gate result + channel health.

A clean run can still be needs-user-decision. Since mmr 4.0.0 the verdict also reflects how many channels reported. If fewer than defaults.min_completed_channels (default 2) completed, the result is needs-user-decision even with zero findings — one reviewer agreeing with itself is not multi-model review. In practice you'll hit this when only one CLI is installed or authed: fix the channel (mmr doctor) rather than treating the empty result as a pass. blocked still outranks the floor, so a real blocking finding is always reported as blocked.

Proceed only on pass or degraded-pass. On blocked or needs-user-decision, never merge automatically — surface the verdict and the remaining findings to the user. The wrappers enforce this: report only says the PR is merge-ready on pass / degraded-pass content/tools/review-pr.md:147.

Step 4 — Fix the blocking findings (bounded)

When the verdict is blocked, the loop is: fix the findings at or above the threshold → re-review → repeat. The guard rail is that this loop is bounded per finding, so a finding the model can't actually fix doesn't trap you in an infinite cycle.

The 3-round-per-finding limit

The limit is 3 attempts per finding, not 3 rounds total. Each round that surfaces genuinely new findings is healthy iteration — keep going. The loop stops only when one specific finding has been attempted three times without resolution.

A "finding" here is its stable identity, not its wording, so a re-worded report of the same defect still counts against the same finding. (The MMR reference covers how that identity is computed.) Co-equal stop conditions:

When you stop, do not merge. Document each unresolved finding (severity, location, attempt count) and hand the decision to the user content/tools/review-pr.md:147.

How the round budget is enforced

Round-bounding is native to the engine. The wrappers pass mmr review --session <id> --round <N> --max-rounds 3 content/tools/review-pr.md:81, incrementing --round each fix round (--round is required — MMR compares it against --max-rounds, so without it every call is round 1 and the cap never fires). MMR enforces the budget using a stable, line-number-independent finding_key packages/mmr/src/core/stable-id.ts:115 — the same identity across severity changes and line-number shifts (a materially reworded description does change the key, since description and suggestion are part of it). (See the MMR reference for how that key collapses the same issue at different severities.) A finding that survives the budget stops being re-attempted; you don't track strikes by hand.

This replaced the former wrapper-side attempts file (.scaffold/review-attempts/<session-id>.json), retired when the review tools were slimmed to the MMR-dispatch core.

Practical takeaway. Pass --session and an incrementing --round so the round budget actually applies — --max-rounds is inert without --round (MMR compares --round to the cap; --session alone doesn't). For a very noisy loop you may narrow the gate for one run with --fix-threshold P1 — but don't permanently lower the project default (P2).

Step 5 — Handle degraded mode

A review never silently runs with fewer reviewers. A channel is degraded when its binary isn't installed, auth fails, it times out, or it errors out. The workflow's response is to compensate and tell you how to recover, then cap the verdict at degraded-pass. The mechanics — how compensation runs and the per-channel auth checks — live in the MMR reference; below is what it means for your workflow.

Foreground only. When the mmr CLI is unavailable and a wrapper falls back to invoking Codex / Claude / Grok / Antigravity directly, run them as foreground Bash calls — never with run_in_background, &, or nohup. Background execution produces empty output, which the parser then reads as a degraded channel content/tools/review-code.md:204.

Once any channel was compensated, the best possible verdict is degraded-pass — full pass requires all channels to have completed for real.

See also