---
name: review-pr
description: Run all configured code review channels on a PR (Codex CLI, Gemini CLI, Claude CLI)
phase: null
order: null
dependencies: []
outputs: []
conditional: null
stateless: true
category: tool
knowledge-base: [multi-model-review-dispatch, automated-review-tooling]
argument-hint: "<PR# or blank> [--fix-threshold P0|P1|P2|P3]"
---

## Purpose

Run the three CLI review channels (Codex, Gemini, Claude) on a pull request
**plus** the Superpowers code-reviewer agent as a complementary 4th channel,
and reconcile all findings through MMR. This is the single entry point for
**PR-scoped** code review — agents call this once instead of remembering four
separate review invocations.

**For non-PR targets**, don't use this tool. Call `mmr review` directly with
the appropriate input mode, or use `scaffold run review-code` for local
pre-commit review:

- `mmr review --staged` — staged changes
- `mmr review --base <ref> --head <ref>` — branch diff
- `mmr review --diff <path.patch>` — existing diff/patch file
- `<git diff …> | mmr review --diff -` — any piped diff (including a single
  tracked file via `git diff HEAD -- <path>`, or a new file via
  `(diff -u /dev/null <path> || true)` — the `|| true` guard is required
  because `diff` exits 1 whenever files differ, which breaks pipelines
  under `set -o pipefail`)

The `--diff` flag expects diff-format content; it does not read raw document
content. The three-channel review itself is not PR-specific — this tool is
just the PR wrapper around the more general `mmr review` CLI.

The three channels are:
1. **Codex CLI** — OpenAI's code analysis (implementation correctness, security, API contracts)
2. **Gemini CLI** — Google's design reasoning (architectural patterns, broad context)
3. **Claude CLI** — Anthropic's code review (plan alignment, code quality, testing)

## Inputs

- $ARGUMENTS — PR number (optional; auto-detected from current branch if omitted) and/or `--fix-threshold P0|P1|P2|P3` to override the project's configured threshold for this run
- `.mmr.yaml` — MMR CLI configuration (channels, review_criteria, defaults)

The CLI handles review context via config (`review_criteria` in `.mmr.yaml`).
Project-specific standards (coding-standards, review-standards) are referenced
in the review criteria config rather than read at dispatch time.

## Expected Outputs

- All three CLI review channels executed (or fallback documented) plus the Superpowers code-reviewer 4th channel reconciled via `mmr reconcile`
- findings at or above the configured `fix_threshold` fixed before proceeding (read from `results.fix_threshold` in the verdict JSON; default `P2`)
- Review summary with per-channel results and reconciliation

## Instructions

### Step 1: Identify the PR

```bash
# Strip --fix-threshold from $ARGUMENTS if present; remainder is the PR number.
# Strip the entire matched span (BASH_REMATCH[0]) — including whatever
# whitespace separator was used (space, tab, multi-space). Replacing with a
# single space preserves token boundaries; tr -d '[:space:]' below drops
# everything else.
FIX_THRESHOLD=""
ARGS_REMAINING="$ARGUMENTS"
if [[ "$ARGS_REMAINING" =~ (^|[[:space:]])--fix-threshold[[:space:]]+(P[0-3])($|[[:space:]]) ]]; then
  FIX_THRESHOLD="${BASH_REMATCH[2]}"
  ARGS_REMAINING="${ARGS_REMAINING//${BASH_REMATCH[0]}/ }"
fi

# Use remaining argument if provided, otherwise detect from current branch
PR_NUMBER="$(echo "$ARGS_REMAINING" | tr -d '[:space:]')"
PR_NUMBER="${PR_NUMBER:-$(gh pr view --json number -q .number 2>/dev/null)}"
```

If no PR is found, stop and tell the user to create a PR first.

### Step 2: Run MMR Review

Use the MMR CLI as the primary entry point for automated dispatch, reconciliation, and verdict:

```bash
MMR_FLAGS=(--pr "$PR_NUMBER" --sync --format json)
[ -n "$FIX_THRESHOLD" ] && MMR_FLAGS+=(--fix-threshold "$FIX_THRESHOLD")
MMR_RESULT=$(mmr review "${MMR_FLAGS[@]}")
# Extract job_id from JSON output for use in mmr reconcile
JOB_ID=$(echo "$MMR_RESULT" | grep -o '"job_id": "[^"]*"' | head -1 | cut -d'"' -f4)
```

The CLI handles:
- Installation and auth checks for each channel (codex, gemini, claude)
- Compensating passes when channels are unavailable (dispatched via `claude -p`)
- Output parsing and finding reconciliation
- Verdict derivation (pass/degraded-pass/blocked/needs-user-decision)
- Exit codes: 0=pass/degraded-pass, 2=blocked, 3=needs-user-decision

The CLI supports multiple input modes:
- `--pr <number>` — review a GitHub PR (fetches diff via `gh pr diff`)
- `--diff <file>` — review a diff file
- `--staged` — review staged changes (`git diff --cached`)
- `--base <ref> --head <ref>` — review diff between two refs

**Manual fallback** (when MMR CLI is not installed):

Run Codex, Gemini, and Claude CLI commands individually as foreground Bash calls.
Never use `run_in_background`, `&`, or `nohup`.

#### Channel 1: Codex CLI

```bash
command -v codex >/dev/null 2>&1 || echo "Codex not installed"
codex login status 2>/dev/null
codex exec --skip-git-repo-check -s read-only --ephemeral "REVIEW_PROMPT" 2>/dev/null
```

If not installed or auth fails, queue a compensating pass focused on implementation
correctness, security, and API contracts. Auth failure recovery: `! codex login`.

#### Channel 2: Gemini CLI

```bash
command -v gemini >/dev/null 2>&1 || echo "Gemini not installed"
NO_BROWSER=true gemini -p "respond with ok" -o json 2>&1
NO_BROWSER=true gemini -p "REVIEW_PROMPT" --output-format json --approval-mode yolo 2>/dev/null
```

If not installed or auth fails, queue a compensating pass focused on architectural
patterns, design reasoning, and broad context. Auth failure recovery: `! gemini -p "hello"`.

#### Channel 3: Claude CLI

```bash
claude -p "REVIEW_PROMPT" --output-format json 2>/dev/null
```

Claude CLI handles its own auth. Focus: plan alignment, code quality, testing.

**After all channels:** Run any queued compensating passes as additional `claude -p`
dispatches with focused prompts. Label findings as `[compensating: Codex-equivalent]`
or `[compensating: Gemini-equivalent]`.

### Step 3: Run Agent Code Review (4th channel)

Dispatch your platform's code-reviewer skill for a complementary review:
- **Claude Code:** dispatch `superpowers:code-reviewer` subagent with the PR diff and review criteria
- **Other platforms:** use your platform's equivalent agent review skill

The agent skill runs inside your agent's context — it has access to conversation history, project knowledge, and plan context that external CLIs lack.

**Important:** The agent's review output must use MMR-compatible finding schema: each finding needs `severity` (P0-P3), `location` (file:line), and `description` (`category` and `suggestion` are optional, but `category` is recommended for finding identity). The strict validator in `mmr reconcile` will reject findings with missing or invalid required fields.

### Step 4: Inject Agent Review into MMR

Feed the agent review findings into MMR for unified reconciliation:

```bash
# job_id is captured from mmr review --sync --format json output
# Write agent findings to a temp file for mmr reconcile
echo "$AGENT_FINDINGS" > /tmp/agent-findings.json
mmr reconcile "$JOB_ID" --channel superpowers --input /tmp/agent-findings.json
```

The `reconcile` command:
- Adds the agent's findings as a new channel in the job
- Re-runs reconciliation across ALL channels (CLI + agent)
- Outputs the unified verdict with all sources included

### Step 5: Reconcile Findings

When using `mmr review --sync`, reconciliation is automatic. For manual fallback,
reconcile findings after all channels complete:

| Scenario | Confidence | Action |
|----------|-----------|--------|
| Multiple channels flag same issue | **High** | Fix immediately |
| All channels approve (no findings) | **High** | Proceed to merge |
| One channel flags P0, others approve | **High** | Fix it — P0 is critical from any source |
| One channel flags P1, others approve | **Medium** | Fix it — P1 findings are mandatory regardless of source count |
| Channels contradict each other | **Low** | Present to user for adjudication |
| Compensating-pass blocking finding | **Single-source** | Fix per normal thresholds, label as compensating |

### Step 6: Report Results

Output a review summary in this format:

```
## Code Review Summary — PR #[number]

### Channels Executed
- [ ] Codex CLI — root cause: [completed / not installed / auth failed / timeout / failed], coverage: [full / compensating (Codex-equivalent)]
- [ ] Gemini CLI — root cause: [completed / not installed / auth failed / timeout / failed], coverage: [full / compensating (Gemini-equivalent)]
- [ ] Claude CLI — root cause: [completed / not_installed / auth_failed / timeout / failed], coverage: [full / none (Claude is never compensated — it IS the compensator for Codex/Gemini)]
- [ ] Agent review — [completed / skipped], injected via mmr reconcile

### Consensus Findings (High Confidence)
[Findings flagged by 2+ channels]

### Single-Source Findings
[Findings from only one channel, with attribution]

### Disagreements
[Contradictions between channels]

### Verdict
[pass / degraded-pass / blocked / needs-user-decision]
```

### Step 6a: Final Verdict

Return exactly one verdict:

- `pass` — all channels completed and the gate passed (no unresolved findings at or above the configured fix threshold; the threshold defaults to `P2` but is configurable via `.mmr.yaml` or `--fix-threshold`)
- `degraded-pass` — gate passed but some channels were skipped or replaced by compensating passes (max achievable verdict when any channel was compensated)
- `blocked` — gate failed: at least one unresolved finding sits at or above the fix threshold (typically the *same* finding(s) remain unresolved after 3 fix attempts)
- `needs-user-decision` — no channels completed (no reconciled result was possible), reviewer disagreement / contradictions, or a finding requires human judgment that automated iteration can't resolve

Verdict precedence: `needs-user-decision` > `blocked` > `degraded-pass` > `pass`.

When compensating passes ran, maximum achievable verdict is `degraded-pass`. When both external channels were compensated, note "All findings are single-model."

### Step 7: Fix Blocking Findings

If any findings sit at or above `fix_threshold` (the verdict JSON's `fix_threshold` field; default `P2`):
1. Fix them in the code
2. Push the fixes: `git push`
3. Re-run the review to verify fixes: `mmr review --pr "$PR_NUMBER" --sync --format json`
4. The 3-round limit is **per finding**, enforced by the wrapper-side hash in Step 7a (`.scaffold/review-attempts/<session-id>.json`):
   - **Keep going** when each new round surfaces genuinely different findings with *new* hashes — that is healthy review/fix iteration.
   - **Stop and ask the user** when (a) any blocking finding's hash hits 3 attempts in the attempts file (`_review_at_strike_limit` returns true), (b) the same underlying defect keeps recurring across 3 rounds even if reviewer wording changes and produces a new hash, (c) a finding is genuinely ambiguous (channels contradict each other), or (d) the user explicitly asks to stop.
   - **When stopped**, do NOT merge automatically. Document the unresolved findings (severity, location, hash, attempt count, and semantic recurrence notes when wording changed) and let the user decide whether to continue fixing, create follow-up issues, or override.
   - Identity components used by the hash — `location`, `category`, `description`, `suggestion` — mirror MMR T2-A's forthcoming native `finding_key` (v3.30) so this bookkeeping migrates cleanly.

**Note:** Fix cycles are an orchestration concern — the caller (agent or human) handles the fix loop. The CLI provides the review and verdict; the caller decides whether to fix and re-run.

**Fix cycle channel rule:** Re-run only channels that originally completed or ran as compensating passes. Never retry a channel marked `not_installed`, `auth_failed`, or `timeout` during fix rounds — its availability does not change within a session.

### Step 7a: Wrapper-Side Per-Finding Hash (Stopgap until MMR v3.30)

Before each fix round, compute a stable hash per finding and record an attempt
in `.scaffold/review-attempts/<session-id>.json`. The hash mirrors MMR T2-A's
forthcoming `finding_key` identity (location + category + description +
suggestion) so it migrates cleanly when MMR v3.30 ships native `--session` and
`finding_key`. **Until then**, this wrapper-side bookkeeping is what enforces
the per-finding 3-strike rule.

This section is throwaway — when MMR v3.30 lands, replace this entire block
with `mmr review --session <id> --max-rounds N` and read `finding_key` from
the verdict JSON directly.

#### Derive the session id

<!-- review-wrapper-hash-helpers:start -->

```bash
# Session id rules (first match wins):
#   1. PR mode (--pr N) → "pr-<N>"
#   2. Branch+base    → "<branch>@<base>" (sanitized to ^[a-zA-Z0-9_.-]+$)
#   3. Fallback       → "ts-$(date -u +%Y%m%dT%H%M%SZ)"
_review_session_id() {
  _review_sanitize_session_id() {
    local raw="$1" sanitized
    sanitized=$(printf '%s' "$raw" | tr -c 'a-zA-Z0-9_.-' '_')
    if [ -z "$sanitized" ] || [ "$sanitized" = "." ] || [ "$sanitized" = ".." ]; then
      echo "Error: review session id resolves to an unsafe path segment" >&2
      return 1
    fi
    printf '%s' "$sanitized"
  }

  if [ -n "${REVIEW_SESSION_ID:-}" ]; then
    _review_sanitize_session_id "$REVIEW_SESSION_ID"
    return
  fi
  if [ -n "${__REVIEW_SESSION_ID:-}" ]; then
    printf '%s' "$__REVIEW_SESSION_ID"
    return
  fi
  if [ -n "${PR_NUMBER:-}" ]; then
    __REVIEW_SESSION_ID=$(_review_sanitize_session_id "pr-$PR_NUMBER") || return 1
    printf '%s' "$__REVIEW_SESSION_ID"
    return
  fi
  local branch base
  branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
  base="${BASE_REF:-main}"
  if [ -n "$branch" ] && [ "$branch" != "HEAD" ]; then
    __REVIEW_SESSION_ID=$(_review_sanitize_session_id "$branch@$base") || return 1
    printf '%s' "$__REVIEW_SESSION_ID"
    return
  fi
  __REVIEW_SESSION_ID=$(_review_sanitize_session_id "ts-$(date -u +%Y%m%dT%H%M%SZ)") || return 1
  printf '%s' "$__REVIEW_SESSION_ID"
}

_review_attempts_file() {
  local id; id=$(_review_session_id)
  mkdir -p .scaffold/review-attempts
  printf '.scaffold/review-attempts/%s.json' "$id"
}
```

#### Normalize the four identity components

`normalized_location` strips trailing line/column spans (anchored to
end-of-string so mid-path digits survive):

```bash
_review_normalize_location() {
  # Input: $1 = raw location (e.g. "src/foo.ts:42-44" or "pkg/Bar.kt (line 10)")
  # Output: lowercased file path with trailing :N, :N-M, :N:M, (line N) stripped
  printf '%s' "$1" \
    | tr '[:upper:]' '[:lower:]' \
    | awk '{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, ""); print }' \
    | sed -E 's/(:[0-9]+(:[0-9]+)?(-[0-9]+)?|[[:space:]]+\(line[[:space:]]+[0-9]+\))$//'
}
```

The `(line N)` form requires whitespace before the parenthetical, so paths
like `archive/v1(line 10).txt` are preserved rather than treated as review
line references.

`description_normalized` is the tricky one — backtick-quoted code spans must
stay case-sensitive while prose around them is lowercased and stripped of
line-number filler. A python3 one-liner is the least painful implementation:

```bash
_review_normalize_description() {
  # Input: $1 = raw description
  # Output: tokenize on backticks → normalize non-code segments → reassemble
  printf '%s' "$1" | python3 -c '
import re, sys
s = sys.stdin.read()
parts = s.split("`")
out = []
for i, seg in enumerate(parts):
    if i % 2 == 1:
        # Odd index = inside backticks = code, preserve exactly
        out.append("`" + seg + "`")
    else:
        seg = seg.lower()
        seg = re.sub(r"\bline\s+\d+\b", "", seg)
        seg = re.sub(r"\bat\s+line\s+\d+\b", "", seg)
        seg = re.sub(r"^\s*(p[0-3]|critical|high|medium|low|trivial)\s*:\s*", "", seg)
        seg = re.sub(r"\s+", " ", seg).strip()
        out.append(seg)
print(" ".join(p for p in out if p))
'
}
```

`suggestion_normalized` is lowercase + collapse-whitespace only (suggestions
are short and distinguishing — no further stripping):

```bash
_review_normalize_suggestion() {
  printf '%s' "$1" | python3 -c 'import re, sys; print(re.sub(r"\s+", " ", sys.stdin.read().lower()).strip())'
}
```

#### Compute the stable hash

```bash
_review_finding_hash() {
  # Input: $1 = single-finding JSON object (with location, category, description, suggestion fields)
  # Output: 40-char sha1 hex of normalized_location + "|" + category + "|" + sha1(description_normalized) + "|" + sha1(suggestion_normalized)
  local f="$1"
  local loc cat desc sugg
  loc=$(printf '%s' "$f"  | jq -r '.location // ""')
  cat=$(printf '%s' "$f"  | jq -r '.category // ""')
  desc=$(printf '%s' "$f" | jq -r '.description // ""')
  sugg=$(printf '%s' "$f" | jq -r '.suggestion // ""')

  local nloc ndesc nsugg dhash shash
  nloc=$(_review_normalize_location "$loc")
  ndesc=$(_review_normalize_description "$desc")
  nsugg=$(_review_normalize_suggestion "$sugg")
  dhash=$(printf '%s' "$ndesc" | shasum -a 1 | awk '{print $1}')
  shash=$(printf '%s' "$nsugg" | shasum -a 1 | awk '{print $1}')

  printf '%s|%s|%s|%s' "$nloc" "$cat" "$dhash" "$shash" \
    | shasum -a 1 | awk '{print $1}'
}
```

#### Compute the description shingle (for future cross-round fuzzy matching)

This array is persisted alongside the hash so a follow-up MMR v3.30 migration
can run Jaccard ≥ 0.7 against historical findings without re-deriving
shingles. The wrapper itself does not currently consume the shingle for any
gating decision — strict-hash exact match is enough for the 3-strike rule.

```bash
_review_description_shingle() {
  # Input: $1 = normalized description
  # Output: JSON array of normalized 5-grams (token-based)
  printf '%s' "$1" | python3 -c '
import json, sys
tokens = sys.stdin.read().split()
shingles = sorted({" ".join(tokens[i:i+5]) for i in range(max(0, len(tokens)-4))})
print(json.dumps(shingles))
'
}
```

#### Record an attempt and check the strike limit

```bash
_review_record_attempt() {
  # Input: $1 = finding JSON, $2 = current round number (1-based), $3 = optional precomputed finding hash
  # Side effect: increments attempts in the attempts file
  # Output: prints new attempt count on stdout
  local f="$1" round="$2" hash="${3:-}"
  local file loc cat desc sugg nloc ndesc nsugg dhash shash shingle
  file=$(_review_attempts_file)
  loc=$(printf '%s' "$f"  | jq -r '.location // ""')
  cat=$(printf '%s' "$f"  | jq -r '.category // ""')
  desc=$(printf '%s' "$f" | jq -r '.description // ""')
  sugg=$(printf '%s' "$f" | jq -r '.suggestion // ""')
  nloc=$(_review_normalize_location "$loc")
  ndesc=$(_review_normalize_description "$desc")
  nsugg=$(_review_normalize_suggestion "$sugg")
  if [ -z "$hash" ]; then
    dhash=$(printf '%s' "$ndesc" | shasum -a 1 | awk '{print $1}')
    shash=$(printf '%s' "$nsugg" | shasum -a 1 | awk '{print $1}')
    hash=$(printf '%s|%s|%s|%s' "$nloc" "$cat" "$dhash" "$shash" \
      | shasum -a 1 | awk '{print $1}')
  fi
  shingle=$(_review_description_shingle "$ndesc")

  [ -f "$file" ] || jq -n --arg id "$(_review_session_id)" --arg created "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
    '{session_id: $id, created_at: $created, findings: {}}' > "$file"

  jq --arg h "$hash" --arg loc "$nloc" --argjson sh "$shingle" --argjson r "$round" '
    .findings[$h] = (
      .findings[$h] // {attempts: 0, first_seen_round: $r, normalized_location: $loc, description_shingle: $sh}
      | .attempts += (if .last_seen_round == $r then 0 else 1 end)
      | .last_seen_round = $r
    )
  ' "$file" > "$file.tmp" && mv "$file.tmp" "$file"

  jq -r --arg h "$hash" '.findings[$h].attempts' "$file"
}

_review_at_strike_limit() {
  # Input: $1 = finding JSON, $2 = optional precomputed finding hash
  # Exit: 0 if hash already has >= 3 attempts, 1 otherwise
  local f="$1" file hash
  hash="${2:-}"
  file=$(_review_attempts_file)
  [ -f "$file" ] || return 1
  [ -n "$hash" ] || hash=$(_review_finding_hash "$f")
  local n; n=$(jq -r --arg h "$hash" '.findings[$h].attempts // 0' "$file")
  [ "$n" -ge 3 ]
}
```

This bookkeeping assumes sequential execution within a single workspace or
worktree. Do not run multiple review/fix loops against the same
`REVIEW_SESSION_ID` concurrently.

#### Per-round flow

After every `mmr review … --sync --format json` call:

1. Extract reconciled findings: `FINDINGS=$(mmr results "$JOB_ID" | jq -c '.reconciled_findings[]')`
2. Iterate safely over the newline-delimited JSON:
   `printf '%s\n' "$FINDINGS" | while IFS= read -r f; do ... done`
3. For each blocking finding (severity at or above `fix_threshold`):
   - Compute its hash via `_review_finding_hash`.
   - Call `_review_at_strike_limit "$f" "$hash"` before incrementing — if true,
     this finding already has 3 recorded attempts. Abort the entire task with
     a clear blocked error; do not merely `break` the inner `while` loop or
     continue the outer fix loop. Then follow the **Stop path** in Step 8.
   - Call `_review_record_attempt "$f" "$ROUND" "$hash"` to increment its counter.
4. Otherwise apply fixes, re-push, increment `ROUND`, and loop.

For very noisy fix loops, you may suggest the user re-run with
`--fix-threshold P1` to narrow the gate (the project default stays at P2 per
the design's Decision 4). Do not auto-change the threshold.

This mirrors T2-A's identity components — `location`, `category`,
`description`, `suggestion` — so a future migration to MMR's native
`finding_key` is a search-and-replace of the helper calls with the field
read from the verdict JSON.

<!-- review-wrapper-hash-helpers:end -->

### Step 7b: File blocking findings as Beads tasks (opt-in)

If `.mmr.yaml` has `beads.create_issues_from_blocking_findings: true` AND `.beads/`
exists in the project, file each blocking finding (severity at-or-above
`beads.fix_threshold`, default `P2`) as a Beads bug. This is purely additive
tracking — it does NOT replace Step 7's fix-in-place flow; it only creates a
durable record of findings that ought to become standalone follow-up work.

```bash
# First: gate on the opt-in flag in .mmr.yaml. Defaults to disabled.
# Uses pure bash + grep/sed — no yq dependency (yq isn't always installed).
beads_enabled=false
beads_fix_threshold=P2
beads_default_type=bug
if [ -f .mmr.yaml ]; then
  # Match a `create_issues_from_blocking_findings: true` line under any indentation.
  # We don't validate it's nested under `beads:` — false positives in unrelated
  # config keys with the same name are unlikely given the explicit name.
  # POSIX character classes ([[:space:]]) for BSD-sed compatibility (macOS default).
  # Patterns tolerate trailing whitespace/comments — uncommenting a template line with
  # a trailing `# comment` should still match.
  if grep -qE '^[[:space:]]*create_issues_from_blocking_findings:[[:space:]]*true([[:space:]]+#.*)?[[:space:]]*$' .mmr.yaml; then
    beads_enabled=true
  fi
  # Optional overrides for threshold / type. Defaults apply if the keys are absent.
  if v=$(grep -E '^[[:space:]]*fix_threshold:[[:space:]]*P[0-4]([[:space:]]+#.*)?[[:space:]]*$' .mmr.yaml | head -1 | sed -E 's/^[^:]*:[[:space:]]*(P[0-4]).*/\1/'); [ -n "$v" ]; then
    beads_fix_threshold=$v
  fi
  if v=$(grep -E '^[[:space:]]*default_type:[[:space:]]*[a-zA-Z]+([[:space:]]+#.*)?[[:space:]]*$' .mmr.yaml | head -1 | sed -E 's/^[^:]*:[[:space:]]*([a-zA-Z]+).*/\1/'); [ -n "$v" ]; then
    beads_default_type=$v
  fi
fi

if [ "$beads_enabled" = "true" ] && [ -d .beads ] && command -v bd >/dev/null 2>&1 \
   && command -v mmr >/dev/null 2>&1 && [ -n "${JOB_ID:-}" ]; then
  threshold_rank=$(case "$beads_fix_threshold" in P0) echo 0;; P1) echo 1;; P2) echo 2;; P3) echo 3;; *) echo 4;; esac)

  # Capture the reconciled findings from the MMR job we already ran in Step 2.
  # MMR JSON shape: { reconciled_findings: [{ severity, location, description, suggestion, ... }] }
  review_json=$(mmr results "$JOB_ID" --format json)

  while IFS= read -r finding; do
    title=$(jq -r '.description | .[0:120]' <<<"$finding")
    severity=$(jq -r '.severity' <<<"$finding")
    pnum="${severity#P}"
    description=$(jq -r --arg job "$JOB_ID" '"\(.description)\n\nSuggestion: \(.suggestion // "(none)")\n\nLocation: \(.location // "(unknown)")\n\nFirst seen in MMR job: \($job)"' <<<"$finding")
    # Per-finding identity in the external-ref so a future Beads release with
    # filter-by-external-ref can dedupe on re-runs. Uses the same
    # location+description identity components as the wrapper-side hash stopgap
    # (Step 7a) for consistency. NOTE: bd v1.0.4 has no `bd list --external-ref`
    # flag, so cross-run dedupe is not enforced at the bridge level today —
    # known limitation; re-running on the same MMR job will create duplicates.
    loc=$(jq -r '.location // ""' <<<"$finding")
    desc_for_hash=$(jq -r '.description // ""' <<<"$finding")
    finding_hash=$(printf '%s|%s' "$loc" "$desc_for_hash" | shasum -a 1 | cut -c1-8)

    # Build args conditionally — only include --deps discovered-from when SOURCE_BD_ID
    # is set AND non-empty. Avoids bd create rejecting a bogus "discovered-from:unknown".
    args=(
      "$title"
      --type "$beads_default_type"
      -p "$pnum"
      --description "$description"
      --external-ref "mmr:$finding_hash"
    )
    if [ -n "${SOURCE_BD_ID:-}" ]; then
      args+=(--deps "discovered-from:$SOURCE_BD_ID")
    fi
    bd create "${args[@]}"
  done < <(jq -c --argjson maxRank "$threshold_rank" '
    .reconciled_findings[]?
    | (.severity | sub("^P";"") | tonumber) as $rank
    | select($rank <= $maxRank)
  ' <<<"$review_json")
fi
```

Notes on this script:
- The opt-in flag `beads.create_issues_from_blocking_findings` in `.mmr.yaml` is read first; the rest of the block is skipped unless it's `true`. No env-var override — config is the single source of truth.
- Pure-bash YAML parsing (grep + sed) — no `yq` dependency. The match patterns are intentionally simple: a single boolean enable flag, plus optional `fix_threshold` and `default_type` overrides. Anything more complex than that should be parsed with a real YAML library.
- `--argjson maxRank "$threshold_rank"` passes a number so jq can compare numerically.
- `(.severity | sub("^P";"") | tonumber)` extracts the integer rank from `P2`-style severities.
- `while IFS= read -r` streams one JSON object per line without word-splitting on spaces.
- `.description | .[0:120]` truncates safely under UTF-8 (unlike `head -c 120`).
- The `--deps discovered-from:$SOURCE_BD_ID` flag is included only when `$SOURCE_BD_ID` is non-empty — avoids a bogus `discovered-from:unknown` dependency or a `bd create` failure for the common case where no source task is in scope.

Use `--external-ref "mmr:$finding_hash"` to link the new Beads issue to the *finding identity* (location + description hash, same components as Step 7a's wrapper hash). The ref is stable across MMR job IDs. **Known limitation:** Beads v1.0.4 has no `bd list --external-ref <ref>` filter, so this bridge can't dedupe at write time — re-running the bridge on the same finding will create another Beads issue. When upstream adds an external-ref filter, prepend a `bd list --external-ref "mmr:$finding_hash"` skip-check to this loop. The job ID is preserved in the issue's description (`First seen in MMR job: <id>`) for traceability.

`--deps discovered-from:$SOURCE_BD_ID` chains the new issue to whatever current task triggered the review (only when that ID is known).

### Step 8: Confirm Completion

**Success path** — all findings resolved (verdict is `pass` or `degraded-pass`):

```
Code review complete. Verdict: [pass/degraded-pass]. Channels: [N] executed, [N] compensating. PR #[number] is ready for merge.
```

**Stop path** — a per-finding stop condition from Step 7 was hit (verdict is `blocked` or `needs-user-decision`). Do NOT use the ready-for-merge message and do NOT merge. Instead, hand off to the user:

```
Code review halted. Verdict: [blocked/needs-user-decision]. PR #[number] is NOT ready for merge.
Unresolved findings:
- [severity] [location] [hash] — [description] (rounds attempted: [N])
- ...
Reason for stop: [same finding recurred 3× / channels contradict each other / user requested stop]
```

In either path, output the message and stop. Do NOT proceed to the next task without this confirmation.

## Fallback Behavior

| Situation | Action |
|-----------|--------|
| Channel not installed | Queue compensating pass, report root-cause `not_installed` |
| Auth expired, user recovers | Retry dispatch |
| Auth expired, user declines | Queue compensating pass, report root-cause `auth_failed` |
| Auth check timeout (after retry) | Queue compensating pass, report root-cause `timeout` |
| Channel fails during execution | Queue compensating pass, report root-cause `failed` |
| Both external channels unavailable | Two compensating passes, max verdict: `degraded-pass`, note "All findings single-model" |

## Process Rules

1. **Foreground only** — Always run Codex, Gemini, and Claude CLI commands as foreground Bash calls. Never use `run_in_background`, `&`, or `nohup`.
2. **All three CLI channels are mandatory** — Codex CLI, Gemini CLI, and Claude CLI. Plus the Superpowers code-reviewer agent as a complementary 4th channel reconciled via `mmr reconcile` (Step 3). Skip a CLI channel only when a tool is genuinely not installed or auth cannot be recovered (in which case MMR emits a compensating pass for missing Codex/Gemini channels; a missing Claude CLI has no compensator). Never skip by choice.
3. **Auth failures are not silent** — always surface to the user with the exact recovery command.
4. **Independence** — never share one channel's output with another. Each reviews the diff independently.
5. **Fix before proceeding** — findings at or above `fix_threshold` must be resolved before moving to the next task.
6. **3-round limit (per finding hash)** — never attempt to fix the *same* blocking finding (identified by the Step 7a hash of `location` + `category` + `description` + `suggestion`) more than 3 times. The attempts file `.scaffold/review-attempts/<session-id>.json` is the source of truth; `_review_at_strike_limit` checks it. Each round that surfaces genuinely different findings with *new* hashes is healthy iteration — keep going. Stop when a hash hits 3 attempts, when the same underlying defect recurs across 3 rounds even if reviewer wording produces new hashes, when channels contradict each other, or when the user asks to stop. For noisy fix loops, optionally suggest `--fix-threshold P1` (the project default stays at P2).
7. **Document everything** — the review summary must show which channels ran and which were skipped, with reasons.
8. **CLI-first** — use `mmr review --sync` as the primary entry point. Manual dispatch is a fallback only.
9. **Job storage** — the CLI stores job data at `~/.mmr/jobs/{job-id}/results.json`. Review results are available via `mmr results <job-id>`.
10. **Dispatch pattern** follows `multi-model-review-dispatch` knowledge entry. When modifying channel dispatch in this file, verify consistency with `review-code.md` and `post-implementation-review.md`.

---

## After This Step

When code review is complete, tell the user:

---
**Code review complete** — Verdict: [pass/degraded-pass]. All channels executed for PR #[number].

**Results:**
- Channels run: [list which of the 3 ran, noting any compensating]
- Findings fixed: [count]
- Remaining: [none / list]

**Next:** Return to the task execution loop.

---
