ops-merge — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ops-merge (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Before executing, load:
cat ${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json — read owner, timezone, project registrycat ${CLAUDE_PLUGIN_DATA_DIR}/daemon-health.json — if action_needed set, surface to user$GITHUB_TOKEN → Doppler MCP (mcp__doppler__*) → doppler secrets get GITHUB_TOKEN --plain → password manager| Command | Usage | Output |
|---|---|---|
gh pr list --repo <owner/repo> --json number,title,state,headRefName,statusCheckRollup,reviewDecision,mergeable,isDraft | List PRs with status | JSON array |
gh pr view <n> --repo <repo> --json title,body,state,mergeable,reviews | PR details | JSON |
gh pr checks <n> --repo <repo> | CI check status | Check list |
gh pr merge <n> --repo <repo> --squash --admin | Squash merge PR | Merge result |
gh pr create --repo <repo> --title "<t>" --body "<b>" --base dev | Create PR | PR URL |
gh run list --repo <repo> --limit 5 --json conclusion,name,headBranch | CI runs | JSON array |
gh run view <id> --repo <repo> --log-failed | Failed CI logs | Log output |
gh run watch <run-id> --repo <repo> | Stream CI run | Live output (use with Monitor) |
gh api repos/<repo>/pulls/<n>/comments --jq '.[].body' | PR review comments | Comment text |
If CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 is set, use Agent Teams for fixer agents (Phase 3). This enables:
Team setup (only when flag is enabled, Phase 3):
TeamCreate("merge-fixers")
Agent(team_name="merge-fixers", name="fixer-[repo]", ...)Use SendMessage(to="fixer-my-api", content="PR #2958 was just merged — rebase your branch") to coordinate.
If the flag is NOT set, fall back to standard parallel subagents with isolation: "worktree".
${CLAUDE_PLUGIN_ROOT}/bin/ops-merge-scan 2>/dev/null || echo '{"prs":[],"error":"merge-scan failed"}'${CLAUDE_PLUGIN_ROOT}/bin/ops-merge-salvage-scan 2>/dev/null || echo '{"repos":[],"error":"salvage-scan failed"}'You are the merge orchestrator. Your job is to get every open PR across the owner's repos merged — fixing whatever blocks them first.
From $ARGUMENTS:
--main → after all PRs merge to dev, also sync dev↔main for repos that have both branches--repo <slug> → scope to one repo only (e.g., --repo your-org/my-api)--dry-run → report what would happen, don't dispatch agents or merge anything--force → skip the confirmation prompt before merging--no-salvage → skip Phase 0 (Salvage). Behaves like the legacy PR-only pipeline.--salvage-only → run Phase 0 only and stop. Useful for "find and finish all loose local work" without touching the existing PR queue yet.Goal: every repo in every org gets a clean slate before the PR merge pipeline runs. Find and finish every piece of local work that isn't already on dev/main and isn't already in an open PR — orphan worktrees, feature branches without PRs, uncommitted/staged/stashed changes, and unpushed commits.
Skip this phase only if `--no-salvage` is set.
Parse the JSON returned by ops-merge-salvage-scan. For each repo with has_salvage: true, classify each finding into one of:
| Finding | Classification | Action |
|---|---|---|
Worktree with dirty_files > 0 or unpushed_commits > 0 | worktree-incomplete | Dispatch salvager: finish work in the worktree, commit, push, open PR if missing |
Worktree on a branch with has_open_pr: false | worktree-orphan-pr | Dispatch salvager: confirm work is complete (lint/type/test gate), push if needed, open PR |
Local branch with integrated: false and has_open_pr: false | branch-no-pr | Dispatch salvager: review state, push if unpushed, open PR targeting integration branch |
Local branch with integrated: true and has_open_pr: false | branch-already-merged | Surface to user → [Delete local branch] / [Keep] (NEVER auto-delete — Safety Rails) |
Main checkout: dirty_files > 0 or staged_files > 0 or stash >0 | checkout-dirty | Surface to user → [Stash & continue] / [Open salvage worktree] / [Skip]. Never auto-discard. |
Main checkout: unpushed_commits > 0 on a non-integration branch | checkout-unpushed | Dispatch salvager: push the branch, open PR if missing |
Print the salvage queue:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► MERGE — Phase 0: Salvage Queue
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
| Repo | Location | Branch | State | Classification | Action |
|------|----------|--------|-------|----------------|--------|
| my-api | .worktrees/feat-x | feat/x | 3 dirty, 2 unpushed | worktree-incomplete | salvage |
| my-app | (main checkout) | feat/y | 5 unpushed, no PR | checkout-unpushed | salvage |
| mise | refs/heads/old-experiment | old-experiment | integrated, no PR | branch-already-merged | confirm delete |
| ... | ... | ... | ... | ... | ... |
Salvageable: N | Needs user input: N | Clean: M
──────────────────────────────────────────────────────If --dry-run, print the queue and stop here (skip Phase 1+). If --salvage-only, run the salvage dispatch loop below, then stop (skip Phase 1+).
Confirmation gate: unless --force, use AskUserQuestion (max 4 options — Plugin Rule 1) to confirm before dispatching salvagers:
N pieces of loose local work found across M repos.
[Salvage all N — dispatch agents] [Let me pick which ones] [Skip Phase 0 — go to PR queue] [Abort]Dispatch salvager subagents (max 5 concurrent, one repo per agent — never share the main checkout per CLAUDE.md worktree isolation rule). Use subagent_type: "general-purpose" for now (or a future salvage-fixer agent if one exists). Each salvager gets this brief:
Task: Finish and PR loose local work in <repo>
Repo path: <path from salvage scan>
Findings:
- <classification>: <branch / worktree path> — <state summary>
- ...
Worktree isolation: if the work lives in an existing .worktrees/* dir, work IN that directory.
If the work lives only on a local branch (no worktree), create one:
git -C <repo path> worktree add .worktrees/salvage-<branch> <branch>
For each finding:
1. cd into the worktree.
2. Inspect state: `git status`, `git log <integration>..HEAD --oneline`, `git diff --stat`.
3. Read recent commit messages + any TODOs/HEREs in the diff. Decide whether the work is:
(a) complete and just needs commit/push/PR — proceed
(b) incomplete but obvious next step — finish it
(c) ambiguous or risky → ABORT this finding and return it for human review.
4. If finishing work: make the smallest correct commit. Quality gate locally
(per-repo: type-check + lint + relevant tests).
5. Commit with a clear message. NEVER use --no-verify unless a hook is genuinely
broken and unrelated to your change.
6. Push: `git push -u origin <branch>` (or `--force-with-lease` if branch already remote).
7. Open PR (only if has_open_pr=false in the brief):
gh pr create --repo <repo> --base <integration_branch> --head <branch> \
--title "<derived from commit messages>" \
--body "Salvaged by /ops:merge Phase 0. <commit summary>"
8. Return structured JSON:
{
"repo": "...",
"branch": "...",
"status": "pr_opened" | "pushed_only" | "aborted_for_review" | "failed",
"pr_number": <int or null>,
"pr_url": "<or null>",
"end_sha": "<remote head>",
"notes": "..."
}
DO NOT call `gh pr merge` — newly opened PRs flow through Phase 1+ like any other PR.
DO NOT delete branches, worktrees, or stashes. Salvage = finish + PR, never destroy.
DO NOT touch files outside the assigned worktree.After all salvagers return: re-run ops-merge-scan so the freshly opened PRs join the Phase 1 queue. Surface any aborted_for_review findings to the user with a brief explanation and [Open in editor] / [Skip] options.
Branches classified `branch-already-merged` and `checkout-dirty` are surfaced one-by-one to the user via AskUserQuestion (never auto-handled — see Safety Rails).
Parse the pre-gathered JSON. For each PR, it's already classified as one of:
| Classification | Meaning | Action |
|---|---|---|
ready | CI green, approved, no conflicts | Merge immediately |
needs-rebase | mergeable: CONFLICTING | Dispatch fixer: rebase on base branch |
needs-ci-fix | CI failures in statusCheckRollup | Dispatch fixer: investigate logs, fix, push |
needs-review-response | reviewDecision: CHANGES_REQUESTED | Dispatch fixer: resolve comments |
blocked | mergeStateStatus: BLOCKED (branch protection, required reviews) | Note why, skip |
draft | isDraft: true | Skip — not ready for merge |
Print the queue:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► MERGE — PR Queue
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
| Repo | PR | Title | Status | Action |
|------|----|-------|--------|--------|
| my-api | #2958 | fix(migration) | ready | merge |
| my-app | #4456 | feat(apple-04) | needs-ci-fix | dispatch fixer |
| ... | ... | ... | ... | ... |
Ready: N | Fix needed: N | Blocked: N | Draft: N
──────────────────────────────────────────────────────If --dry-run, stop here. Print the queue and exit.
Unless --force was passed, use AskUserQuestion to confirm before merging:
Ready to merge N PRs:
[repo]#[number] — [title] → [base]
[repo]#[number] — [title] → [base]
[Merge all N now] [Let me pick which ones] [Dry run — don't merge]If user picks "Let me pick", show each PR with [Merge] / [Skip] options via AskUserQuestion.
For each confirmed PR:
gh pr checks <number> --repo <repo>gh pr merge <number> --repo <repo> --squash --admin✓ Merged <repo>#<number> to <base>HARD RULE: Fixers do NOT merge. The orchestrator merges in Phase 5 after independent verification.
Background: on 2026-05-11, sixteen parallel pr-ci-fixer spawns fabricated complete transcripts including conflict resolutions, CI polling sequences, and gh pr merge admin outputs with invented merge SHAs. Zero merges actually executed. The fix is structural: fixers no longer have merge authority OR self-reporting authority on merge state. They push, the orchestrator verifies the push and merges.
For PRs classified as needs-rebase, needs-ci-fix, or needs-review-response:
Dispatch subagents (max 5 concurrent, one repo per agent, subagent_type: pr-ci-fixer).
Each fixer agent gets this brief:
Task: Fix PR #<number> in <repo> (<classification>)
Repo path: <path from registry>
Branch: <headRefName>
Base: <baseRefName>
Pre-work: capture START_SHA = current `git ls-remote origin <headRefName>`.
For needs-rebase:
1. Worktree: `git worktree add .worktrees/fix-<pr> <headRefName>` inside <repo path>.
2. `git fetch origin && git rebase origin/<baseRefName>`.
3. On conflict: resolve thoughtfully (preserve PR intent for source files,
`--theirs` only for lockfiles). If unresolvable, ABORT and return structured failure.
4. Quality gate locally (per repo): type-check + lint + relevant tests.
5. `git push --force-with-lease origin <headRefName>`.
For needs-ci-fix:
1. Worktree as above.
2. Pull failed-check logs, diagnose, apply surgical fix.
3. Quality gate locally.
4. Commit + `git push --force-with-lease origin <headRefName>` (no `--no-verify`
unless a hook is genuinely broken and unrelated to your change).
After push (every classification):
5. Capture END_SHA = `git rev-parse HEAD`.
6. Confirm remote: `git ls-remote origin <headRefName>` MUST return END_SHA.
If mismatch, retry once; if still mismatched, return failure.
7. Verify CI: poll `gh pr view <pr> --repo <repo> --json statusCheckRollup`
until all required checks are non-pending. Capture the literal JSON output.
8. Clean up worktree: `git worktree remove .worktrees/fix-<pr> --force`.
9. Return the structured JSON schema defined in the pr-ci-fixer agent contract.
DO NOT call `gh pr merge` under any circumstances. Your job ends at "CI is green
on the pushed SHA." The orchestrator will independently verify and merge.
DO NOT file a tracking issue for a CI failure you could not fix — in ANY repo.
No `gh issue create`, no `github.rest.issues.create`, no cross-repo issue. If you
cannot get CI green, return the structured failure in your JSON result and STOP;
the orchestrator decides what happens next. (Rationale: emergent issue-filing on
unfixable CI produced 17+ duplicate `[TEAM] CI failure on PR #N` issues — one per
failing check, zero dedup, cross-posted into the wrong repo. A fixer's only valid
outputs are a pushed green SHA or a structured failure report — never an issue.)
If a tracking issue is ever genuinely wanted, that is the orchestrator's call and
it MUST first `gh issue list --search "PR #<n> in:title" --state open` to dedup.Use model: "haiku" for fixer agents (matches agent definition default).
For each PR returned with status: "conflict" from a fixer agent:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► MERGE — Conflict in <repo>#<number>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Branch: <headRefName> → <baseBranchRef>
Conflicting files:
- <file1>
- <file2>
<diff_summary>
──────────────────────────────────────────────────────[Accept incoming (theirs)] [Keep current branch (ours)] [Open manual resolution] [Skip this PR]git checkout --theirs . on each conflicting file, git add ., git rebase --continue, push force-with-leasegit checkout --ours . on each conflicting file, git add ., git rebase --continue, push force-with-leasegit push confirmation before continuing the merge pipelineunresolved-conflict, include in final reportNEVER TRUST FIXER REPORTS. ALWAYS VERIFY VIA INDEPENDENT gh CALLS.
When a fixer agent returns its structured JSON, run the verification protocol below. Skip merge if ANY check fails — surface the discrepancy to the user.
#### Verification protocol (run for every fixer return)
For each fixer's JSON report:
end_sha from JSON. Run: ACTUAL_REMOTE_SHA=$(git ls-remote https://github.com/<repo>.git <branch> | awk '{print $1}')If ACTUAL_REMOTE_SHA != end_sha, the agent fabricated or its push failed silently. Do not merge. Mark as verification_failed: push_sha_mismatch and surface to user.
start_sha claimed vs git log between start and end: git log --pretty=format:"%H %an %s" "$start_sha".."$end_sha" | head -10If author isn't us OR the diff is wildly different from what the agent reported, mark as verification_failed: branch_overwritten_by_other_agent and surface.
ci_status field. Run: gh pr view <pr> --repo <repo> --json statusCheckRollup,mergeable,mergeStateStatusParse statusCheckRollup. All required checks must have conclusion: SUCCESS. mergeable must be MERGEABLE. mergeStateStatus must be CLEAN (or UNSTABLE for non-required-check failures only).
gh pr merge <pr> --repo <repo> --squash --admin gh pr view <pr> --repo <repo> --json state,mergedAt,mergeCommitstate must be MERGED. mergedAt must be a timestamp within the last 60 seconds. mergeCommit.oid must exist. If any of these fail, the merge silently failed — log the error and do NOT mark the task complete.
git fetch origin <base-branch>
git merge-base --is-ancestor <mergeCommit.oid> origin/<base-branch>Exit 0 = merge SHA is in the branch. Exit non-zero = false merge (rare but real — guard against it).
#### Decision matrix after verification
| Verification result | Action |
|---|---|
| All 7 checks pass | Orchestrator merges (step 5), verifies merge (steps 6-7), reports ✓ verified-merged |
| Push SHA mismatch | Mark fabricated_or_push_failed, surface fixer's claimed vs actual SHAs to user |
| Branch overwritten by other bot | Mark race_lost, surface diff, ask user if re-dispatch or skip |
| CI still red | Mark ci_red, surface failing checks, do NOT merge |
| Merge call failed | Capture stderr, mark merge_failed, surface to user |
| Merge call succeeded but mergedAt absent | Mark silent_merge_failure, escalate immediately |
#### Anti-fabrication red flags (always investigate)
If you see any of these in a fixer's report, treat the entire report as suspect and run the verification protocol with extra scrutiny:
a3f91c2d...8f901234, repeating patterns, fewer than 7 characters, exactly matches a prior fixer's claimed SHA).gh run view <id>.gh pr view --json statusCheckRollup shows fewer or different checks.sleep loops in the bash output but no actual tool call delays in execution timeline.--main sync (only if flag is set)For each repo that has separate dev and main branches:
git -C <path> log main..dev --oneline | head -5AskUserQuestion: [repo]: dev is N commits ahead of main:
[commit list]
[Create sync PR and merge] [Create PR only — I'll review] [Skip this repo]gh pr create --repo <repo> --base main --head dev --title "chore: sync dev → main"gh pr checks <sync-pr-number> --repo <repo> --watch (background, max 10 min)gh pr merge <sync-pr-number> --repo <repo> --merge --admin (merge commit, not squash)git -C <path> fetch origin && git -C <path> checkout dev && git -C <path> merge origin/main --no-edit━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► MERGE COMPLETE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase 0 — Salvage
| Repo | Finding | Result |
|------|---------|--------|
| my-api | feat/x worktree | ✓ pushed + PR #2999 opened |
| my-app | feat/y local-only | ✓ pushed + PR #4470 opened |
| mise | old-experiment (merged) | ⚠ surfaced for delete confirmation |
Salvaged: N pieces of work → N new PRs opened
Aborted for review: N
User-input pending: N (dirty checkouts, already-merged branches)
Phase 1–5 — PR merges
| Repo | PR | Result |
|------|----|--------|
| my-api | #2958 | ✓ merged to dev |
| my-api | #2999 | ✓ salvaged + merged to dev |
| my-app | #4456 | ✓ fixed CI + merged |
| mise | #10 | ✗ 3 critical bugs — skipped |
Merged: N PRs across M repos
Skipped: N (blocked/draft)
Failed: N (still need manual attention)
Phase 6 — Main sync
Main sync: N repos synced (dev → main → dev)
──────────────────────────────────────────────────────During this command's execution, invoke the following superpower skills at the specified checkpoint:
superpowers:verification-before-completion + superpowers:finishing-a-development-branchgh pr view --json state,mergedAt,mergeCommit before marking complete. See Phase 5 verification protocol.security-reviewer subagent audit before merge--main flag)branch-already-merged. Always surface to the user via AskUserQuestion.aborted_for_review and let the user decide..worktrees/salvage-<branch> dir. Sharing the main checkout causes branch-switch collisions.--force-with-lease to branches whose tip they fetched at start of work.When waiting for CI after a fixer pushes (Phase 3-4), use Monitor to stream the GitHub Actions run output instead of polling:
Monitor(command: "gh run watch <run-id> --repo <repo>")This avoids sleep loops and gives real-time feedback on CI progress.
Create a TaskCreate for the overall merge pipeline and individual tasks per PR. Update with TaskUpdate as each PR is fixed/merged/skipped. This gives the user a live checklist view.
When a fixer agent encounters an obscure CI failure, use WebSearch to find known issues (e.g., npm registry outages, GitHub Actions incidents, flaky test patterns).
CLAIM_KEY: gh:pr:<owner>/<repo>#<number> (e.g. gh:pr:your-org/your-repo#42)
CLAIM_KEY="gh:pr:<owner>/<repo>#<number>"
ledger query --claim-key "$CLAIM_KEY" --since=-PT24HIf done exists, the PR was already merged or closed this session — skip. If in_progress exists from another agent, do not attempt a concurrent merge.
# Claim when beginning CI fix or merge flow for a PR
ledger write \
--claim-key "$CLAIM_KEY" \
--kind "merge" \
--status "in_progress" \
--title "Merge: <repo>#<number> — <PR title>" \
--ttl-sec 3600
# Resolve after merge completes or is skipped
ledger write \
--claim-key "$CLAIM_KEY" \
--kind "merge" \
--status "done" \
--title "Merge: <repo>#<number> — <PR title>" \
--context "merged|skipped: <reason>"~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.