| name | merge-coordination |
| description | Main-agent skill for coordinating the merge of a PR after the canonical `minsky-reviewer[bot]` review. Covers: gathering bot review status, running the local smoke test, reading posted review prose before retriggering, diagnosing reviewer-bot silence, and bypass-merging bot-authored PRs. |
| user-invocable | true |
Merge Coordination Skill
Coordinate the merge of a PR after the minsky-reviewer[bot] posts its review.
Arguments
The argument is a task ID (e.g., /merge-coordination mt#328) or a GitHub PR URL. The bot reviews automatically; this skill is invoked when you need to check merge status, diagnose silence, or execute the bypass-merge path.
Process
0. Load the merge toolset bundle (mt#2822)
Check first whether /implement-task's Step 0b bundle already ran this conversation — its
18-tool bundle already covers tasks_spec_get, session_pr_wait-for-review,
session_pr_checks, forge_check_runs_list, session_pr_get, session_pr_merge, and
session_commit. Re-requesting an already-loaded tool via ToolSearch is harmless (the
harness dedupes), but the two call shapes below keep each entry point requesting only what it
actually still needs, so this step's own baseline-count evidence isn't diluted by counting
redundant re-selects as if they were new discovery:
Chain-walked from /implement-task this conversation — only 2 tools are genuinely new:
ToolSearch(query: "select:mcp__minsky__session_pr_review_context,mcp__minsky__reviewer_retrigger", max_results: 10)
Invoked standalone (this skill entered directly, no prior /implement-task bundle this
conversation) — load the full merge-coordination subset in one call:
ToolSearch(query: "select:mcp__minsky__session_pr_review_context,mcp__minsky__tasks_spec_get,mcp__minsky__session_pr_wait-for-review,mcp__minsky__session_pr_checks,mcp__minsky__forge_check_runs_list,mcp__minsky__session_pr_get,mcp__minsky__session_pr_merge,mcp__minsky__session_commit,mcp__minsky__reviewer_retrigger", max_results: 15)
1. Gather context
Use mcp__minsky__session_pr_review_context with the task ID or session ID to fetch all review data in a single call. This returns PR metadata, CI check runs, the raw diff, the task spec, and existing review threads with resolved/outdated state.
If the session tool fails (e.g., no session exists for this PR), fall back to fetching in parallel:
- PR metadata:
mcp__github__pull_request_read with method: "get"
- CI status:
mcp__github__pull_request_read with method: "get_check_runs"
- Bot review status:
mcp__github__pull_request_read with method: "get_reviews" — look for a review by minsky-reviewer[bot]
The goal of this step is to surface: (a) whether the bot has posted a review, (b) the review state (APPROVED, CHANGES_REQUESTED, COMMENT), (c) whether CI is green, and (d) whether any BLOCKING findings remain open.
2. Identify the task
Extract the task ID from the PR title or branch name (e.g., mt#671 from task/mt-671). If the task spec was not already returned by step 1, fetch it with mcp__minsky__tasks_spec_get. This is needed for smoke test targeting (step 6.3) and bypass-merge audit trail (step 8).
3. Watch for bot review + CI in parallel
When waiting for the bot's review and CI, kick off both in the same message. Use a single tool-call message with TWO parallel calls:
mcp__minsky__session_pr_wait-for-review(task: "mt#X", reviewer: "minsky-reviewer[bot]") — waits for the next bot review
mcp__minsky__session_pr_checks(task: "mt#X", wait: true, timeoutSeconds: 600) — CI polling waits synchronously
The reviewer waits until a review arrives; CI polling waits synchronously. When both complete, the merge decision can be made in one step instead of two sequential round-trips. This pattern saves 5–10 minutes per merge cycle.
On subsequent waits (after pushing a fix — pass since set to the previous review's submittedAt timestamp so the call returns the NEW review, not the stale one):
mcp__minsky__session_pr_wait-for-review(
task: "mt#<id>",
reviewer: "minsky-reviewer[bot]",
since: "<previous review.submittedAt>"
)
Reference: feedback_parallel_subagent_dispatch_pattern for the broader parallel-dispatch pattern this is a special case of.
6.3 Smoke test
Run at least one CLI command that exercises the changed code path against the PR branch. Examples:
- DI-changing PR →
bun src/cli.ts tasks list to verify the container initializes correctly
- Session-mutation PR →
bun src/cli.ts session list
- New CLI command → invoke the new command with a representative argument
- Docs / prompt-only PR → may skip with rationale recorded in the review body
Record one of three outcomes for the review body's Smoke: line:
pass — <command run> if the command exited 0
fail — <command run>: <stderr summary> if it exited non-zero (BLOCKING finding)
skipped — <rationale> for docs / prompt-only / config-only PRs where no code path runs (skipped is acceptable; the pre-merge hook treats it as a valid value, not as missing)
Smoke is an independent gate, not part of CI-status counts. The CI status line counts only GitHub Actions check_runs; the Smoke line is a separate field that the pre-merge hook parses independently. A Smoke=fail blocks merge regardless of CI N/M; a Smoke=skipped is treated as a valid value (not as missing).
The smoke catches PR-introduced regressions that pre-merge CI may have missed (container init failures, command-registration breakage, etc.). It does not cover concurrent-merge interactions — those are tracked separately in mt#1592.
7a. Reviewer-bot silence on subsequent commits
After pushing a follow-up commit that addresses BLOCKING findings, minsky-reviewer[bot] should fire a new review within ~5 minutes. If it doesn't, that's almost certainly the webhook-miss-on-subsequent-push reliability class (mt#1110-tracked; instances on mt#677, mt#748, PR #763).
Diagnosis steps:
-
Confirm the latest push reached GitHub: mcp__minsky__session_pr_get and check head.sha matches your local HEAD.
-
Check whether CI fired on the same push: mcp__minsky__session_pr_checks (or mcp__github__pull_request_read with method: "get_check_runs" if the Minsky tool is unavailable). If CI also produced 0 check_runs, that's a separate webhook/CI-trigger problem — note both classes when filing a reliability issue. The merge gate enforces this independently as of mt#1309. The CI-check-never-ran class (zero check_runs) is distinct from this section's reviewer-webhook-miss class and can co-occur on the same PR: work docs/ci-check-never-ran-playbook.md (mt#2800) for the CI piece, and continue this section's remaining steps below for the reviewer-silence piece.
-
Check the service is serving the RIGHT app — read the health BODY, not just the status code (mt#3154). A 200 from the reviewer host does NOT mean the reviewer is running there. A Railway source-cutover can leave the service building a DIFFERENT app from the repo, which boots fine and answers /health with 200 — so a status-only check reports "healthy" for a service that cannot review anything. Discriminate on the body:
- reviewer (correct):
{"provider":"openai","model":"gpt-5","tier2Enabled":true}
- wrong app (the Minsky MCP server):
{"server":"Minsky MCP Server"}
If the body lacks the reviewer signature, stop working this ladder — this is not a webhook miss, and neither retriggering nor bypassing addresses it. It is a deploy/source problem: route to the railway:use-railway skill and memory mem#700, and recover by forcing a fresh deploy from the CURRENT source via a service-VARIABLE change (e.g. set then remove a throwaway variable).
Do NOT reach for railway redeploy. It replays the last build artifact — which on a source-cutover IS the wrong app — so it cheerfully re-deploys the same wrong image and reports success. Two exit-0 attempts that leave the served app unchanged is a wrong-OUTCOME 2-strikes trip: stop improvising and load the skill (see error-investigation.mdc).
-
Wait at most 5 minutes. Do not loop indefinitely.
Unblock options (in preference order):
- Empty commit to wake the webhook: push an empty commit (
session_commit with noFiles: true and noStage: true) and wait again. Often resolves the miss.
- Bypass merge via
gh api PUT /repos/.../pulls/N/merge (merge_method=merge, with audit message naming the substantive fixes that landed). Only after BLOCKING findings are addressed and the remaining gap is the missing reviewer signal.
- Track the instance in the agent memory store (
mcp__minsky__memory_create) so the calibration work has data points.
The webhook-miss class is distinct from the same-App-identity APPROVE block above: same-App is a structural gate (when minsky-ai[bot] is both author and reviewer, GitHub rejects the APPROVE — see step 8 event selection), webhook-miss is a reliability gate against the cross-identity minsky-reviewer[bot] failing to fire. Recognize which one you're hitting before choosing a recovery path.
7b. Read posted review prose before retriggering (mt#2829)
Before calling mcp__minsky__reviewer_retrigger for ANY reviewer-silence or defective-verdict situation — including a CHANGES_REQUESTED or COMMENT review whose structured findings channel came back empty, or any verdict that looks wrong at a glance — read what the reviewer actually posted FIRST:
mcp__minsky__session_pr_get(task: "mt#X", reviews: true)
This returns every posted review on the PR — reviewer login, state, submitted_at, FULL body text, and per-review inline comments (path, line, body) — in submission order, across all rounds. Works for both bot and human reviews. Bodies are included (capped only if enormous, with the truncation stated explicitly via a [TRUNCATED: ...] marker and a bodyTruncated flag — never a silent cut); diffs are never included.
Why this is mandatory, not optional. A defective-looking verdict — e.g. CHANGES_REQUESTED with zero structured findings — usually still carries real diagnostic prose in the review's raw body. The structured channel being empty does not mean the reviewer had nothing to say. Retriggering blind discards that prose permanently: the re-review may return a different verdict (e.g. APPROVED) and the original content becomes unrecoverable.
Originating incident (PR #1893, conversation bdf8f782): after a defective CHANGES_REQUESTED-with-zero-findings verdict, the agent burned 3 ToolSearch queries plus session_pr_get(content:true) and session_pr_review_context attempts, found nothing that returns review text, and blind-retriggered via reviewer_retrigger — the re-review came back APPROVED and the original defective verdict's content was never seen or diagnosed. session_pr_get(reviews: true) is the in-band fix for that exact gap.
Only retrigger after reading the posted review, and only when either:
- the PR genuinely has zero reviews (an empty
reviews: true result) — a legitimate webhook-miss or CI-not-fired situation; work the §7a ladder first, or
- the posted review's prose is itself uninformative (a truly empty body AND no inline comments) — a genuine defective-verdict case worth retriggering.
If the body has real diagnostic content, work from that content directly (fix the finding, or dispute it with evidence) rather than discarding it via retrigger.
Composes with §8's mt#2777 SC#2 precondition, not a duplicate of it. §8's "direct reviews-list read before citing silence" answers does a review exist on the current HEAD (existence, gating a bypass). This step answers what does an existing review actually say (content, gating a retrigger). Both checks read from the same underlying reviews list but serve different actions — run the one relevant to what you're about to do (bypass vs. retrigger), and both if you're evaluating both paths.
8. Bot-authored PR merge
This section applies when the PR author is minsky-ai[bot] or any bot identity.
GitHub structurally blocks self-approval: a PR author cannot APPROVE their own PR. When minsky-ai[bot] opened the PR and the same App identity is submitting the review, the reviewer can only post COMMENT — never APPROVE. This is a platform constraint, not a configuration issue.
Prerequisite checks before merging:
- Chinese-wall reviewer (
minsky-reviewer[bot]) has cleared all blocking findings (review posted to GitHub)
- CI is green (all required checks passing)
- No
REQUEST_CHANGES reviews outstanding that haven't been resolved
Standard merge path:
Use mcp__minsky__session_pr_merge. This succeeds after reviewer-bot APPROVE or when the review body satisfies the merge-gate's text patterns.
Mandatory precondition: direct reviews-list read before citing silence (mt#2777 SC#2). Before citing reviewer-silence as grounds for ANY bypassReason — even after the §7a diagnosis ladder or a session_pr_wait-for-review timeout — perform a DIRECT reviews-list read on the PR's current HEAD (mcp__github__pull_request_read method get_reviews, or a fresh short-timeoutSeconds session_pr_wait-for-review call) immediately before invoking the bypass. A wait-for-review timeout alone is NOT sufficient evidence of silence — mt#2751's near-bypass timed out twice while a real review had landed mid-churn. (mt#2777 SC#1 folds a fresh re-read into the timeout payload itself — finalCheckPerformed / reviewerCheckRunState — but the bypass-decision moment is a SEPARATE point in time from when the wait returned, so this direct read is required again, right before the bypass call, not inferred from an earlier timeout.) If the direct read finds a review on the current HEAD, the silence premise is false — refuse the bypass and process the found review instead.
Numbered step: verify the condition against its definition before invoking any bypass (mt#2777 SC#2). Before calling either bypass path below, name which ONE of the four documented conditions is firing and verify it against its precise definition — do not invoke the bypass on a name-level match alone:
- Self-reversal — round N's BLOCKING finding contradicts an earlier round's ACCEPTED fix ON THE SAME ARTIFACT STATE. The reviewer re-reviewing DIFFERENT commit states the agent itself created (e.g. an add-then-revert churn across rounds) is NOT self-reversal — each round reviewed genuinely different code, so there is no contradiction to resolve by bypassing.
- CoT-leakage — the reviewer emitted raw reasoning / chain-of-thought prose instead of structured findings, on the SAME HEAD, at least twice consecutively.
- Webhook-silence — the reviewer has been absent for >5 minutes AFTER completing the full §7a diagnosis ladder, which now INCLUDES the direct reviews-list read above.
- Verified-false-positive — the cited code, when actually re-read, does NOT contain the claimed defect. An out-of-diff, pre-existing, but FACTUALLY TRUE finding is NOT a false positive — surface and file it (per mt#1882) rather than bypassing.
If the named condition, once checked against its definition, does not actually hold, the bypass is refused — continue waiting, fix the finding, or escalate instead.
Preferred audited bypass — in-band session_pr_merge forceBypass (mt#2215):
mcp__minsky__session_pr_merge(task: "mt#<id>", forceBypass: true,
bypassReason: "<evidence: which review is a verified false-positive and why>")
This is the in-band replacement for the raw gh api PUT below — no hand-run CLI, and Minsky session cleanup still fires. It requires a non-empty bypassReason and a present (non-DISMISSED) CHANGES_REQUESTED review (for reviewer ABSENCE / webhook-miss with no CHANGES_REQUESTED, use the acceptStaleReviewerSilence flag instead), refuses on failing status checks or other merge blockers, auto-dismisses the blocking review using bypassReason as evidence, writes the canonical audit signature into the merge-commit body, and always uses merge_method=merge. Use it whenever the bypass conditions below are met and the agent has merge rights.
Fallback — bypass merge via gh api PUT (only when the in-band forceBypass path is unavailable AND session_pr_merge fails AND bypass conditions are met):
gh api -X PUT /repos/<owner>/<repo>/pulls/<N>/merge \
-f merge_method=merge \
-f commit_title="Merge pull request #<N> from <branch>" \
-f commit_message="<body>"
The merge_method=merge flag is required. Minsky preserves merge commits per docs/pr-workflow.md. The merge_method=squash value is hook-blocked — using it will fail at the pre-merge hook.
Bypass conditions (per feedback_self_authored_pr_merge_constraints): R ≥ 1 substantive review rounds have completed AND any one of: (a) reviewer-bot fired CoT-leakage errors twice consecutively on the same HEAD; OR (b) round-N self-reversal — round N's BLOCKING contradicts an earlier round's accepted fix; OR (c) reviewer-bot silent for >5 minutes after diagnosing the silence per §7a above. Verify the fired condition against its precise definition in the numbered step above before invoking either bypass path — a name-level match to "self-reversal," "CoT-leakage," or "silence" is not sufficient; the mt#2477 originating incident (PR #1688 add-then-revert churn) was reviewing genuinely different commit states across rounds, which fails the same-artifact-state check and is NOT self-reversal.
Audit trail requirement: The commit message must document the bypass:
"Bot self-approval bypass per feedback_self_authored_pr_merge_constraints — Chinese-wall review cleared, CI green."
This is not optional. Without an audit trail, the bypass is indistinguishable from a merge that skipped review.
References: feedback_self_authored_pr_merge_constraints, feedback_gh_api_bypass.
Regression example: 2026-04-28 session (9 PRs merged)
During the 2026-04-28 reviewer structural-output session, 9 PRs were merged in approximately 6 hours using the parallel reviewer+CI poll pattern combined with the gh api PUT bypass for bot-authored PRs. Representative merges:
- mt#1388, mt#1390 — merged within 5–7 minutes of "code complete" using parallel dispatch
- mt#1395 cluster — back-to-back merges benefiting from pre-warmed CI poll state
- mt#1404, mt#1413 — both used the
gh api PUT bypass after Chinese-wall reviewer posted COMMENT (not APPROVE)
Without the parallel pattern (sequential: wait for CI, then wait for reviewer), the same 9 PRs would have required approximately 12 hours at ~5–10 min overhead per PR plus reviewer subagent latency. The parallel pattern halved the wall-clock time.
This pattern is now canonical operating procedure for bot-authored PRs.
Worked example: bot review + agent watches + merge
The minsky-reviewer[bot] fires automatically on every push to a PR branch. The main agent's role is to watch for the review, assess the outcome, and drive to merge.
Typical flow:
- PR created by
minsky-ai[bot] via session_pr_create.
- Agent calls §3 (parallel watch):
session_pr_wait-for-review + session_pr_checks in the same message. Both block until the bot posts its review and CI completes.
- Bot posts APPROVE with a well-formed review body (spec verification, adoption sweep, smoke test, documentation impact).
- Merge gate passes: all required checks present, Smoke field valid, spec verification table present, documentation impact section present.
- Agent calls
session_pr_merge. The at-merge handler sets DONE atomically.
Variation: CHANGES_REQUESTED flow:
- Bot posts CHANGES_REQUESTED with BLOCKING findings.
- Agent fixes findings, commits, pushes.
- Agent calls §3 again with
since: "<previous review.submittedAt>" to wait for the new review.
- Bot posts APPROVE on the updated commit.
- Agent calls
session_pr_merge. DONE.
Variation: webhook miss:
- Bot doesn't post within 5 minutes after push.
- Agent runs §7a diagnosis: confirms push reached GitHub, checks CI fired, waits 5 min, tries empty commit to wake webhook.
- If still silent: bypass merge per §8 with audit trail.
Key principles
- The bot reviews; the agent coordinates.
minsky-reviewer[bot] is the canonical reviewer — do not attempt to perform your own code review. This skill covers orchestration around the bot's review, not content production.
- Parallel watch saves wall time. Always dispatch
session_pr_wait-for-review and session_pr_checks in the same tool-call message.
- Smoke is the agent's local gate. Run the smoke test yourself per §6.3; it is a local-agent action the bot cannot perform.
- Diagnose before bypassing. The webhook-miss diagnosis in §7a is a structured ladder — run it before reaching for the bypass merge.
- Read before retriggering. §7b:
session_pr_get(reviews: true) is the mandatory diagnostic step before reviewer_retrigger — a defective-looking verdict usually still has real diagnostic prose in the body; retriggering blind discards it.
- Bot-authored PRs require the bypass path. Self-approval is structurally blocked by GitHub; never attempt to APPROVE a PR from the same App identity that opened it.
- The docs/gate gap is structurally prevented. The merge gate enforces the structured review fields (spec verification, adoption sweep, smoke, documentation impact) via tool-call provenance inspection; the agent's role is to drive the cycle, not to re-implement the checks.