Auto-review recovery
How coordinate --auto-review resolves review loops that stopped without saying so.
The latch
tryClaimAutoReview writes autoReviewStartedAt as a one-way claim so two
coordinators cannot review the same PR. The loop it starts is detached, and
nothing recorded how it ended. So:
- a review that finished stayed
autoReviewStatus: startedfor ever, and - a review that was killed mid-round stayed
startedtoo, and was never retried — the claim kept every later tick away.
Killed reviews are routine, not exotic: systemd stops a unit by signalling its whole cgroup, so every coordinator restart kills the reviews it spawned — each deploy, crash, or OOM kill.
How a loop reports its end
cli/coordinate-auto-review.sh writes MAINARK_AUTO_REVIEW_EXIT_FILE
(.mainark/logs/coordinate-auto-review-pr-N.exit.json) from its traps:
| How it ended | outcome |
|---|---|
| Consensus | consensus |
| Rounds exhausted | exhausted |
| Anything else that exits | error |
| SIGTERM / INT / HUP | terminated |
| SIGKILL, power loss | (no file) |
The outcome matters more than the exit code. On SIGTERM bash runs the EXIT
trap with $? of the last completed command — usually 0 — so a review killed
mid-run would otherwise be recorded as a success. The stale file is deleted
before each spawn so an earlier run's result cannot be mistaken for this one's.
Reconciliation
Every tick, before starting new reviews, reconcileAutoReviewClaims looks at
claims still starting / started:
| Situation | Action |
|---|---|
Loop pid alive (and, on Linux, /proc/<pid>/cmdline is a review loop) | leave it |
starting, no pid, under 10 min | leave it — spawn in progress |
Exit file consensus / exhausted / error | record final status; no re-run |
No exit file, or terminated | orphan — see below |
For an orphan, the review/fix ledger rows it left running are cancelled first
(otherwise eligibility reports review_active and nothing can restart), then:
| Condition | Result |
|---|---|
PR MERGED or CLOSED | flag needs_human, keep latch |
PR state unknown (e.g. gh failed) | defer — leave the claim untouched, ask again next tick |
| Claim older than 24 h | flag needs_human, keep latch |
| Already recovered twice | flag needs_human, keep latch |
| Otherwise | clear the latch, count the recovery, restart |
Released orphans are restarted even in --watch mode, which otherwise only
considers runs refreshed in the current tick. Each claim is resolved exactly
once: flagged and finished claims leave the started state and are not
reconsidered.
The pid check reads the command line on Linux because a pid can be reused after
a crash or reboot; kill(pid, 0) alone would keep an orphan latched until the
unrelated process exited.
Separately from orphans, auto-review never starts on a run whose ledger issue is
done or cancelled (issue_terminal), and mainark adopt refuses such an
issue whether it was named with --issue or reached through the PR mapping.
Slack notices
Reconciliation is also where a person hears how the loop ended. Each claim is
resolved once, so each notice is sent at most once (recorded as
autoReviewNotified in the run's metadata).
| Outcome | Notice |
|---|---|
consensus | PR ready for your review — rounds, fixes pushed by Cursor Cloud, implement cost, Claude's summary. Mainark never merges. |
exhausted | Needs a human — rounds ran out; outstanding MUST FIX items (up to 5) |
error | Needs a human — the loop exited with an error |
| Orphan flagged (stale, recovery cap) | Needs a human — the review was interrupted and not restarted |
| Orphan on a merged/closed PR, released orphan, deferred, still running | nothing |
Notices go through softPost, which skips when SLACK_WEBHOOK_URL is unset. A
failed post never blocks or undoes reconciliation.
Before this, the unattended loop posted nothing on success: the "PR ready" and "review failed" notices existed only in the daemon pipeline. A PR could reach consensus with nobody told.
Re-running a loop: mainark review-loop --pr N
A loop runs once per implement run. When it has ended (consensus, exhausted, error), the watching coordinator never starts another, because it only looks at runs that finish in the current tick. To run it again:
mainark review-loop --pr 84 [--max-rounds 3]
It refuses while a loop is still running for the PR, and checks eligibility
before changing anything, so a refusal leaves the ledger untouched. On success it
archives the previous loop's record into the run's autoReviewHistory, moves
the old exit file aside (otherwise the reconciler would finish the new loop with
the old outcome), and starts through startAutoReviewForRun, the coordinator's
own path. Claim, reconciliation and Slack notices behave exactly as for a new PR.
Only PRs Mainark built have an implement run; use mainark adopt for others.