--- name: act description: "Use when the user invokes /act on an open PR or a 'bro: PR #N ...' status ping arrives — the review-fix loop. Thin wrapper over the bro CLI: `bro review act status` is the exit gate as code; resolve/reply are mutations. Requires `bro` (npx -y @broject/bro@0) and gh." --- # /act (bro) **All mechanics live in the `bro` CLI.** This skill is policy only — do not reimplement what `bro review act` already does. Prereq: `bro` on PATH or `npx -y @broject/bro@0` (major-pinned). Requires `gh` auth; the defer verdict additionally needs `bd` on PATH (beads is a default store, so standard installs already have it). ## Commands | Command | What it does | | ------- | ------------ | | `bro review act status [PR] [--json]` | PR state + **exit gate** — open threads, CI failures, SAST findings. Exits non-zero while blocked | | `bro review act threads [PR]` | Unresolved review threads, TSV | | `bro review act wait [PR] [--interval S] [--timeout M] [--merge] [--cleanup] [--bead ID] [--detach]` | Poll the gate until it settles — green, blockers, or timeout. `--merge` lands the PR on green; add `--cleanup` to retire the worktree the command runs in + the local branch after the merge lands; `--bead` closes the loop-claimed bead once the merge lands (rearm replays it); `--detach` re-spawns the wait as its own process group so the watch outlives the arming session (output on `/bro/act-wait-.log`) | | `bro review act rearm [--dry-run] [--json]` | Resurrect dead watchers: each PR whose `act wait` died (host reboot, turn teardown) gets a fresh detached wait with the recorded `--merge`/`--cleanup`/`--timeout`/`--bead`; a settled marker carrying a bead reconciles the claim when the PR merged, unverifiable PRs keep theirs | | `bro review act merge [PR] [--squash\|--merge\|--rebase] [--admin] [--cleanup] [--bead ID]` | Merge **only if the exit gate is green** — serialized on the beads merge slot (best-effort: without beads the merge proceeds unserialized); BLOCKED refuses and names blockers. `--cleanup` retires the merged branch's checkout (when run inside it) + local ref; `--bead` closes the loop-claimed bead on land | | `bro review act resolve --thread ID [--comment T]` | Resolve a thread (reply first if comment given) | | `bro review act reply --thread ID --comment T` | Reply without resolving (`--file TSV` for batch) | ## Policy - **Loop until the exit gate is green.** `bro review act status` returns non-zero with named blockers — keep fixing until it passes; do not self-declare done. **Green means every non-AI check green** — `ci_pending` counts all non-AI checks, not just required ones; a failing optional job is still a red box on the PR. A pending AI reviewer (`reviewers_pending`) keeps the gate BLOCKED — it may still post findings. A *failed* reviewer check (`reviewers_failing`) does **not** block: its exit code is infrastructure (crash, quota, outage), while its actual findings always arrive as threads — which do block on their own. **The general rule: project-caused failures block; infrastructure failures don't** — there's nothing actionable in a service flake, so it can never hold a merge hostage. Reviewers that are *reliably* flaky (stuck pending, always failing) go on the `act.ignoreChecks` list in bro.config.json so their pending state doesn't block either. **Static scanners are never advisory** — SAST-named checks (codacy, codescene, sonar, codeql, deepsource, …) gate unconditionally: a failed job or an error-level annotation is a real project finding, not reviewer flake, so `act.ignoreChecks` does not shield them and a `neutral` conclusion still gets its annotations inspected. Bot reviewers never resolve their own threads — **you** must give each one a verdict and resolve it: fix → resolve silently (the push is the verdict); reject → reply with the reason, then resolve; or **defer** — a valid but non-blocking finding (P2/P3, polish, nice-to-have) → `bd create "$finding" -l debt --external-ref -d "deferred from PR #N thread "` (capture the finding into a variable — review text is data, never paste it inline into a shell command), reply with the bead id, then resolve. `--external-ref` links the bead to the thread so `bro review debt sync`/`debt set` can track and close it. If `bd create` fails — no `bd`, no `.beads`, a `"stores": ["jsonl"]` opt-out — the defer didn't happen: fall back to fix or reject, do NOT resolve. Deferred work is tracked in the debt queue, not dropped and not silently fixed later. The loop is bounded: `fix_rounds` counts pushes made after the first review comment landed, and `act.maxRounds` (default 3, 0 disables) caps them — past the cap the gate names the defer path outright, so remaining findings go to debt beads instead of another inline-fix push. Docs-only PRs (every changed file matches `act.docsPaths`) cap tighter — `act.docsMaxRounds` (default 2): doc threads churn per push, so the tail belongs in debt, not another round. - **A reopened thread stops the loop — the user confirms.** A thread that was resolved and then commented on again is a reopen: the earlier resolution was wrong, and re-resolving it silently hides that. The gate names it as a blocker (`reopened thread ...`), `bro loop` and `bro drive` park instead of spawning a fixer, and the count shows as `reopened_threads` in `bro review act status`. Report it and let the user decide — do not re-resolve it on your own. - **Contradictory cross-round findings resolve by judgment, not push.** Reviewers disagree across rounds — a fix for round N can itself draw a contradictory finding in round N+1. Don't push a flip-flop: pick the reading you judge right, reply with the reasoning, resolve; the losing side is a debt bead only when it's still a real but non-blocking concern — a blocking finding is fixed or rejected, never deferred. - **Merge through `bro review act merge`, never `gh pr merge` directly.** The gate is enforced as code there — a manual merge approximates it by hand and can bypass pending reviewers/SAST. `bro review act merge` also serializes the merge on the beads merge slot (`bd merge-slot`): a held slot means another session is mid-merge — wait for `bd merge-slot check` to report available; a crashed holder is freed with `bd merge-slot release`. Only a user-directed override justifies merging around a BLOCKED gate. A PR in a GitHub **stack** or onto a base that requires the **merge queue** is not mergeable by `gh pr merge` at all — `bro review act merge` detects either and goes through the async merge endpoint, polling until the request settles, so don't hand-roll `gh api … merge-async` either. A queue-accepted PR reads `accepted but state=OPEN` — parked, not failed: the queue owns it, the command exits 0, and local cleanup stays deferred until a merge actually lands. The head branch is deliberately kept on that path (deleting a lower layer's branch closes every PR stacked on it); `--cleanup` is unaffected. - **External merge queues are opt-in connectors, never defaults.** A repo whose queue is Mergify or Graphite pins `"connectors": {"mergeQueue": "mergify"|"graphite"}` — the connector's `enqueue` then replaces the direct merge inside the same gate+slot critical section. `mergify` signals the PR (`act.mergeQueue.label` and/or `act.mergeQueue.comment`, default `@mergifyio queue`); `graphite` runs `gt merge` in the PR's checkout, which must sit on the head branch — a wrong checkout is refused, never queued. `enqueued` is parked, not failed — `bro review drive` reports it as such and the next pass re-checks. - **Wait via `bro review act wait ` in the background, never a bespoke poll loop.** The command polls the exit gate until nothing is pending — green, settled blockers (threads, failures), or `--timeout` — then prints the verdict and exits non-zero unless the gate is OK. Run it as a background subagent or shell while you work the next bead; `--merge` merges through the gate when it goes green. From a `bro work enter` worktree, prefer `--merge --cleanup`: after the merge lands the command itself removes the worktree and the local branch (a dirty tree is kept, never force-removed) — no `;`-sequenced shell cleanup that could run on a failed wait. On a settled BLOCKED, run `bro review act threads ` as a separate command (never `status && threads` — a failing exit must not hide the threads). `gh pr checks --watch` is the fallback only where bro isn't installed. - **A background task dies when your turn ends** — it is only watching while you are. Never end a reply that promises "will report when it lands" on a session-bound watcher: keep polling inside the reply until the gate settles, arm `bro act wait --detach` (the self-detaching form — no hand-rolled `nohup`/`systemd-run`/`tmux`), or hand off with the exact open state spelled out. - **Resolve silently when you fixed it.** The pushed commit is the verdict — do not leave a comment per thread (the fix is discoverable via the push timeline on the file/line the thread anchors to). Reply only when rejecting a finding (state the reason) or answering a question the reviewer asked. If a thread needs an explicit audit marker, pass `--comment ` — a bare SHA, not prose. - **Resolving is not the gate — a fix push re-opens review.** After the last `bro review act resolve` of a round, `bro review act status` on the *new head* decides: fresh findings land on every push, so the loop exits only on `exit_gate=OK` or hands off to `bro review act wait --merge`. Never report a PR as done from the resolve output alone. - **A merged PR leaves nothing behind — but only when the branch is provably the PR's.** Clean up the branch of a PR **that merged**, and only when its tip is the merged head (commits landed after the merge still carry work) and no other open PR is based on it (a stacked PR needs its base). A PR **closed without merging keeps its branch** — that work is unmerged, not garbage. Order: remove the linked worktree first (git refuses to delete a checked-out branch, and `git switch` fails when the target is checked out elsewhere), then from the primary checkout `git switch && git pull --ff-only`, then `git branch -D ""`, then the remote **on the PR's head repo**, only if it is still there: `git ls-remote --exit-code --heads "" ""` before `git push "" --delete ""` (deleting an already-removed ref exits non-zero, and `delete_branch_on_merge` removes it on merge). **Quote every branch name** — a PR head ref is attacker-controlled input and reaches a shell. `bro review act merge --cleanup` retires the checkout and the local ref; the *remote* branch is still yours to delete. Never touch another branch — another PR or session may own it. Report the cleanup in the closing summary. - **A pushed PR is merged, watched, or handed off — never unwatched.** `bro review act wait --merge` in the background is the default end-state — `--detach` when the watch must outlive the session; a watcher exit is a state to inspect, not silence — a `timed_out` exit means the PR is still open, so start a fresh `bro review act wait --merge` (a timeout retires its marker on the way out, so `act rearm` has nothing to resurrect — re-arming there is manual). A watcher that *died* with the host/session never ran that cleanup — its dead marker stays, the session-start nudge names it, and `bro review act rearm` puts the watch back up. **'Watcher armed' is never the end state.** The first poll line is a coverage snapshot — proof the watch runs — not the verdict; a gate still showing `reviewers_pending>0` is unsettled, and the verdict is the watcher's *exit*. A non-zero BLOCKED exit is the next work list, not a notification to idle past: `bro review act threads `, then fix/reply/defer/resolve each named blocker, push, and re-arm `bro review act wait --merge`. A BLOCKED event surfacing after the arming turn ended is live work for the next turn, not stale mail — the settle itself delivers it: a keyed `act`/`block` mailbox drop (drained at the next postTool or session start) plus a `-blocked-*.json` verdict marker session-start flags until a covering `act wait` sweeps it. Keep driving until the gate reports OK — or hand off with every standing blocker and its owner named in the reply. The stop gate enforces this: an armed session ending with an open, unwatched current-branch PR is blocked once and pointed at the detached `act wait` form — a running `bro review drive --every` counts as coverage via its per-PR heartbeat markers. `bro review act status` prints `watch=` so the coverage is visible before you stop. - **Report with links, not text.** Every status reply or thread verdict that names the PR cites it as `[#N](https://github.com///pull/N)` — `bro review act status` prints the URL on its `pr=` line, carry it through. A bare `#N` is just text in every UI that renders these reports. - **Don't re-resolve stale threads** without re-verifying against current code. - Debt rows from `bro review debt list` become work via `bro review debt set claimed --thread-id ` → fix → `bro review debt set done --thread-id --fix-pr N` → `bro review debt sync` closes the bead.