Cloud fix executor

MAINARK_EXECUTOR=cursor-cloud runs the fix leg of the review loop in Cursor Cloud, on the PR's own branch.

Why

Every other fix executor runs locally. local and cursor shell out to the Cursor CLI (cursor agent); codex needs OPENAI_API_KEY. A headless host that dispatches implement runs to Cursor Cloud has neither.

Observed on the first live run on the Hetzner host: implement (Cursor Cloud) and review (Claude) both worked, and the fix phase printed Error: required command not found: cursor. It was harmless only because the review passed on round 1. A FAIL verdict could have been reviewed but never fixed, and the loop would have spent all its rounds re-reviewing unchanged code.

How

The executor reads .reviews/pr-N.json, decides whether there is anything to fix, and if so starts a Cursor Cloud agent with:

OptionValueWhy
startingRefPR head branchWork on the PR's code, not main
prUrlthe PRAttach to the existing PR
workOnCurrentBranchtruePush to the head branch (ADR-002)
autoCreatePRfalseNever open a second PR

It waits for the run, then requires the PR head SHA to have moved. A finished agent that pushed nothing is a failure, because otherwise the loop would go back to review identical code.

Cost guards

  • Worktree sync. After a confirmed push the executor fast-forwards the local PR worktree (merge --ff-only) to the pushed SHA. The next review round validates there; without the sync it re-tests the pre-fix code, fails, and dispatches again. A diverged worktree fails the round rather than being reset.

  • Branch check, before paying. The workspace must be checked out on the PR's head branch before a run is dispatched, and again before syncing. When no PR worktree exists, review falls back to the main clone; a fast-forward there would move the operator's branch to the PR head.

  • No-push guard. .reviews/pr-N.cloud-fix.json records the head the last cloud fix worked on and whether it produced a usable push. If it did not on the same head, the executor refuses another paid run. "Did not" covers every way a created run can fall short — finished unchanged, ended in error or cancelled, timed out, push unconfirmed, or pushed but the worktree sync failed (recorded against the new head). A create call that is rejected outright records nothing, since it cost nothing and may be transient. A new head re-enables it; deleting the marker forces a retry. The review loop has no brake of its own and will otherwise dispatch once per round.

  • A lost stream is confirmed, not believed. If wait() throws, or resolves with anything other than finished, the executor asks the API for the run's real state and keeps polling while it is still running. At the deadline, or if the state cannot be read at all, the run is cancelled — never left running unmonitored. Found live: the stream reported error: Run stream is no longer available while the agent was still working and could have pushed after the loop had reached consensus.

  • SHOULD FIX alone does not start a paid run. A PASS verdict whose only items are suggestions no longer dispatches a cloud fix (live: $0.41 on "add database indexes" after approval). Set MAINARK_CLOUD_FIX_SHOULD_FIX=1 to opt back in. A fix already running for a failure still triages SHOULD FIX items.

The first two were found on the first live run: one real fix, then four paid runs against an already-fixed branch before the round cap.

Differences from the local fix

  • Findings are inlined in the brief. The VM clones the repo and cannot read .reviews/ on the host.
  • The agent commits and pushes. Locally, Mainark commits after the run.
  • Same run/skip gate as cli/cursor-fix-pr-review.sh. The review loop calls the fix phase even after a PASS; without the gate this would start a paid agent on every approved PR.

Configuration

EnvDefault
MAINARK_EXECUTORautoSet to cursor-cloud
CURSOR_API_KEY, REPO_URL—Same as implement
MAINARK_CLOUD_FIX_TIMEOUT_MS45 minRun is cancelled on timeout
MAINARK_CLOUD_FIX_SHOULD_FIXoffLet SHOULD FIX items alone start a paid run
MAINARK_VALIDATE_CMDpnpm mainark:validateNamed in the brief

Set MAINARK_EXECUTOR, not MAINARK_FIX_EXECUTOR: the bash pipeline derives the latter from the former and overwrites a directly set value.

Requires the TypeScript fix pipeline (the default). The legacy bash router (MAINARK_FIX_TS=0) refuses cursor-cloud with an explicit error.