Sub-agent type: explore; budget: 3 min.
PrNumber.- The pushed
HeadOidfrom step 7 (for the independent sanity check). - Whether the loop is in normal mode or single-iteration mode (decided at step 1).
{ converged, head_oid, latest_review_commit_oid, submitted_at,
open_thread_count, open_threads_awaiting_reply, escalated_threads }
converged is the single source-of-truth boolean — Converged: true
returned by 02-check-review-status.ps1.
Run the status check, passing -SingleIteration iff the loop took the
fallback at step 1:
pwsh ./scripts/02-check-review-status.ps1 -PrNumber <n>
# single-iteration variant:
pwsh ./scripts/02-check-review-status.ps1 -PrNumber <n> -SingleIterationThen run an independent HEAD-vs-LatestCopilotReview.commitOid
sanity check — the parent's recorded HeadOid from step 7 should
match HEAD and (in normal mode) match the latest review's
commitOid.
After the status check, the parent agent must branch on converged:
if converged == true:
run step 10 once (cleanup outdated)
call task_complete with proof (HeadOid, LatestCopilotReview.commitOid, submittedAt)
DONE — exit the loop
else:
# non-converged = a fresh Copilot finding OR an unresolved human thread.
# round = count of Copilot review submissions in the PR's history,
# read deterministically from the API (NOT a mental tally):
# pwsh ./scripts/09-review-round.ps1 -PrNumber <n> -> {Round, RecapDue}
if RecapDue == true: # Round is 10, 20, 30, ...
RUN THE RECAP GATE (see "Round cap & recap gate" below) BEFORE looping:
recap ALL prior rounds, then pick CONTINUE / REVERT-AND-SHIP / HAND-OFF.
CONTINUE -> fall through and start another round
REVERT-AND-SHIP -> drop drifted commits, ship the in-scope result, exit
HAND-OFF -> escalate to the user with the recap, exit
GO BACK TO STEP 1 — start another round
(re-trigger via 01-request-review.ps1, wait via 02-wait,
list via 03-list-threads, triage, fix, push, reply+resolve,
re-check via this step)
A non-converged result is never terminal on its own — each round
addresses the open review feedback on the previous round's HEAD,
whether that's a Copilot finding or a human review comment (this
skill handles both). The loop terminates only when there are no new
review comments from either source AND every open thread — Copilot
or human — has a reply from the agent (a thread the agent escalated
to the user counts as replied; it stays open in OpenThreadCount as an
explicit hand-off, not as loop work). But "never terminal" must not be
read as "infinite": a bot-review loop has no guaranteed fixed point and
can drift into over-engineering or oscillation. No script enforces a
cap or stops the loop — capping is a reasoning decision the parent owns
at the round-cap recap gate
below. What is scripted is the round count itself
(09-review-round.ps1), so the
gate's trigger is deterministic rather than a fallible mental tally
(and oscillation — the same finding re-raised across rounds — is
broken earlier per
04-triage.md).
-SingleIteration mode is the one exception: by definition, it
runs one round only (the trigger path is unavailable), and the
converged result is taken as terminal whichever way it goes.
02-check-review-status.ps1 implements a PR-state guard plus three Converged branches (see the Converged = if (...) block near the end of that script for the canonical source):
- PR State guard (overrides everything) — if
State != 'OPEN'(CLOSED / MERGED),Converged: falseregardless of all other flags. The agent cannot push to a non-OPEN PR; surface the state change to the user and abort the loop rather than callingtask_complete. - Normal (Copilot-driven) mode — a Copilot review exists OR
CopilotPending: true:Converged: trueiffReviewAtHead && NoNewComments && OpenThreadsAwaitingReply == 0. - Single-iteration mode (
-SingleIterationpassed because the loop took the fallback at step 1):Converged: trueiffOpenThreadsAwaitingReply == 0. The stale-review checks can never advance without a new Copilot review, so they're omitted. - No Copilot review ever observed AND not pending (brand-new PRs with zero findings, or PRs where the trigger silently failed and the script wasn't called with
-SingleIteration):Converged: trueiffOpenThreadsAwaitingReply == 0. Do NOT trust this as "loop done" before step 1 has fired — it just means there's no human-thread work pending. The parent agent MUST run01-request-review.ps1first (per step 1) and re-check; treating brand-new-PR convergence as terminal will short-circuit the entire loop.
OpenThreadCount MAY be > 0 when escalated-to-user threads stay
open — that's an explicit human hand-off, not a loop failure. Return
the list of escalated thread_ids so the parent can include them in
the convergence proof.
No script enforces a max-rounds cap or stops the loop — a hard number can't tell a productive round from a drifting one. Instead the parent agent runs a recap gate as reasoning: default STOP at every 10th round (10, 20, 30, …) before looping back to step 1, recap all prior rounds, and decide whether the loop is still serving the PR's original scope.
What is scripted is the count, so the gate's trigger is
deterministic instead of a fallible mental tally. A round is one
execution of step 1 — one Copilot-review
trigger at the top of the loop — which produces exactly one Copilot
review submission. 09-review-round.ps1
counts those submissions straight from the PR's API history and reports
whether the cadence is hit:
pwsh ./scripts/09-review-round.ps1 -PrNumber <n>
# {"PrNumber":<n>,...,"Round":20,"RecapInterval":10,"RecapDue":true}Run it at the top of the non-converged branch and gate on RecapDue.
Because the count is derived from history, not remembered, it can't
drift even across a 100+ round run — the exact failure this gate exists
to catch. The cap counts review rounds (Copilot review submissions),
not sub-agent calls, tool calls, or individual fix edits — so a round
that triages five threads still counts as one. The cadence is the
-RecapInterval knob (default 10). The script reports the trigger only;
it never decides the verdict.
This exists because an unbounded bot-review loop is the failure mode this skill was built to survive: a real run drifted for 156 rounds — later rounds "fixing" things the PR never set out to change, eventually reverting their own earlier fixes. The gate catches that class of drift early, every 10 rounds, instead of once at the end.
- Original PR scope — the issue/PR title and the diff at the PR's base. This is the yardstick; everything else is measured against it.
- Per-round ledger — for each round so far: the Copilot finding, the disposition (fixed / declined / escalated), and the resulting change (files + intent in one line).
- Drift signals across the whole history:
- Out-of-scope — a change that doesn't trace back to the original issue/PR goal (new feature, adjacent refactor, polish the PR never promised).
- Over-engineering — defensive layers, abstractions, or config added solely to satisfy bot nits, not the PR's actual goal.
- Wrong-direction — a fix that later rounds had to undo, work around, or re-fix (self-revert / oscillation across rounds).
- Belongs-in-separate-PR — a legitimate improvement that is nonetheless unrelated to this PR's stated change.
- Scope/complexity growth — diff size or file count climbing while the original goal was met rounds ago.
| Verdict | When | Action |
|---|---|---|
| CONTINUE | Every round so far traces to the original PR scope; no drift signals; Copilot is still surfacing in-scope findings. | Loop back to step 1 for the next 10-round block. |
| REVERT-AND-SHIP | One or more rounds drifted (over-engineering / wrong-direction / oscillation) but the in-scope fixes are sound. | git revert (or drop) only the drifted commits, keep the in-scope ones, run step 6 build/test, then ship the clean result. Record which rounds were reverted in the convergence proof. |
| HAND-OFF | Drift is entangled with in-scope work, the right fix is a redesign, or the change belongs in a separate PR. | Stop the loop, reply on the relevant threads, and escalate to the user with the recap and a recommendation (separate PR / redesign). Do not keep looping. |
The trigger is scripted but the verdict is agent reasoning —
deliberately. 09-review-round.ps1
makes the count deterministic (so the gate can't be missed), but
which verdict to pick stays a judgment call: no number can tell a
productive round from a drifting one. The recap is cheap (read the
per-round commits + the PR base diff); the cost of skipping it is
another runaway loop.
- Trust
02-check-review-status.ps1'sConvergedflag, not your own re-derivation. The script enforces all three conditions (normal mode) or the simplified condition (single-iteration) and is the canonical source. - Don't call
task_completeuntilconverged == true. Print the proof (HeadOid,LatestCopilotReview.commitOid,submittedAt,OpenThreadsAwaitingReply: 0, list of escalated threads ifOpenThreadCount > 0) in the completion message. -SingleIterationis sticky to the fallback decision. If step 1 took the fallback, every step 9 in this loop uses-SingleIteration; don't flip it mid-loop.- PR State != OPEN aborts the loop. If
StateisCLOSEDorMERGED,Convergedis forcedfalseby the script's state guard. The parent agent cannot push to a non-OPEN PR — surface the state change to the user and stop the loop rather than retrying or callingtask_complete.