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: started for ever, and
  • a review that was killed mid-round stayed started too, 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 endedoutcome
Consensusconsensus
Rounds exhaustedexhausted
Anything else that exitserror
SIGTERM / INT / HUPterminated
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:

SituationAction
Loop pid alive (and, on Linux, /proc/<pid>/cmdline is a review loop)leave it
starting, no pid, under 10 minleave it — spawn in progress
Exit file consensus / exhausted / errorrecord final status; no re-run
No exit file, or terminatedorphan — 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:

ConditionResult
PR MERGED or CLOSEDflag needs_human, keep latch
PR state unknown (e.g. gh failed)defer — leave the claim untouched, ask again next tick
Claim older than 24 hflag needs_human, keep latch
Already recovered twiceflag needs_human, keep latch
Otherwiseclear 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).

OutcomeNotice
consensusPR ready for your review — rounds, fixes pushed by Cursor Cloud, implement cost, Claude's summary. Mainark never merges.
exhaustedNeeds a human — rounds ran out; outstanding MUST FIX items (up to 5)
errorNeeds 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 runningnothing

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.