Purpose
Coordinate cross-repository hygiene. This skill owns bounded fleet discovery, canonical
checkout resolution, fleet-scale evidence collection, rollup, and action-plan routing. It does
not own per-repository cleanup decisions or execution; those belong to repo-hygiene and
source-control.
The collector produces the detailed read-only report described below, including the compact
machine-readable rollup and action-plan artifact. Tell the user to run
/repo-fleet-hygiene:apply --plan-file <path> (dry-run by default; --apply plus confirmation or
--yes to mutate). Do not add an execute flag to this audit script.
Non-negotiable boundary
Never run or suggest running inline from this skill: git fetch, git worktree prune,
git worktree repair, git worktree remove, git branch -d/-D, git remote set-url, or any
filesystem deletion. The bundled script has no mutation mode. --apply-plan is a read-only
dry-run approval artifact over a prior plan file. A report may name a command/tool as a future
handoff; the fleet action plan lists those invocations once per repository behind one confirmation
gate. Actual fleet mutation belongs to /repo-fleet-hygiene:apply, not this skill.
Input resolution
Parse $ARGUMENTS as opaque arguments for the bundled script. Supported flags:
<dir>: a bare positional path, treated as --root. A drive root (D:, D:/) is a legitimate
discovery root and normalizes to D:/. This is the form /repo-fleet-hygiene:audit D: uses.
--root <dir>: bounded recursive repository discovery (repeatable).
--repo <dir>: exact repository/worktree target (repeatable).
--config <file>: explicit Git-format config (at most one).
--canonical <github.com/owner/repo=path>: invocation-specific canonical checkout override
(repeatable; explicit wins over config).
--skip <name>: discovery directory-name skip (repeatable). Explicit --skip / fleet.skip
entries replace the default skip list rather than appending. Otherwise shrinking is
impossible. Default (neither CLI nor config): node_modules, vendor, .venv. To extend, pass
those three defaults plus your names; to shrink (e.g. reach a repo under vendor/), omit names
you want walked. CLI and config compose additively with each other like other scope inputs.
Values must be bare directory names (no empty value, no path separator). ., .., and .git
stay skipped unconditionally even when an explicit list omits them.
--max-depth <1..12>: discovery bound; explicit wins over config/default 5.
--project-dir <dir>: the session's project directory, used for the project-scoped config rung.
It is not a scope fallback. A run with no scope fails rather than auditing it.
--detail: emit collapsed per-target evidence after the rollup (default is rollup + action plan
only).
--plan-file <path>: write the machine-readable action-plan JSON to this path (otherwise a temp
file is created and named in the report).
--apply-plan <path>: standalone read-only mode. Render the ordered dry-run approval artifact
for a previously written plan (cannot combine with discovery flags).
Always pass --project-dir "${CLAUDE_PROJECT_DIR}" on audit runs (not on --apply-plan). That
variable is substituted in this markdown content and in allowed-tools Bash rules, but it is
not present in the Bash tool's environment, so the script cannot read it for itself. Passing
it in is what makes the project rung below reachable at all.
If no scope resolves, no bare path, no --root, no --repo, and no config-supplied
fleet.root/fleet.repo, the run stops and names the ways to supply scope plus
/repo-fleet-hygiene:setup apply. Pass that guidance through rather than re-deriving a root
yourself. The project directory is not a fallback scope: auditing the session's incidental
working directory would silently audit whatever tree the shell happens to sit in. Config
resolution is the script's own ladder. Do not pre-resolve or pass a probed path yourself:
explicit --config wins, else the script probes
<project-dir>/.claude/repo-fleet-hygiene.conf (project-scoped), else
~/.claude/repo-fleet-hygiene.conf (user-global, a machine-scoped fleet config placed there is
recorded user intent, not a guessed root). The report header names the consumed config and its
source, or states that none was consumed. Never guess a broader machine root from the current
path beyond that ladder.
Config-supplied scope is additive to CLI-supplied scope: a --repo X run still walks every
configured root. The header's Scope: line names each contributing rung and its entry count, so
report that line rather than assuming the arguments were the whole scope.
Before execution, reject any arguments outside this grammar, noting that a bare positional path
is in the grammar, so /repo-fleet-hygiene:audit D: and
/repo-fleet-hygiene:audit /path/to/tree are valid invocations to pass through, not arguments to
refuse. What stays rejected is an unrecognized flag: anything beginning with - that is not listed
above. Pass every path/override as a quoted argument; never assemble a shell fragment from config,
repository, remote, or branch text.
Run exactly once:
${CLAUDE_SKILL_DIR}/scripts/audit-fleet.sh --project-dir "${CLAUDE_PROJECT_DIR}" <validated-and-quoted-arguments>
The script validates config with git config --file; it never sources or executes it.
Evidence rules
The bundled collector is authoritative for classifications. Preserve its evidence in the report:
- Canonical checkout: explicit remote-keyed override → configured remote-keyed override → the
repository's main worktree, read as the first record of
git worktree list --porcelain
(which lists the main worktree first regardless of where it runs). git rev-parse --show-toplevel alone cannot identify a canonical checkout: inside a linked worktree it returns
the linked root, so a sibling worktree reached first by discovery would otherwise become the
path every handoff points at. When a supplied or discovered path resolves to a different main
worktree, the header states the substitution on one Resolved to main worktree: line per
repository, naming every path that resolved into it. Relay it, because the operator named one
path and the report is about another. The report always shows
discovered and canonical paths. An
override target with a missing/non-GitHub remote, or a different identity that cannot be proven to
resolve to the same GitHub repository, stops that repository's local audit before evidence combines.
- GitHub identity: read the selected fetch remote with
git remote get-url; accept only
github.com/owner/repo; query GET /repos/{owner}/{repo}. If returned full_name differs, report
HIGH transfer/rename evidence and continue branch/worktree analysis against that resolved
identity. A moved remote is not a reason to skip local classification or merge evidence. A
404/403/network error is UNKNOWN, never "deleted" or "moved". A 404/403 on an identity listed
in fleet.ackUnavailable is demoted to ACKNOWLEDGED, still reported, never suppressed; acks
never touch non-404/403 failures or successful-response evidence.
- Merged branch: one aliased
gh api graphql query per repository page of local branches
(up to MERGED_PR_GRAPHQL_ALIAS_PAGE headRefName aliases per call, first:1,
states:[MERGED]). GraphQL's headRefName argument is an exact match, never the search
API's prefix-matching head: qualifier, so feature/auth and feature/auth-v2 never conflate.
Measured rate cost stays 1 per call (nodeCount equals the alias count); that stays well under
GitHub's documented 500,000-node ceiling and 5,000-point/hour primary limit. There is no merged-PR
result window and no per-branch fallback: every non-default local branch the operator asked
about is queried by exact name, including heads GitHub auto-deleted and a later fetch pruned. Fail closed when gh/GraphQL is
unavailable. Emit github-pr-evidence-unavailable and never infer unmerged from a missing
row after a failed page. Identical branch names in another repository are unrelated. HIGH
requires the PR headRefOid to equal the current local tip. Tip drift is MEDIUM manual
review. Git ancestry without GitHub evidence is LOW and never called merged-by-PR, and
under squash merges that ancestry predicate is near-inert, so on a squash-merging fleet
GitHub evidence is effectively the only merge evidence.
- Merged remote branch: after local classification, the same merged-PR rows are matched
against each remote-tracking tip under the selected remote. When
headRefOid equals that tip
and the branch is not the default, probe live existence with
git ls-remote --heads <remote> refs/heads/<branch>. A matching tip → HIGH
merged-remote-branch (remote head still present after merge, unset or blocked
delete_branch_on_merge). ls-remote failure → MEDIUM cached observation (may be stale after a
prune-less fetch). Empty ls-remote → no finding (head already gone upstream). Remote-only heads
(local already deleted) are included. The handoff is an optional git push --delete --dry-run
preview naming the remote and branch; this skill never runs it and never calls org-admin APIs to
flip repository settings. Enabling delete_branch_on_merge is complementary (it stops the class
accruing) and is not a substitute for this fleet visibility.
- Local inventories: parse only
git worktree list --porcelain -z registrations and
NUL-delimited git for-each-ref branch/tip records. Directory naming is
never worktree evidence. Compare each existing registered path's actual --git-common-dir with
the canonical checkout's expected common dir. A mismatch is HIGH evidence of an administrative
linkage problem but manual review only. Missing/prunable registrations never trigger pruning.
Linked, unlocked registrations with reliable admin emit one MEDIUM worktree-status-handoff
per repository naming those paths. Disposability (stranded / unknown / safe) is owned by
/source-control:worktree status, and this collector emits no git status-based substitute
verdict. Separately, every linked worktree that passes existence and root-verifiability checks
is classified against the configured worktree root (worktreeroot.path when present on the
first resolvable TARGET, else source-control worktree_root): conforming, outside/wrong-layout
(expected <root>/<owner>-<repo>-<slug> or <root>/<repo>-<slug> without origin, matching
/source-control:worktree create; create-shaped basenames stay conforming after branch
rename/detach; comparisons use physical paths so symlink aliases of the configured root do not
false-positive), or tool-owned (Codex/Cursor).
Missing, prunable, non-root, and root-unverifiable registrations keep their own finding kinds and
are excluded from conformance denominators. When no root is configured, placement is reported
without asserting a convention. The collector uses a single fleet-wide root (first TARGET with
worktreeroot.path, else pluginConfigs); intentionally different per-repository includeIf
roots are not modeled. If pluginConfigs cannot be read because jq is missing, emit
worktree-root-pluginconfigs-unreadable rather than pretending the key is unset.
Per-repository and fleet rollups always state the classifiable counts. If either inventory
command fails or emits malformed/partial output, discard it, emit UNKNOWN, stop local
branch/worktree classification, and do not count that repository as successfully audited; an
empty/failed inventory never means no branches are attached.
- Protection: current/default/worktree-attached branches are never emitted as standalone branch
cleanup candidates. A merged worktree is routed to worktree dry-run first.
merged-remote-branch
is independent of local attachment. It describes the remote ref.
Every emitted finding kind, both confidence axes, and the merge-strategy and
gc.worktreePruneExpire dependencies the tiers rest on:
reference/confidence-model.md. The official Git/GitHub behaviours
this collector relies on: reference/official-sources.md. The
read-only enforcement model and its threat assumptions:
reference/security-review.md.
Presentation
Default output is screen-scale:
- Fleet header (config, scope, discovery counts).
- Repository rollup. One row per repository with
CLEAN / N candidates /
BLOCKED (evidence gap), plus counts by finding kind. Fleet-level findings (stale config,
duplicate checkouts) get their own row. A fleet verdict summarizes blocked vs candidate vs clean.
- Fleet action plan. Recommended skill invocations once per repository (not once per
finding), ordered so branch cleanups precede worktree cleanups, behind one confirmation gate.
- Path to the machine-readable action-plan JSON (and the
--apply-plan dry-run invocation).
Pass --detail when the operator needs evidence: targets are collapsed (one entry per path/branch
carrying every applicable finding), never duplicated across confidence groups. Never collapse
same-named branches across repositories.
ACKNOWLEDGED is a prominence demotion, not a fifth confidence tier: the evidence stays exactly
as weak as the UNKNOWN it came from. A rollup CLEAN verdict means no actionable cleanup-plan
candidates (the kinds that produce skill invocations) and no UNKNOWN evidence gap for that
repository, not "GitHub was unreachable so nothing was wrong." Manual-review HIGH/MEDIUM findings
(for example locked-worktree or merged-pr-tip-drift) remain in kind counts but do not inflate
N candidates when the action plan correctly lists Actions: none.
When the user wants to act on a fleet report, tell them to run
/repo-fleet-hygiene:apply --plan-file <path> (dry-run, then --apply) rather than driving
per-repository skills by hand.
Fleet cleanup plan
This section defines how audit relates to the execute verb; it does not add a mutation command to
the audit argument grammar.
The cleanup-plan consumer is /repo-fleet-hygiene:apply. It takes only the machine-readable
rollup artifact. Never parse this skill's human report into executable operations. The apply
verb:
- rejects an incomplete, invalid, or non-audit artifact and preserves every repository-qualified
target, confidence, evidence gap, and disposition;
- owns batched merged-local-branch deletion (with fail-closed OID refresh) and worktree cleanup in
plan order rather than widening this audit script;
- presents one fleet action plan and obtains one explicit confirmation (or
--yes) before any
mutation; and
- re-derives mutable facts, including relevant branch/worktree OIDs, at execution time. An old
artifact is evidence, not authorization.
The rollup, the --apply-plan dry-run, and /repo-fleet-hygiene:apply all ship. Do not
invent --cleanup-plan, --execute, or report-and-execute behavior on audit-fleet.sh. Return the
report, rollup, and plan path; tell the user to run /repo-fleet-hygiene:apply for execution. A
HIGH evidence tier is never itself permission to delete a branch or worktree.
Related fleet contracts that remain separate:
- merged remote branches carry a distinct safety gate: this skill reports them, and remote
deletion is not part of
/repo-fleet-hygiene:apply.
Graceful degradation
- Git missing or too old: stop before scanning and give the prerequisite error.
- Invalid config SYNTAX, invalid override, or an invalid CLI-supplied
--repo/--root path: report
the exact invalid input and stop; never silently fall back.
- No scope given and the project directory is not a Git working tree: stop, and relay the script's
remedy block verbatim. The operator did not choose that path, so the rejection alone is not
actionable.
- A config-sourced
fleet.repo/fleet.root path that is missing or not a Git working tree degrades
per-entry, not per-run: the entry becomes an UNKNOWN stale-config-entry finding and the rest of
the fleet is still audited (deleting repositories right after an audit must not abort every
subsequent run until the config is edited).
- A path discovered under
--root that is unreadable or not a Git working tree (despite a .git
marker) degrades the same way: an UNKNOWN discovery-skip finding, header skip counts, and the
rest of the fleet is still audited. An explicitly named --repo that is not a working tree still
hard-fails.
- A directory that itself carries a
.git marker (directory or file) is treated as a nested
repository: discovery add_targets it and returns without descending into its children. A
repository buried inside another repository's working tree therefore never appears as its own
audit target unless named explicitly via --repo / fleet.repo.
- A symlinked or junctioned intermediate directory under
--root is not followed, but is disclosed as an UNKNOWN discovery-symlink-skip finding and counted on
the discovery-skips header line. Windows directory junctions test as symlinks under Git Bash, so
they take this path. Symlinked discovery roots remain a hard refusal (CLI) or stale-config-entry
(configured).
gh missing/unauthenticated or API/timeout failure: continue Git/worktree checks, report GitHub
evidence as UNKNOWN, and make no merged/migration claim. Compatible timeout/gtimeout is
preferred; otherwise use the collector's finite TERM-to-KILL Bash watchdog.
- Non-GitHub or ambiguous remote: continue local checks; GitHub identity/PR evidence is
UNKNOWN.
- Canonical override is missing a GitHub remote or does not resolve to the same normalized GitHub
identity as the discovered repo: surface
UNKNOWN, stop that repository, and do not merge evidence.
- Worktree porcelain fails: surface
UNKNOWN and stop local branch/worktree classification for that
repository so no branch can be mislabeled unattached.
- Branch enumeration fails or is malformed: discard every partial record, surface
UNKNOWN, stop
branch classification, and exclude the repository from the successful-audit count.
Integration
| Finding |
Handoff (not executed here) |
merged-local-branch |
Run /repo-hygiene:clean git in the named canonical repository |
merged-remote-branch |
Optional preview only: git push --delete --dry-run <remote> <branch> in the canonical repository (never executed here). Enabling GitHub delete_branch_on_merge is complementary and owned by the repository's settings automation. This audit does not change it |
merged-worktree, prunable-worktree, missing-worktree |
Run /source-control:worktree cleanup --dry-run in the canonical repository |
worktree-status-handoff |
Run /source-control:worktree status in the canonical repository (stranded-work axis); use cleanup --dry-run only after Work is safe. If source-control is not installed, name the listed worktree targets and the missing collaborator. Emit no porcelain-based substitute verdict |
worktree-admin-mismatch |
Manual inspection; git worktree repair is an option only after validating which administrative directory is authoritative |
worktree-not-a-root |
Manual inspection of the registered path; a git -C probe of it describes the CONTAINING repository, so no cleanup handoff is safe until the path is resolved |
worktree-root-unverifiable |
Manual inspection of the registered path. Root-ness is unproven here rather than disproven. The probe itself failed, so infer nothing about the path in either direction |
worktree-nested-in-repository |
Recreate at an external root with /source-control:worktree create, then remove the nested one |
worktree-outside-configured-root |
Recreate at the expected location named in evidence with /source-control:worktree create, then remove the misplaced one |
worktree-wrong-layout |
Recreate at the expected <owner>-<repo>-<slug> path under the configured root, then remove the wrong-layout one |
worktree-tool-owned |
Leave to Codex/Cursor lifecycle, or migrate deliberately to the configured root |
worktree-root-conformance |
Read the per-worktree outside/wrong-layout findings for expected paths; migrate toward the configured root |
worktree-root-conformance-summary |
Same as per-repository conformance; fleet-scale migration toward the configured root |
worktree-root-unconfigured |
Set worktreeroot.path (git config) or source-control worktree_root, then rerun |
worktree-root-pluginconfigs-unreadable |
Install jq, or set worktreeroot.path; do not treat the fleet as unconfigured |
worktree-placement-unverifiable |
Inspect the canonical checkout; placement was not checked for any of its worktrees, so their placement is unknown rather than confirmed |
bare-repo-with-working-tree |
Manual review. core.bare=true coincides with working-tree content or registered linked worktrees, so the main worktree is disabled while linked worktrees keep working. Nothing is lost; the documented remedy is git config --local core.bare false in the named checkout |
github-remote-moved |
Human-reviewed git remote set-url; this plugin never changes remotes |
This plugin remains useful if those optional collaborators are absent: the report names the local
Git/GitHub evidence and target so another tool or human can act. For worktree-status-handoff, when
source-control is missing, name the listed worktree paths and the missing collaborator and do not
invent a porcelain-based disposability substitute.
Gotchas
- Which config is consumed depends on where the audit runs. Config resolution follows the ladder in
the Input resolution section: explicit
--config, else the project-scoped
.claude/repo-fleet-hygiene.conf, else the user-global one. A project-scoped config is invisible when
the audit runs from a different project, so confirm the consumed config named in the report header
before trusting a run's scope.
- Config paths resolve relative to the config file's directory. A relative
root/repo/canonical
path is anchored at the config directory, not the audit's working directory. Absolute paths work but
are what a consumer's write-time path guard flags, so author config via
/repo-fleet-hygiene:setup apply, which prefers the portable relative form.
1---2name: audit-143description: Coordinate Git/GitHub hygiene across a cross-repository fleet: discover canonical repositories, collect and roll up cross-repository evidence (including merged remote-tracking heads still on origin), and hand an action plan to repo-hygiene/source-control, which own per-repository cleanup. The current collector is read-only and emits detailed exact handoffs; it never deletes, prunes, repairs, fetches, checks out, or rewrites. Use when: 'audit repositories across a fleet', 'stale branches across repos', 'orphaned worktrees across repos', 'merged remote branches still on origin', 'moved or renamed GitHub repos'.4---56## Purpose78Coordinate cross-repository hygiene. This skill owns bounded fleet discovery, canonical9checkout resolution, fleet-scale evidence collection, rollup, and action-plan routing. It does10**not** own per-repository cleanup decisions or execution; those belong to `repo-hygiene` and11`source-control`.1213The collector produces the detailed read-only report described below, including the compact14machine-readable rollup and action-plan artifact. Tell the user to run15`/repo-fleet-hygiene:apply --plan-file <path>` (dry-run by default; `--apply` plus confirmation or16`--yes` to mutate). Do not add an execute flag to this audit script.1718## Non-negotiable boundary1920Never run or suggest running inline from this skill: `git fetch`, `git worktree prune`,21`git worktree repair`, `git worktree remove`, `git branch -d/-D`, `git remote set-url`, or any22filesystem deletion. The bundled script has no mutation mode. `--apply-plan` is a read-only23dry-run approval artifact over a prior plan file. A report may name a command/tool as a future24handoff; the fleet action plan lists those invocations once per repository behind one confirmation25gate. Actual fleet mutation belongs to `/repo-fleet-hygiene:apply`, not this skill.2627## Input resolution2829Parse `$ARGUMENTS` as opaque arguments for the bundled script. Supported flags:3031- `<dir>`: a bare positional path, treated as `--root`. A drive root (`D:`, `D:/`) is a legitimate32 discovery root and normalizes to `D:/`. This is the form `/repo-fleet-hygiene:audit D:` uses.33- `--root <dir>`: bounded recursive repository discovery (repeatable).34- `--repo <dir>`: exact repository/worktree target (repeatable).35- `--config <file>`: explicit Git-format config (at most one).36- `--canonical <github.com/owner/repo=path>`: invocation-specific canonical checkout override37 (repeatable; explicit wins over config).38- `--skip <name>`: discovery directory-name skip (repeatable). Explicit `--skip` / `fleet.skip`39 entries **replace** the default skip list rather than appending. Otherwise shrinking is40 impossible. Default (neither CLI nor config): `node_modules`, `vendor`, `.venv`. To extend, pass41 those three defaults plus your names; to shrink (e.g. reach a repo under `vendor/`), omit names42 you want walked. CLI and config compose additively with each other like other scope inputs.43 Values must be bare directory names (no empty value, no path separator). `.`, `..`, and `.git`44 stay skipped unconditionally even when an explicit list omits them.45- `--max-depth <1..12>`: discovery bound; explicit wins over config/default `5`.46- `--project-dir <dir>`: the session's project directory, used for the project-scoped config rung.47 It is **not** a scope fallback. A run with no scope fails rather than auditing it.48- `--detail`: emit collapsed per-target evidence after the rollup (default is rollup + action plan49 only).50- `--plan-file <path>`: write the machine-readable action-plan JSON to this path (otherwise a temp51 file is created and named in the report).52- `--apply-plan <path>`: standalone read-only mode. Render the ordered dry-run approval artifact53 for a previously written plan (cannot combine with discovery flags).5455Always pass `--project-dir "${CLAUDE_PROJECT_DIR}"` on audit runs (not on `--apply-plan`). That56variable is substituted in this markdown content and in `allowed-tools` Bash rules, but it is57**not** present in the Bash tool's environment, so the script cannot read it for itself. Passing58it in is what makes the project rung below reachable at all.5960If no scope resolves, no bare path, no `--root`, no `--repo`, and no config-supplied61`fleet.root`/`fleet.repo`, the run **stops** and names the ways to supply scope plus62`/repo-fleet-hygiene:setup apply`. Pass that guidance through rather than re-deriving a root63yourself. The project directory is **not** a fallback scope: auditing the session's incidental64working directory would silently audit whatever tree the shell happens to sit in. Config65resolution is the script's own ladder. Do not pre-resolve or pass a probed path yourself:66explicit `--config` wins, else the script probes67`<project-dir>/.claude/repo-fleet-hygiene.conf` (project-scoped), else68`~/.claude/repo-fleet-hygiene.conf` (user-global, a machine-scoped fleet config placed there is69recorded user intent, not a guessed root). The report header names the consumed config and its70source, or states that none was consumed. Never guess a broader machine root from the current71path beyond that ladder.7273Config-supplied scope is **additive** to CLI-supplied scope: a `--repo X` run still walks every74configured root. The header's `Scope:` line names each contributing rung and its entry count, so75report that line rather than assuming the arguments were the whole scope.7677Before execution, reject any arguments outside this grammar, noting that a bare positional path78**is** in the grammar, so `/repo-fleet-hygiene:audit D:` and79`/repo-fleet-hygiene:audit /path/to/tree` are valid invocations to pass through, not arguments to80refuse. What stays rejected is an unrecognized flag: anything beginning with `-` that is not listed81above. Pass every path/override as a quoted argument; never assemble a shell fragment from config,82repository, remote, or branch text.8384Run exactly once:8586```bash87${CLAUDE_SKILL_DIR}/scripts/audit-fleet.sh --project-dir "${CLAUDE_PROJECT_DIR}" <validated-and-quoted-arguments>88```8990The script validates config with `git config --file`; it never sources or executes it.9192## Evidence rules9394The bundled collector is authoritative for classifications. Preserve its evidence in the report:95961. **Canonical checkout:** explicit remote-keyed override → configured remote-keyed override → the97 repository's **main worktree**, read as the first record of `git worktree list --porcelain`98 (which lists the main worktree first regardless of where it runs). `git rev-parse99 --show-toplevel` alone cannot identify a canonical checkout: inside a linked worktree it returns100 the linked root, so a sibling worktree reached first by discovery would otherwise become the101 path every handoff points at. When a supplied or discovered path resolves to a different main102 worktree, the header states the substitution on one `Resolved to main worktree:` line per103 repository, naming every path that resolved into it. Relay it, because the operator named one104 path and the report is about another. The report always shows105 discovered and canonical paths. An106 override target with a missing/non-GitHub remote, or a different identity that cannot be proven to107 resolve to the same GitHub repository, stops that repository's local audit before evidence combines.1082. **GitHub identity:** read the selected fetch remote with `git remote get-url`; accept only109 `github.com/owner/repo`; query `GET /repos/{owner}/{repo}`. If returned `full_name` differs, report110 `HIGH` transfer/rename evidence and **continue** branch/worktree analysis against that resolved111 identity. A moved remote is not a reason to skip local classification or merge evidence. A112 404/403/network error is `UNKNOWN`, never "deleted" or "moved". A 404/403 on an identity listed113 in `fleet.ackUnavailable` is demoted to `ACKNOWLEDGED`, still reported, never suppressed; acks114 never touch non-404/403 failures or successful-response evidence.1153. **Merged branch:** one aliased `gh api graphql` query per repository page of local branches116 (up to `MERGED_PR_GRAPHQL_ALIAS_PAGE` `headRefName` aliases per call, `first:1`,117 `states:[MERGED]`). GraphQL's `headRefName` argument is an **exact** match, never the search118 API's prefix-matching `head:` qualifier, so `feature/auth` and `feature/auth-v2` never conflate.119 Measured rate cost stays 1 per call (nodeCount equals the alias count); that stays well under120 GitHub's documented 500,000-node ceiling and 5,000-point/hour primary limit. There is no merged-PR121 result window and no per-branch fallback: every non-default local branch the operator asked122 about is queried by exact name, including heads GitHub auto-deleted and a later fetch pruned. Fail closed when `gh`/GraphQL is123 unavailable. Emit `github-pr-evidence-unavailable` and never infer unmerged from a missing124 row after a failed page. Identical branch names in another repository are unrelated. `HIGH`125 requires the PR `headRefOid` to equal the current local tip. Tip drift is `MEDIUM` manual126 review. Git ancestry without GitHub evidence is `LOW` and never called merged-by-PR, and127 under squash merges that ancestry predicate is near-inert, so on a squash-merging fleet128 GitHub evidence is effectively the only merge evidence.1294. **Merged remote branch:** after local classification, the same merged-PR rows are matched130 against each remote-tracking tip under the selected remote. When `headRefOid` equals that tip131 and the branch is not the default, probe live existence with132 `git ls-remote --heads <remote> refs/heads/<branch>`. A matching tip → `HIGH`133 `merged-remote-branch` (remote head still present after merge, unset or blocked134 `delete_branch_on_merge`). ls-remote failure → `MEDIUM` cached observation (may be stale after a135 prune-less fetch). Empty ls-remote → no finding (head already gone upstream). Remote-only heads136 (local already deleted) are included. The handoff is an optional `git push --delete --dry-run`137 preview naming the remote and branch; this skill never runs it and never calls org-admin APIs to138 flip repository settings. Enabling `delete_branch_on_merge` is complementary (it stops the class139 accruing) and is **not** a substitute for this fleet visibility.1405. **Local inventories:** parse only `git worktree list --porcelain -z` registrations and141 NUL-delimited `git for-each-ref` branch/tip records. Directory naming is142 never worktree evidence. Compare each existing registered path's actual `--git-common-dir` with143 the canonical checkout's expected common dir. A mismatch is `HIGH` evidence of an administrative144 linkage problem but **manual review only**. Missing/prunable registrations never trigger pruning.145 Linked, unlocked registrations with reliable admin emit one `MEDIUM` `worktree-status-handoff`146 per repository naming those paths. Disposability (stranded / unknown / safe) is owned by147 `/source-control:worktree status`, and this collector emits no `git status`-based substitute148 verdict. Separately, every linked worktree that passes existence and root-verifiability checks149 is classified against the configured worktree root (`worktreeroot.path` when present on the150 first resolvable TARGET, else source-control `worktree_root`): conforming, outside/wrong-layout151 (expected `<root>/<owner>-<repo>-<slug>` or `<root>/<repo>-<slug>` without origin, matching152 `/source-control:worktree create`; create-shaped basenames stay conforming after branch153 rename/detach; comparisons use physical paths so symlink aliases of the configured root do not154 false-positive), or tool-owned (Codex/Cursor).155 Missing, prunable, non-root, and root-unverifiable registrations keep their own finding kinds and156 are excluded from conformance denominators. When no root is configured, placement is reported157 without asserting a convention. The collector uses a single fleet-wide root (first TARGET with158 `worktreeroot.path`, else pluginConfigs); intentionally different per-repository `includeIf`159 roots are not modeled. If pluginConfigs cannot be read because `jq` is missing, emit160 `worktree-root-pluginconfigs-unreadable` rather than pretending the key is unset.161 Per-repository and fleet rollups always state the classifiable counts. If either inventory162 command fails or emits malformed/partial output, discard it, emit `UNKNOWN`, stop local163 branch/worktree classification, and do not count that repository as successfully audited; an164 empty/failed inventory never means no branches are attached.1656. **Protection:** current/default/worktree-attached branches are never emitted as standalone branch166 cleanup candidates. A merged worktree is routed to worktree dry-run first. `merged-remote-branch`167 is independent of local attachment. It describes the remote ref.168169Every emitted finding kind, both confidence axes, and the merge-strategy and170`gc.worktreePruneExpire` dependencies the tiers rest on:171[reference/confidence-model.md](reference/confidence-model.md). The official Git/GitHub behaviours172this collector relies on: [reference/official-sources.md](reference/official-sources.md). The173read-only enforcement model and its threat assumptions:174[reference/security-review.md](reference/security-review.md).175176## Presentation177178Default output is screen-scale:1791801. Fleet header (config, scope, discovery counts).1812. **Repository rollup**. One row per repository with `CLEAN` / `N candidates` /182 `BLOCKED (evidence gap)`, plus counts by finding kind. Fleet-level findings (stale config,183 duplicate checkouts) get their own row. A fleet verdict summarizes blocked vs candidate vs clean.1843. **Fleet action plan**. Recommended skill invocations **once per repository** (not once per185 finding), ordered so branch cleanups precede worktree cleanups, behind **one** confirmation gate.1864. Path to the machine-readable action-plan JSON (and the `--apply-plan` dry-run invocation).187188Pass `--detail` when the operator needs evidence: targets are collapsed (one entry per path/branch189carrying every applicable finding), never duplicated across confidence groups. Never collapse190same-named branches across repositories.191192`ACKNOWLEDGED` is a prominence demotion, not a fifth confidence tier: the evidence stays exactly193as weak as the `UNKNOWN` it came from. A rollup `CLEAN` verdict means no actionable cleanup-plan194candidates (the kinds that produce skill invocations) and no UNKNOWN evidence gap for that195repository, not "GitHub was unreachable so nothing was wrong." Manual-review HIGH/MEDIUM findings196(for example `locked-worktree` or `merged-pr-tip-drift`) remain in kind counts but do not inflate197`N candidates` when the action plan correctly lists `Actions: none`.198199When the user wants to act on a fleet report, tell them to run200`/repo-fleet-hygiene:apply --plan-file <path>` (dry-run, then `--apply`) rather than driving201per-repository skills by hand.202203## Fleet cleanup plan204205This section defines how audit relates to the execute verb; it does not add a mutation command to206the audit argument grammar.207208The cleanup-plan consumer is `/repo-fleet-hygiene:apply`. It takes only the machine-readable209rollup artifact. Never parse this skill's human report into executable operations. The apply210verb:2112121. rejects an incomplete, invalid, or non-audit artifact and preserves every repository-qualified213 target, confidence, evidence gap, and disposition;2142. owns batched merged-local-branch deletion (with fail-closed OID refresh) and worktree cleanup in215 plan order rather than widening this audit script;2163. presents one fleet action plan and obtains one explicit confirmation (or `--yes`) before any217 mutation; and2184. re-derives mutable facts, including relevant branch/worktree OIDs, at execution time. An old219 artifact is evidence, not authorization.220221The rollup, the `--apply-plan` dry-run, and `/repo-fleet-hygiene:apply` all ship. Do not222invent `--cleanup-plan`, `--execute`, or report-and-execute behavior on `audit-fleet.sh`. Return the223report, rollup, and plan path; tell the user to run `/repo-fleet-hygiene:apply` for execution. A224`HIGH` evidence tier is never itself permission to delete a branch or worktree.225226Related fleet contracts that remain separate:227228- merged remote branches carry a distinct safety gate: this skill reports them, and remote229 deletion is not part of `/repo-fleet-hygiene:apply`.230231## Graceful degradation232233- Git missing or too old: stop before scanning and give the prerequisite error.234- Invalid config SYNTAX, invalid override, or an invalid CLI-supplied `--repo`/`--root` path: report235 the exact invalid input and stop; never silently fall back.236- No scope given and the project directory is not a Git working tree: stop, and relay the script's237 remedy block verbatim. The operator did not choose that path, so the rejection alone is not238 actionable.239- A config-sourced `fleet.repo`/`fleet.root` path that is missing or not a Git working tree degrades240 per-entry, not per-run: the entry becomes an `UNKNOWN` `stale-config-entry` finding and the rest of241 the fleet is still audited (deleting repositories right after an audit must not abort every242 subsequent run until the config is edited).243- A path discovered under `--root` that is unreadable or not a Git working tree (despite a `.git`244 marker) degrades the same way: an `UNKNOWN` `discovery-skip` finding, header skip counts, and the245 rest of the fleet is still audited. An explicitly named `--repo` that is not a working tree still246 hard-fails.247- A directory that itself carries a `.git` marker (directory or file) is treated as a nested248 repository: discovery `add_target`s it and **returns without descending into its children**. A249 repository buried inside another repository's working tree therefore never appears as its own250 audit target unless named explicitly via `--repo` / `fleet.repo`.251- A symlinked or junctioned intermediate directory under `--root` is not followed, but is disclosed as an `UNKNOWN` `discovery-symlink-skip` finding and counted on252 the discovery-skips header line. Windows directory junctions test as symlinks under Git Bash, so253 they take this path. Symlinked discovery *roots* remain a hard refusal (CLI) or `stale-config-entry`254 (configured).255- `gh` missing/unauthenticated or API/timeout failure: continue Git/worktree checks, report GitHub256 evidence as `UNKNOWN`, and make no merged/migration claim. Compatible `timeout`/`gtimeout` is257 preferred; otherwise use the collector's finite TERM-to-KILL Bash watchdog.258- Non-GitHub or ambiguous remote: continue local checks; GitHub identity/PR evidence is `UNKNOWN`.259- Canonical override is missing a GitHub remote or does not resolve to the same normalized GitHub260 identity as the discovered repo: surface `UNKNOWN`, stop that repository, and do not merge evidence.261- Worktree porcelain fails: surface `UNKNOWN` and stop local branch/worktree classification for that262 repository so no branch can be mislabeled unattached.263- Branch enumeration fails or is malformed: discard every partial record, surface `UNKNOWN`, stop264 branch classification, and exclude the repository from the successful-audit count.265266## Integration267268| Finding | Handoff (not executed here) |269|---|---|270| `merged-local-branch` | Run `/repo-hygiene:clean git` in the named canonical repository |271| `merged-remote-branch` | Optional preview only: `git push --delete --dry-run <remote> <branch>` in the canonical repository (never executed here). Enabling GitHub `delete_branch_on_merge` is complementary and owned by the repository's settings automation. This audit does not change it |272| `merged-worktree`, `prunable-worktree`, `missing-worktree` | Run `/source-control:worktree cleanup --dry-run` in the canonical repository |273| `worktree-status-handoff` | Run `/source-control:worktree status` in the canonical repository (stranded-work axis); use cleanup `--dry-run` only after Work is safe. If `source-control` is not installed, name the listed worktree targets and the missing collaborator. Emit no porcelain-based substitute verdict |274| `worktree-admin-mismatch` | Manual inspection; `git worktree repair` is an option only after validating which administrative directory is authoritative |275| `worktree-not-a-root` | Manual inspection of the registered path; a `git -C` probe of it describes the CONTAINING repository, so no cleanup handoff is safe until the path is resolved |276| `worktree-root-unverifiable` | Manual inspection of the registered path. Root-ness is unproven here rather than disproven. The probe itself failed, so infer nothing about the path in either direction |277| `worktree-nested-in-repository` | Recreate at an external root with `/source-control:worktree create`, then remove the nested one |278| `worktree-outside-configured-root` | Recreate at the expected location named in evidence with `/source-control:worktree create`, then remove the misplaced one |279| `worktree-wrong-layout` | Recreate at the expected `<owner>-<repo>-<slug>` path under the configured root, then remove the wrong-layout one |280| `worktree-tool-owned` | Leave to Codex/Cursor lifecycle, or migrate deliberately to the configured root |281| `worktree-root-conformance` | Read the per-worktree outside/wrong-layout findings for expected paths; migrate toward the configured root |282| `worktree-root-conformance-summary` | Same as per-repository conformance; fleet-scale migration toward the configured root |283| `worktree-root-unconfigured` | Set `worktreeroot.path` (git config) or source-control `worktree_root`, then rerun |284| `worktree-root-pluginconfigs-unreadable` | Install `jq`, or set `worktreeroot.path`; do not treat the fleet as unconfigured |285| `worktree-placement-unverifiable` | Inspect the canonical checkout; placement was not checked for any of its worktrees, so their placement is unknown rather than confirmed |286| `bare-repo-with-working-tree` | Manual review. `core.bare=true` coincides with working-tree content or registered linked worktrees, so the main worktree is disabled while linked worktrees keep working. Nothing is lost; the documented remedy is `git config --local core.bare false` in the named checkout |287| `github-remote-moved` | Human-reviewed `git remote set-url`; this plugin never changes remotes |288289This plugin remains useful if those optional collaborators are absent: the report names the local290Git/GitHub evidence and target so another tool or human can act. For `worktree-status-handoff`, when291`source-control` is missing, name the listed worktree paths and the missing collaborator and do not292invent a porcelain-based disposability substitute.293294## Gotchas295296- **Which config is consumed depends on where the audit runs.** Config resolution follows the ladder in297 the Input resolution section: explicit `--config`, else the project-scoped298 `.claude/repo-fleet-hygiene.conf`, else the user-global one. A project-scoped config is invisible when299 the audit runs from a different project, so confirm the consumed config named in the report header300 before trusting a run's scope.301- **Config paths resolve relative to the config file's directory.** A relative `root`/`repo`/canonical302 path is anchored at the config directory, not the audit's working directory. Absolute paths work but303 are what a consumer's write-time path guard flags, so author config via304 `/repo-fleet-hygiene:setup apply`, which prefers the portable relative form.