The PR review loop is a two-phase automated review process. This guide covers how it works, what to verify, and how to troubleshoot it.
See also:
- PR Review Watcher Architecture — implementation details and state machine design
- Merge Decision Instrumentation — metrics and latency tracking for merge decisions
- Review Backend Troubleshooting — diagnosis and recovery procedures
When a task completes with a branch push and await_review: true is set for the repo:
[Task executes] -> [Branch pushed] -> [PR opened] -> [Stage 1: Self-review]
↓ LGTM
[Squash-merge + Done]
↓ CONCERNS
[Revision pass, re-review, repeat]
↓ unresolved
[Stage 2: Human review]
↓ 👍 or timeout
[Squash-merge + Done]
The review watcher polls tracked PRs every 60 seconds (configurable via OPERATIONS_CENTER_WATCH_INTERVAL_REVIEW_SECONDS).
- The executor reads the diff against the base branch.
- The executor writes a verdict:
LGTMorCONCERNS. LGTM→ squash-merge, delete branch, task marked Done.CONCERNS→ the executor runs a revision pass on the branch, then re-reviews.- This loop repeats up to
max_self_review_loopstimes (default: 2). - If still unresolved after all loops → escalates to Stage 2.
The self-review verdict is posted as a PR comment with the <!-- operations-center:bot --> marker so it is never mistaken for human input.
- Watcher posts a comment on the PR explaining what it couldn't resolve automatically.
- A human can then:
- 👍 the PR or 👍 the latest bot comment → squash-merge + Done.
- Post a comment → the executor runs a revision pass; bot replies when done; repeat up to 3 times.
- If no action after 1 day → auto-merge (timeout fallback).
In config/operations_center.local.yaml:
repos:
MyRepo:
clone_url: git@github.com:yourorg/myrepo.git
default_branch: main
await_review: trueNo other config is required for basic operation.
All bot-posted comments carry the <!-- operations-center:bot --> HTML marker. This ensures:
- The bot never responds to its own comments as if they were human review requests.
- The bot never triggers another revision loop from its own output.
Required config to prevent bot loops:
reviewer:
bot_logins:
- operations-center-bot
- your-github-bot-accountAny login in bot_logins has its comments ignored by the review watcher, regardless of comment content.
Optional: restrict human-phase revisions to a whitelist:
reviewer:
allowed_reviewer_logins:
- your-github-username
- trusted-collaboratorWhen set, only comments from listed logins trigger revision passes in Stage 2. All other human comments are ignored.
Before enabling await_review: true for a new repo, verify:
-
reviewer.bot_loginsincludes every GitHub account the bot posts as. -
<!-- operations-center:bot -->marker is being appended to all bot comments (check any existing PR comment instate/pr_reviews/). - Branch protection rules on GitHub do not require status checks that the bot cannot satisfy — otherwise auto-merge will be blocked.
-
max_self_review_loopsis set to a reasonable value (default 2 is conservative; raise to 3 only if the first revision reliably resolves concerns). -
OPERATIONS_CENTER_PR_DRY_RUN=1is NOT set in production unless you intend to prevent all PR actions.
Every PR action is logged in two places:
- Plane comment on the task — transition reason, run_id, outcome.
- Retained artifact under
tools/report/execution_plane/<task-id>/<run-id>/— full diff, executor stdout, summary.json.
PR state files live in state/pr_reviews/<owner>/<repo>/<pr-number>.json. These track:
- Current phase (
self_revieworhuman_review) - Loop count
- Last action taken
- Escalation timestamp
To inspect the state of a specific PR:
cat state/pr_reviews/<owner>/<repo>/<pr-number>.json | python3 -m json.toolIf the review watcher restarts and misses PRs that were opened while it was down:
./scripts/operations-center.sh backfill-pr-reviewsThis scans GitHub for open PRs on all await_review-enabled repos and creates missing state files. Run this after any watcher restart.
Set OPERATIONS_CENTER_PR_DRY_RUN=1 to log all intended PR actions (merge, comment, push) without actually touching GitHub. Use this to:
- Verify the review watcher is polling correctly.
- Test the transition logic after a config change.
- Debug a stuck review state without risk of unintended merges.
OPERATIONS_CENTER_PR_DRY_RUN=1 ./scripts/operations-center.sh watch --role review- Check that
await_review: trueis set for the repo in config. - Check that the PR state file exists in
state/pr_reviews/. If not, runbackfill-pr-reviews. - Check the review watcher log for errors:
logs/local/watch-all/review.log.
- Check the loop count in the PR state file (
self_review_loop_count). - If the count is at
max_self_review_loops, the watcher should have escalated. Check for a Stage 2 comment on the PR. - If the PR has no escalation comment, check the review watcher log for errors around the
pr_self_review_escalateevent.
- Confirm
reviewer.bot_loginsincludes the bot's GitHub login. - Confirm the bot comment includes
<!-- operations-center:bot -->. Checkstate/pr_reviews/and the actual PR comment on GitHub.
This is safe. The review watcher checks merge status before every action. If it sees the PR is already merged, it transitions the task to Done and cleans up the state file without retrying.
- Confirm the human's GitHub login is in
allowed_reviewer_loginsif that config key is set. An empty or missingallowed_reviewer_loginsmeans all logins are allowed. - Confirm the human commented on the PR itself, not on a commit or on the Plane task.
- Check that the comment does not carry the
<!-- operations-center:bot -->marker — if it does, the watcher will skip it.
When a PR in Stage 2 (human review) receives repeated human comments but Kodo produces zero-change revision passes each time, the reviewer watcher will eventually close the PR and create a fresh goal task rather than looping indefinitely.
Trigger condition: REQUEUE_AS_GOAL_ZERO_CHANGE_THRESHOLD consecutive zero-change revision passes (default: 2). A "zero-change pass" is detected when the revision diff between the old and new head commits is empty — Kodo acknowledged the comment but produced no code changes.
What happens:
- The PR is closed with a
<!-- operations-center:bot -->comment explaining the requeue. - A fresh
task-kind: goaltask is created inBacklogwith the original goal text and a note that the previous PR stalled. - The original Plane task is marked Done.
The fresh goal task allows a human to review the scope and promote it when ready, rather than letting the PR loop consume review cycles with no progress.
State tracked in: state/pr_reviews/<owner>/<repo>/<pr-number>.json — the zero_change_count field increments on each zero-change pass and resets if a non-empty revision is detected.
Run these scenarios against a controlled test repo to validate the loop before enabling it in production:
- Happy path (LGTM): Create a task with a simple goal, let it complete, verify Stage 1 merges the PR automatically.
- Revision loop (CONCERNS): Make the diff produce a CONCERNS verdict (e.g. remove a test), verify the executor revises and re-reviews.
- Human escalation: Let max_self_review_loops run out, verify Stage 2 escalation comment appears, verify 👍 triggers a merge.
- Bot loop prevention: Post a comment from a bot login listed in
bot_logins, verify no revision is triggered. - Dry-run: Set
PR_DRY_RUN=1, run a full cycle, verify no GitHub writes but full log output.