--- name: muster description: Have the AI report actionable open-PR state across every repo the connected GitHub account owns — failing/pending checks, merge conflicts, unresolved review threads — grounded in live data, not one repo at a time. Use when the user says muster, asks for a portfolio status, wants to know what's actionable across all their repos, or asks "what needs my attention today across my repos". --- # Muster Workflow Use this skill when the user wants a status pass across their whole GitHub account, not just the current repo — every other status skill here (`runes`, `watchtower`, `reckoning`, `plunder`) is scoped to one repo. ## Contents - [At A Glance](#at-a-glance) - [What This Is Not](#what-this-is-not) - [Trigger Rules](#trigger-rules) - [Requirements](#requirements) - [Prerequisites](#prerequisites) - [Default Path](#default-path) - [Summary Format](#summary-format) - [Guardrails](#guardrails) - [Stop Conditions](#stop-conditions) - [Handoff](#handoff) - [Response Examples](#response-examples) ## At A Glance 1. Enumerate every repo the connected account personally owns (not org membership), excluding forks and archived repos. 2. For each repo's open pull requests, check failing/pending checks, merge conflicts, and unresolved review-thread counts — the same live checks `hooks/watchtower-nudge.sh` already runs for one branch's PR, fanned out across every open PR in every repo. 3. Report only what's actionable, the same "quiet unless there's something to say" instinct `watchtower` uses — not a wall of "repo X: all clear" lines. 4. Stop. Never touch a PR/issue — this skill only reads. ## What This Is Not - Not `watchtower`: `watchtower` is a one-line ambient nudge for the current repo's current branch; Muster is an invoked, multi-repo report covering every open PR across the whole account. - Not `reckoning`/`plunder`: both are single-repo historical reports (every PR ever, open/closed/merged). Muster looks at live, open-PR state only, across many repos — it does not walk closed or merged history anywhere. - Not permission to act on anything it finds — a failing check or an unresolved finding is information for the chief to route to `ship`, `roast`, or `land` on a specific repo, not something this skill acts on itself. ## Trigger Rules - Run when the user explicitly asks for a muster, a portfolio status, what's actionable across their repos, or "what needs attention today" across more than the current repo. - Do not run automatically; every other status-reporting skill here (`runes`, `reckoning`, `plunder`, `watchtower`) is invoke-on-purpose, and this one is no different — this is explicitly not a second ambient hook. ## Requirements - `gh` installed and authenticated. Repo enumeration needs `gh api --paginate user/repos`, since no MCP tool in this session exposes "every repo I own" as a direct list — `search_repositories` is a search index (query `user:`), not the authoritative access list, and can lag or miss private repos depending on token scope. Per-PR checks still prefer the GitHub MCP connector when available, falling back to `gh` the same as every other skill. - Without `gh` authenticated, stop and say the muster can't run rather than reporting a partial or single-repo result as the whole portfolio. - This flow assumes `gh` is authenticated as a user-account token (a personal access token or OAuth token), since `gh api user` and `gh api user/repos` are user-to-server endpoints. A GitHub App installation token can't call either at all — it authenticates as the installation, not a user, and both calls would fail outright rather than silently narrow. That failure is caught by the existing "stop if `gh` isn't authenticated" path; this skill doesn't implement a separate installation-token path (`GET /installation/repositories` under app-installation auth), so an installation token is out of scope here, not a case that produces a misleadingly narrow result. - A *fine-grained* personal access token, by contrast, does work with `/user` and `/user/repos`, but can be scoped to a chosen subset of repos rather than every repo the account owns — the response returns only what the token can see, with no field flagging that it's partial. This skill has no way to independently confirm a PAT's real scope, so it can't detect this case; note it as a caveat in the report (see Summary Format) rather than silently presenting a scoped token's count as the whole portfolio. ## Prerequisites - Confirm the connected identity via `get_me` when MCP is available. Repo enumeration always goes through `gh` regardless (no MCP tool exposes it directly — see below), so also run `gh api user --jq .login` and compare it against `get_me`'s login before enumerating. If they differ, the account whose repos get enumerated isn't the one the report would label as "Account" — stop and surface the mismatch rather than silently reporting one identity's login against another identity's repos. When MCP is unavailable, `gh api user --jq .login` is the only identity check and needs no cross-check against itself. - Enumerate every repo: `gh api --paginate --method GET user/repos -f affiliation=owner --jq '.[] | select(.fork==false and .archived==false) | {name, owner: .owner.login, full_name}'`. The explicit `--method GET` matters here, not just style: `gh api` switches its default method from GET to POST the moment any `-f`/`-F` field is present, and `POST /user/repos` creates a new repository rather than listing them — the opposite of this skill's read-only guarantee. Page through every page the same way `reckoning`'s own `gh api --paginate` fallback does — a single page under-covers an account whose repo count exceeds one page. This lookup is unverified in this repo until exercised live against a real account; confirm the shape of a real response before trusting it at scale, the same caution `plunder`'s new thread-attribution lookup got. - For each enumerated repo, list its open pull requests (`list_pull_requests` with `state: open`, or `gh api --paginate "repos/$owner/$repo/pulls?state=open"` when MCP is unavailable), paginating through every page of MCP results too, not only the `gh` fallback — a repo with more open PRs than one page holds still needs every one counted. Include each PR's `draft` field — it comes back in the same listing call, no separate fetch needed. A repo with zero open PRs needs no further check — move on without reporting it. - For each open PR, gather the same live facts `hooks/watchtower-nudge.sh` already checks for one PR: - Check/status rollup via `pull_request_read` method `get_check_runs`, paginating through every page. A run's `conclusion` (only set once `status` is `completed`) of `failure`, `cancelled`, or `timed_out` is a failing check; `action_required` and `stale` are actionable the same way — GitHub uses both for a run that needs attention before the PR can proceed, not a passive wait state. A `status` of `queued`, `in_progress`, `requested`, `waiting`, or `pending` is still pending. A `status` of `completed` with `conclusion` `success`, `neutral`, or `skipped` is a passing run — contributes nothing to "actionable" or "unverified," unlike every other combination above. Only a `status`/`conclusion` combination none of these three groups (failing, pending, passing) names — including a value GitHub adds later — is reported as unverified, never silently folded into "no detected issues"; an unrecognized state is exactly the kind of gap this skill's own fail-closed rule exists to catch. `get_status` (the older combined-status API) doesn't distinguish any of the above from a plain failure and can't see individual check-run states, so it isn't a real substitute: when `get_check_runs` itself isn't available, or its own pagination fails partway through, report the PR's checks as unverified rather than falling back to `get_status` or a partial page as if either gave equivalent information. - `get_check_runs` only sees GitHub Checks API entries (GitHub Actions and Checks-API-integrated apps). A CI system that posts through the older Statuses API instead — a commit status, not a check run — never shows up there at all, so also call `pull_request_read` method `get_status` on the head commit as an *additional* source, not a replacement. Check the status count first, not just `state` alone: verified live (`gh api repos/.../commits//status`) that GitHub's combined-status endpoint returns `state: "pending"` even when `total_count` is `0` — a repo with no legacy Statuses-API integration at all (the common case; this repo included) would otherwise get every PR flagged as having a pending check forever. Only read `state` once `total_count` is greater than zero: `failure`/`error` is a failing check, `pending` is a pending check, `success` is passing and contributes nothing to either bucket. A `total_count` of `0` also contributes nothing — there's no legacy status to report on. Any other `state` value with `total_count` greater than zero — a combination none of the above names — is reported as unverified, the same fail-closed catch-all `get_check_runs` and `mergeable_state` already use. This is additive to the `get_check_runs` classification above, which stays the primary, more granular source. - Mergeable state (`mergeable_state` / `mergeStateStatus`) — compare case-insensitively, since the REST field returns lowercase (`dirty`) and the GraphQL field returns uppercase (`DIRTY`), and this skill reads either depending on which tool answered. `dirty`/`DIRTY` flags a real conflict; `unknown`/`UNKNOWN` means GitHub hasn't finished computing mergeability yet and is reported as unverified, not assumed clean; `draft`/`DRAFT` (some mergeable-state responses report this directly, in addition to the PR's own `draft` field already captured above) excludes the PR from "no detected issues" the same way the `draft` field does; `unstable`/`UNSTABLE` stays on the checks/status path above rather than being treated as its own merge-conflict category; `blocked`/`BLOCKED` (branch protection requires a status check or review this PR hasn't met) is actionable on its own — GitHub's response doesn't say which requirement is unmet, so report it as blocked when a reason can be inferred from the checks/thread data already gathered, or unverified when it can't, but never let it fall through unclassified. `clean` and `has_hooks`/`HAS_HOOKS` (a legacy pre-receive-hooks status, informational only) both contribute nothing to either bucket. Any other value none of this list names — including one GitHub adds later — is reported as unverified, the same fail-closed catch-all the check-run classification above already uses. - Unresolved review-thread count via `pull_request_read` method `get_review_comments`, paginating through every page of threads (or the same paginated GraphQL `reviewThreads(first: 100, after: $cursor)` loop the hook script and `reckoning` both already use — nested per-thread comment pagination isn't needed, only `is_resolved`/`isResolved` on each thread's first page), counting threads where that field is false. This counts every unresolved thread on the PR, not only ones a `roast` review posted — the same convention `hooks/watchtower-nudge.sh` already uses (it has no per-thread provenance filter either). Report it as "unresolved review threads," not "roast findings," so an ordinary human review comment isn't mislabeled as one. Only report a PR's unresolved count as confirmed when this check actually completed — a failed or incomplete thread query means "unverified," never "assume zero." If the connected GitHub MCP server has a restricted or "lockdown" mode enabled that filters its own responses, this skill has no way to detect that from inside a query result — a filtered query can return successfully while still under-reporting threads. This is a caveat on the same "confirmed complete" claim, not a separate check to add; there's nothing this skill's own instructions can do to detect or work around a connector-level restriction it isn't told about. - A repo or PR whose check fails partway through (a listing page errors, a thread query can't complete) is reported as unverified for that one repo/PR, not silently dropped or assumed clean — the muster continues to the next repo rather than aborting the whole pass for one failure. ## Default Path 1. Confirm identity (see Prerequisites). 2. Enumerate every owned, non-fork, non-archived repo. 3. For each repo, list open PRs; skip repos with none. 4. For each open PR, gather checks/status, mergeable state, and unresolved review-thread count per Prerequisites. 5. Classify each PR: failing checks, pending checks, merge conflict, unresolved review threads, draft, no detected issues, or unverified (a check that couldn't complete). **Draft is its own classification, not a note folded into another one**: a draft PR reports as draft even when every other check comes back clean, and belongs in the Actionable list (see Summary Format) rather than silently landing in the clean-repo count, where a "report a draft PR's draft state" instruction with nowhere in the output to actually put it would otherwise go unfollowed. "No detected issues" reports only what this skill actually checked — it is not a land-readiness guarantee. This pass does check draft state (see Prerequisites); what it does *not* check, unlike `land`'s own preconditions, is required-review approval status — a PR still missing a required approval can otherwise show no detected issues under the checks this skill runs. 6. Report only repos/PRs with something actionable or unverified, per [Summary Format](#summary-format) — omit clean repos from the listed detail, but still count them in the totals. ## Summary Format Keep it to what's actionable — a clean repo gets counted, not narrated. - **Account** — the connected login, and how many repos were counted (owned, non-fork, non-archived) vs. how many had at least one open PR. - **Actionable** — one line per PR that needs attention: repo, PR number/URL, and why (failing check, pending check, merge conflict, blocked by branch protection, N unresolved review threads, draft) — grouped by repo. A draft PR lists here even when every other check is clean. - **Unverified** — any repo or PR whose check couldn't complete, reported separately from a confirmed-clean result. - **Clean** — a count only ("N repos, M open PRs, all clean") — no per-PR detail for PRs with nothing actionable. - **Coverage caveat** — a standing note that the repo count reflects what the authenticated token can see; a scoped fine-grained personal access token can silently narrow that below every repo the account actually owns, and this skill has no way to detect that case. A GitHub App installation token isn't a silent-narrowing case — it can't authenticate this flow at all (see Requirements) — so it doesn't belong in this caveat. ## Guardrails - Never touch a PR or issue, resolve a thread, or post a reply as part of this skill — it only reads. - Never walk closed or merged PR history — that's `reckoning`/`plunder`'s job, not this skill's; Muster stays scoped to open PRs only. - Never report a repo's or PR's check as "clean" when the underlying query didn't actually complete — report it as unverified instead of assuming the best case. - Never expand scope to org-owned repos the account doesn't personally own — `affiliation=owner` only, matching this skill's own stated scope. ## Stop Conditions - Stop and say so if `gh` isn't installed or authenticated — repo enumeration has no MCP path in this session, so there's no fallback left to try. - Stop and say so if repo enumeration itself fails or can't be paginated to completion — don't report a partial repo list as the whole portfolio. - A failure scoped to one repo or PR's checks (not enumeration itself) doesn't stop the whole pass — report that one item as unverified and continue, per Prerequisites. - Stop after presenting the summary; do not proceed into `ship`, `roast`, `land`, or any mutation on any repo without the user separately asking for it, naming that repo. ## Handoff - Unless the user asked for plain output, open with a fresh one-line phrase in the orc voice, speaking as the Warden from `context/personality.md` (the target repo's copy when it exists, otherwise `${CLAUDE_PLUGIN_ROOT}/context/personality.md`). If they did, open with the plain summary instead. - Present the summary per [Summary Format](#summary-format). - If blocked, drop the orc voice and state the blocker plainly. ## Response Examples Opening lines are samples of the orc voice; write a fresh one each time. Actionable state found (illustrative — actual output names real repos/PRs): ```text 🧭 Hrrm. Rode the whole realm: two strongholds need the chief's eye. ✨ Account: (N repos counted, M with at least one open PR) Actionable: - / — PR #12: failing check `test` - / — PR #7: 2 unresolved review threads - / — PR #4: draft Unverified: / — PR #3: review-thread check didn't complete Clean: 6 repos, 9 open PRs, all clean Coverage: reflects what the authenticated token can see ``` Whole portfolio clean: ```text 🧭 Hah! Every banner flies clean across the realm. Account: (N repos counted, M with at least one open PR, all clean) ``` Blocked, no gh access: ```text ⚠️ Muster is blocked: gh isn't installed or authenticated, and no MCP tool in this session lists every repo the account owns. No pass was performed. ```