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 reviewreview-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 auth checks, the Superpowers agent channel, the per-finding round bookkeeping, an opt-in Beads bridge 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 + round bounding, plus file & standards context for the file-blind CLIs. 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.

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: a blocked / needs-user-decision outcome takes the "Stop path" and does not print the ready-for-merge message content/tools/review-pr.md:610.

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:610.

Where the bookkeeping lives today

The engine ships native multi-round sessions — mmr review --session <id> --round N --max-rounds M packages/mmr/src/commands/review.ts:289 — and a stable, line-number-independent finding_key packages/mmr/src/core/stable-id.ts:115. (See the MMR reference for how that key collapses the same issue at different severities.)

The scaffold wrappers have not yet migrated to native sessions. Until they do, review-pr and review-code enforce the 3-strike rule with their own bookkeeping: a per-session attempts file at .scaffold/review-attempts/<session-id>.json. Each fix round computes a hash over the same four identity components, increments that finding's counter, and the _review_at_strike_limit helper blocks once it hits 3 content/tools/review-pr.md:468. The wrapper hash deliberately mirrors the engine's finding_key components, so the eventual swap to mmr review --session is a clean migration rather than a re-think.

Practical takeaway. You drive the fix loop through the wrapper; the attempts file under .scaffold/review-attempts/ is the current source of truth for the strike count. 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 / Gemini / Claude / Grok 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:265.

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

See also