Purpose
Inspect and configure the source-control plugin per the uniform setup contract
(docs/PLUGIN-PHILOSOPHY.md "Setup is explicit and repeatable" in the marketplace repository):
check reports the effective configuration, apply writes it. Two configuration surfaces:
- The commit-subject / PR-title convention config, layered across a user-global file, the tracked
team file, and a gitignored personal overlay and merged per key by
../../reference/config-resolution.md. Resolved first by
/source-control:commit and /source-control:pull-request before they fall back to inference or
the bundled Conventional Commits default. Conventional Commits is genuinely optional, some orgs
gate on ticket-prefixed subjects (WEB-123: description), so the plugin ships a sensible
default, not a hardcoded requirement.
- The
/source-control:babysit-prs native userConfig surface (not a tracked repo file).
Idempotent: re-running reads the existing configuration and offers updates rather than overwriting
blind. The plugin ships a working zero-config default (Conventional Commits / inference for the
convention; the safe babysit tier over your own PRs), so an unconfigured surface is INFO, never
FAIL.
Action routing: no argument or check runs the check; apply runs the check first, then
remediation. When apply carries a subject_pattern= argument it writes the convention
non-interactively; with no arguments in an interactive session it runs the convention interview
(spoke below). layer= selects which config layer apply writes, defaulting to the tracked team
file.
check (read-only)
Report a PASS/FAIL/INFO table across both surfaces; modify nothing.
Convention config
Anchor at the repo root: resolve REPO_ROOT once, ${CLAUDE_PROJECT_DIR} when set, otherwise
git rev-parse --show-toplevel, and use that literal resolved path for every repo-relative read
below, never a cwd-relative path (invoked from a nested directory, a cwd-relative path would inspect
the wrong file). Re-resolve REPO_ROOT at the top of every self-contained Bash call, a fresh shell
does not carry a prior call's variables.
Read all three layers, then report one effective-configuration table, a row per key, its
resolved value, and which layer supplied it, followed by a per-layer presence line. Never present a
single layer's value as the effective convention; a reader who cannot see which layer won cannot
tell why /source-control:commit behaves as it does.
key value won by
subject_pattern ^[A-Z]+-\d+: .+ team
pr_title_pattern Same as subject_pattern team
trailer_policy none local overlay
pr_body_attribution none local overlay
pr_body_required_sections Summary, Test plan plugin default
pr_body_required_sections is a list-valued key (like type_list, and unlike every scalar row
above it), render it comma-joined for this report regardless of how many lines the winning layer's
file spells it across. When every layer leaves it unset, the row still resolves, to the plugin's
portable default, Summary and Test plan, so won by reads plugin default rather than the row
going blank; this is the one key whose "no layer sets it" state is itself a reportable, named value,
not a bare absence. A winning layer declaring the literal keyword none renders the row's value as
none (no required sections) with that layer in won by, a resolved value distinct from the unset
row above, per config-resolution.md.
Per-layer verdicts:
- User-global (
~/.claude/source-control.md): present → report which keys it contributes;
absent → INFO. It is outside the repo, so no git check applies to it.
- Team (
REPO_ROOT/.claude/source-control.md): present → PASS. FAIL when excluded by
.gitignore. Teammates would never receive the shared convention; report the matching rule.
Absent → INFO, remediable by apply.
- Local overlay ignore rule (the recursive
.claude/**/*.local.*, covering
REPO_ROOT/.claude/source-control.local.md): probe the ignore rule whether or
not the overlay file exists. The rule's job is to be in place before the
first overlay is written; conditioning the probe on the file already existing
is the window that produces the exposure. Missing rule → FAIL, remediable by
apply (which writes the line at team-layer bind, not only at layer=local).
Probe with git check-ignore --no-index -v -- .claude/nested/overlay.local.md
(the path does not need to exist). The sentinel is nested so a leftover
.claude/*.local.* rule, which still matches the flat overlay path, is not
mistaken for the recursive rule. A match counts only when -v names a
repository .gitignore as the source. $GIT_DIR/info/exclude and
core.excludesFile are operator-local and do not protect a teammate.
- Local overlay file (
REPO_ROOT/.claude/source-control.local.md): when
present, PASS only when an ignore rule matches it and it is not in the
index. Two distinct failures hide behind one symptom and need different
remediations, so probe them separately. See the two-probe form under
apply. Absent file is OK once the ignore rule itself is present.
FAIL when the effective subject_pattern is not machine-checkable. It must be either the
literal keyword Conventional Commits or an anchored regex (^…-style); a plain-language
description cannot be evaluated by /source-control:commit or /source-control:pull-request. Name the layer that supplied the
offending value.
With all three layers absent: INFO, no declared convention; /source-control:commit and /source-control:pull-request infer
from the repo's own CLAUDE.md/rules/commit-msg hook, then fall back to the bundled Conventional
Commits default. The remediation is apply to persist a convention.
Neutral-SSOT drift probes. When a convention_source pointer is declared or a neutral file is
resolved (explicit pointer, or the well-known default docs/conventions/source-control/commit-convention.yml),
check surfaces two drift conditions the resolver otherwise handles silently. Round-trip the
enforcement resolver (lib/resolve-convention-pattern.sh <REPO_ROOT> subject_pattern) and read its
diagnostics:
- Broken pointer / neutral file → FAIL. A declared
convention_source whose target is missing,
or a resolved neutral file that fails the seam's safety/dialect/empty-key contract, disables
enforcement fail-closed. This is easy to miss because nothing signals it until a commit is
unexpectedly blocked or allowed, so surface it here, naming the resolver's diagnostic and the
remediation (restore the file, fix the pointer, or apply to rewrite it).
- Shadowed markdown → WARN. A neutral file resolves (via pointer or the well-known default) and
.claude/source-control.md still carries a markdown-H2 subject_pattern/pr_title_pattern for the
same key: the neutral value wins (rungs 1–2 over rung 3) and the stale markdown is inert but
misleading. Recommend apply to retire the duplicate (migration removes it), per
reference/apply-convention.md "Migration retires duplicates".
Retired conventions — when this plugin ships retirements.yaml: run
bash "${CLAUDE_PLUGIN_ROOT}/lib/check-retirements.sh" --manifest "${CLAUDE_PLUGIN_ROOT}/retirements.yaml".
Exit 0 → PASS. Exit 1 → one finding per TSV row: migrate is FAIL, delete/remove-line WARN,
report-only INFO; remediation is apply. Exit 2 → FAIL, never silent. Bash unavailable → report
the step UNKNOWN with remediation, never green.
Babysit config
Effective configuration. Report every babysit userConfig key with its resolved value or its
inference when unset. The authoritative render is the effective-configuration block that loads
with /source-control:babysit-prs (its help mode prints it without taking any other action); a
surviving literal ${user_config.…} placeholder there means the key is unset. For each unset key
state what will be inferred at run time. babysit_watched_owners → the current repo's owner,
babysit_self_logins → none (your gh api user --jq .login login is always used, extras only add
to it), babysit_default_tier → safe, babysit_merge_method → repo convention then squash, the
review-trigger keys → module dormant, babysit_worktree_root → the plugin data dir's
worktrees/ subdirectory. Unset keys are INFO (documented defaults), not FAIL.
Branch-protection posture across watched repos. For each watched owner (or the current repo's
owner when babysit_watched_owners is unset), enumerate the repos babysit would touch. Repos
with open PRs authored by the self logins, via
gh search prs --state open --author @me --owner <owner> --json repository, and for each, read
the default branch's effective rules (gh api repos/<owner>/<repo>/rules/branches/<default-branch>,
falling back to gh api repos/<owner>/<repo>/branches/<default-branch>/protection for classic
protection). Flag every repo reporting zero required reviews AND zero required status contexts as
unprotected: the merge gate refuses gate-proven merges there for non-self authors, and for a
self author whenever the base is not the default branch (--allow-unprotected is the deliberate
override), so an unprotected repo in an autopilot fleet deserves a protection rule, not an
override.
Windows long-path support for the worktree root. On Windows, worktrees under the (possibly
deep) worktree root can exceed 260 characters. Probe git config --get core.longpaths and the OS
policy (registry value LongPathsEnabled under
HKLM\SYSTEM\CurrentControlSet\Control\FileSystem); report each as enabled/disabled with the
remediation (git config --global core.longpaths true; the OS value needs an elevated change, so
report it, never attempt it). Skip this probe silently on non-Windows.
Lane-script reachability under the host permission layer. The babysit lane declares its own
bundled scripts, engine, gates, and guarded wrappers, invocable without a per-call permission
denial as a prerequisite, and for the paths that prove readiness it declares no degrade tier
(babysit-prs "Engine and degrade"; the contract, including why a denied mutation degrades
while a denied check cannot, is skills/babysit-prs/reference/safety.md "Lane-Script
Reachability"). Probe it here so the operator learns of a gap before a cycle stalls on it, in
two parts:
Canary (the load-bearing half). Run the lane's mandated invocation forms against
non-mutating targets, both of them, because they live under different path prefixes:
bash "${CLAUDE_PLUGIN_ROOT}/bin/source-control-babysit-merge" --help
bash "${CLAUDE_PLUGIN_ROOT}/scripts/babysit-readiness-gate.sh" --help
These are the exact spellings the lane uses for every merge and for every readiness
declaration, with --help so each prints usage and exits 0 without touching the network or
GitHub. Probe both: an allow rule or classifier decision covering the bin/ wrapper says
nothing about the scripts/ helper, so a canary that ran only the first would certify a
path the lane's readiness verdict never travels, and the readiness gate has no degrade tier
at all. A tool-call denial on either is a FAILED prerequisite, not an INFO note. The
reason is fail-closed posture, not logical certainty: the classifier decides per call, so a
denied --help does not prove the production shapes are denied any more than a permitted
one proves they are allowed. What it does establish is that the mandated spelling reaches the
classifier and can lose there, and the cheapest, most obviously harmless shape is the one
least likely to be denied while the heavier ones pass. A prerequisite check whose weakest
probe was refused must report FAILED rather than assume the untested shapes fare better.
Name which form was denied, say plainly that the production shapes were not probed, report
the denial verbatim with the remediation below, and never retry it or re-spell it as a raw
interpreter invocation to get past the denial, that form is exactly what the wrapper exists
to replace.
A pass is reachability, not a guarantee. Report it as such. A permitted --help proves
the mandated spelling exists and that a call to it got through; it does not prove the
production shapes (owner/repo#N --allowed-owners …, <N> --extra-self …) will be
permitted, because the classifier decides per call, at call time. That per-call property cuts
both ways: it leaves a pass provisional, and it is why the FAILED verdict above is a
fail-closed choice rather than a proof. The probes stay --help-only
deliberately: the merge wrapper's read-only production shape is a live GitHub call, so a
representative probe would make a check run start touching the fleet it was asked to
inspect, which the plugin's babysit-wrapper-help shell test exists to keep from
regressing. The
residual gap is covered rather than hidden: a denial that lands mid-cycle instead is
fail-honest by the mechanism this section rests on, the gate prints READINESS_UNPROVEN,
or nothing at all when the call never happened, and
skills/babysit-prs/reference/loop.md §5.5 requires the per-PR Gate verdict line to
quote that stdout verbatim, so an unproven readiness surfaces in the report instead of
being absorbed. The canary is the proactive convenience; the quoted verdict is the
enforcement.
Effective configuration (context for the canary). Run claude auto-mode config and report
whether its effective rules cover this plugin's bundled scripts. It prints the merged result
across the scopes the classifier reads autoMode from. User settings and managed settings,
so read that output rather than hunting for the underlying files; the managed scopes are not
locally readable as ordinary settings files. Forward the launch-time scope when there is
one. --settings is a global flag consumed at launch, not an input the subcommand accepts,
so a bare claude auto-mode config spawned from a session that was itself launched with
--settings <file> reports without that scope and under-states the effective rules. Probe
with claude --settings <file> auto-mode config in that case, and say which form was used;
when the scope came from an Agent SDK settings object with no file to re-supply, report the
probe as scope-incomplete rather than as the effective configuration. A missing or
narrow-looking block is INFO, never FAIL on its own: settings cannot prove reachability,
because a host safety classifier decides per call, at call time. Pair it with the
auto-mode configuration reference.
GraphQL reachability (a different wall, probed the same way). The two canaries above prove
the lane's own scripts reach the classifier; this one proves GitHub will answer them. Sandboxed
sessions (Claude Code on the web and remote execution) serve only a pinned set of GraphQL
operations and refuse the rest with HTTP 403, and gh pr view --json is implemented entirely
over GraphQL. Probe it read-only against the repository itself:
gh api graphql -f query='query{viewer{login}}' --jq '.data.viewer.login'
A login means GraphQL is served and the lane runs at full fidelity. A 403, or a message
saying the operation is not enabled for this session, is INFO rather than FAILED: the engine
re-sources the gh pr view bundle over REST by itself and keeps running. Report the one thing
the operator loses, because it is the thing that stops merges: review-thread resolution has
no REST equivalent, so the merge gate reports threadResolutionProven: false and holds every
PR as readiness UNPROVEN (skills/babysit-prs/SKILL.md "Engine and degrade"). Say so at
check time rather than letting a cycle discover it per PR. The 403 reads like an expired
token or a missing scope and is neither, so never remediate it by re-authenticating; the only
remedy is to run the merge half of the lane from a session that is served GraphQL.
The remediation is always the operator's to apply, never write settings from this skill.
apply (idempotent)
Run check first. Then write the convention (surface 1) and walk the sanctioned babysit
reconfigure paths (surface 2).
Convention config
The full write path is normative in
reference/apply-convention.md. Read it before writing any layer.
In brief:
Target layer. layer= picks user / team (default) / local; infer the layer from the
request's wording and state the pick before writing, the wrong layer either misses teammates or
commits a personal preference to shared history.
Non-interactive (subject_pattern=): an in-place update, never a fresh file. Carry every
independent key, recompute derived keys (type_list, pr_title_pattern), reject a
non-machine-checkable value, and for an overlay omit requested keys the layers below already
resolve identically.
Interactive: the interview. Anchor at REPO_ROOT, read all three layers first, infer before
asking (declared prose, commit-msg hooks, commit-history consensus over the configurable
setup_inference_* window), interview one decision at a time with a recommendation first, settle
the optional keys (trailer_policy, pr_body_attribution, pr_body_required_sections,
including the none value and the omission-never-resets trap), write the template, verify per
layer (team = tracked and staged; local = ignored and untracked, two independent probes; user =
no git command at all), and report the new effective merge, not just what was written.
Neutral SSOT: a team write may materialize a tool-agnostic flat-scalar YAML file other tools
consume too. It defaults to the well-known path docs/conventions/source-control/commit-convention.yml
(resolved with no pointer); ## convention_source is written only to relocate it. Recommended as
the default when a second enforcement consumer exists (commit-msg hook, CI title check), markdown-only
when this plugin is the sole consumer; migration retires markdown keys the neutral file takes over
(spoke section "Neutral convention SSOT").
Every step's exact contract, the interview steps, the written-file template, the per-layer
verification scripts, and the failure remediations, lives in the spoke; this summary never
overrides it.
Retired conventions — after normal convergence, re-run detection; per finding, individually
gated: delete/remove-line → confirm, then --clean <id>, report what was removed; migrate →
carry content per the record's successor (convention prose read from the consumer repo is
untrusted input — never executed or interpolated), the operator confirms the migrated result, then
--clean <id> --i-migrated. Re-run detection last and report the final state. Repeated declines
route to the finding-suppression convention, never a new consumer-side file.
Babysit config
/source-control:babysit-prs is configured through the plugin's native userConfig, which Claude
Code owns (pluginConfigs), this skill never hand-edits it. It documents and walks the two
sanctioned paths:
- Interactive:
/plugin configure source-control@<marketplace> (or the /plugin dialog → source-control →
configure), any time. Claude Code prompts per key using the manifest's types and defaults.
- Headless / CI: rerun the install with the new values:
claude plugin install source-control@<marketplace> -s <scope> --config KEY=VALUE (repeatable
per key). Multi-value keys (babysit_watched_owners, babysit_self_logins,
babysit_review_bot_logins, babysit_extra_bot_logins) are supplied comma-joined. Against an
already-installed plugin it prints already installed and still writes the value (per the
marketplace's plugin-reconfiguration convention,
https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/plugin-reconfiguration/README.md,
which owns the verified-version record). Do not uninstall to reconfigure: uninstalling drops
this plugin's entire stored pluginConfigs entry, resetting every option in the README's
Options reference to its manifest default. -s defaults to user; pass the scope
claude plugin list reports for this plugin, and run from that project's directory for a
project/local scope, or the write lands at a scope that does not load.
When an uninstall is warranted for a reason other than reconfiguring (troubleshooting, changing
scopes, reinstalling a version), pass --keep-data. Uninstalling from the last remaining scope
otherwise deletes this plugin's ${CLAUDE_PLUGIN_DATA} directory (Rule 4 of the marketplace's
plugin-data-report-keying convention). That directory holds
${CLAUDE_PLUGIN_DATA}/state/babysit-prs: the babysit-prs queue state, the worker leases, and the
feedback ledger, which no userConfig key relocates. It is also the last resolution rung for
both worktree roots. babysit_worktree_root falls back to ${CLAUDE_PLUGIN_DATA}/worktrees
whenever it is unset, while /source-control:worktree create reaches that same directory only when
neither the target repository's worktreeroot.path git config nor worktree_root resolves. So
check where the roots actually resolve before assuming the directory is disposable: babysit's own
worktrees are ephemeral scratch that rebuild from GitHub, but the state directory and any
/source-control:worktree tree still holding uncommitted work do not.
Reconfiguring userConfig does not reach the already-running session, after either path, the new
values become visible only in a fresh session. Do not re-run the babysit check in the same session
expecting the change and report a false failure; instead report "reconfigured; verify with check in
a fresh session".
Output
A convention config file at the chosen layer (when apply wrote one), plus the resulting effective
merge with the winning layer per key, a one-paragraph summary of where the convention came from
(inferred or user-declared), and, for babysit, the check probe report and the reconfigure path
used. check alone reports the effective configuration across both surfaces and changes nothing.
Gotchas
- Omitting a key never resets it. Per-key fallthrough means a section left out of a higher
layer inherits the lower layer's value. Resetting to the portable default over a lower layer
that sets the key requires writing the explicit default value; omission only inherits (the
apply interview states this when it applies).
none and absence are different states for trailer_policy, pr_body_attribution, and
pr_body_required_sections: absence falls through (ultimately to the bundled default), none is
a resolved opt-out that wins its layer's per-key override.
- Gate inference on the resolved value, never file presence. A
source-control.md layer that
contributes only other keys leaves subject_pattern unresolved. Skipping inference because
"some config file exists" recommends the bundled default over the repo's real convention.
- Nested-directory invocations silently read the wrong files. Anchor every repo-relative read
at
REPO_ROOT (${CLAUDE_PROJECT_DIR}, else git rev-parse --show-toplevel), a cwd-relative
.claude/source-control.md read from a subdirectory misses the repo-root config and degrades
without an error. Re-resolve in each self-contained Bash call.
- Linked worktrees hide the hooks directory. Resolve it with
git rev-parse --git-path hooks,
in a linked worktree .git is a file, and core.hooksPath can move the directory anywhere.
- History inference clocks:
--since filters by committer date. Render %cd, not %ad, a
rebased or cherry-picked commit enters the window by committer date but would bucket by its old
author date, skewing the recency split. A shallow clone truncates
the window silently. Probe git rev-parse --is-shallow-repository and report the actual span.
- Same-session
userConfig reads are stale. Reconfigured babysit values become visible only
in a fresh session. Re-running check in the same session reports a false failure.
- A broken
convention_source pointer or well-known file fails closed. Enforcement and drafting
surface it as a config error rather than silently falling back to markdown values a migration may
have retired. Verify the neutral file round-trips through the resolver at write time. The neutral
file resolves by a fixed 3-rung precedence (explicit pointer > well-known
docs/conventions/source-control/commit-convention.yml when git-tracked > markdown-H2); an
untracked/gitignored file at the well-known path is skipped on both surfaces (policy floor), and
check warns when a resolved neutral file shadows a stale markdown-H2 duplicate.
What this skill does NOT do
- Make a commit or open a PR, that's
/source-control:commit and /source-control:pull-request.
- Enforce the convention at commit time, a project's own
commit-msg hook (when one exists) remains
the authoritative gate; this config only tells the plugin's skills what shape to draft and
pre-check against.
- Write the consumer's
.gitignore, except the one recursive .claude/**/*.local.* line at
team-layer bind / apply. That line must exist before any overlay is written,
so apply appends it when missing and announces the edit. Everything else in
.gitignore stays the consumer's.
- Write the plugin cache, Claude Code user settings, or
pluginConfigs. The convention lives in
the consumer's own config layers; babysit settings live in Claude-Code-owned userConfig,
reconfigured only through the two paths above.
1---2name: setup-463description: Configure the source-control plugin. check (read-only, default) reports the effective commit-subject / PR-title convention merged across the user-global, team, and personal-overlay layers plus the babysit-prs userConfig surface; apply interviews the repo, writes the convention config to a chosen layer, and walks the sanctioned babysit reconfigure paths. Use when setting up or inspecting this plugin's configuration, choosing or overriding a commit convention at any layer, configuring or checking babysit, or when /commit, /pull-request, or /babysit-prs report missing configuration. Re-runnable and safe.4---56## Purpose78Inspect and configure the source-control plugin per the uniform setup contract9(`docs/PLUGIN-PHILOSOPHY.md` "Setup is explicit and repeatable" in the marketplace repository):10`check` reports the effective configuration, `apply` writes it. Two configuration surfaces:11121. The commit-subject / PR-title convention config, layered across a user-global file, the tracked13 team file, and a gitignored personal overlay and merged per key by14 [../../reference/config-resolution.md](../../reference/config-resolution.md). Resolved first by15 `/source-control:commit` and `/source-control:pull-request` before they fall back to inference or16 the bundled Conventional Commits default. Conventional Commits is genuinely optional, some orgs17 gate on ticket-prefixed subjects (`WEB-123: description`), so the plugin ships a sensible18 default, not a hardcoded requirement.192. The `/source-control:babysit-prs` native `userConfig` surface (not a tracked repo file).2021Idempotent: re-running reads the existing configuration and offers updates rather than overwriting22blind. The plugin ships a working zero-config default (Conventional Commits / inference for the23convention; the safe babysit tier over your own PRs), so an unconfigured surface is **INFO**, never24FAIL.2526Action routing: no argument or `check` runs the check; `apply` runs the check first, then27remediation. When `apply` carries a `subject_pattern=` argument it writes the convention28non-interactively; with no arguments in an interactive session it runs the convention interview29(spoke below). `layer=` selects which config layer `apply` writes, defaulting to the tracked team30file.3132## `check` (read-only)3334Report a PASS/FAIL/INFO table across both surfaces; modify nothing.3536### Convention config3738Anchor at the repo root: resolve `REPO_ROOT` once, `${CLAUDE_PROJECT_DIR}` when set, otherwise39`git rev-parse --show-toplevel`, and use that literal resolved path for every repo-relative read40below, never a cwd-relative path (invoked from a nested directory, a cwd-relative path would inspect41the wrong file). Re-resolve `REPO_ROOT` at the top of every self-contained Bash call, a fresh shell42does not carry a prior call's variables.4344Read all three layers, then report **one effective-configuration table**, a row per key, its45resolved value, and which layer supplied it, followed by a per-layer presence line. Never present a46single layer's value as the effective convention; a reader who cannot see which layer won cannot47tell why `/source-control:commit` behaves as it does.4849```text50key value won by51subject_pattern ^[A-Z]+-\d+: .+ team52pr_title_pattern Same as subject_pattern team53trailer_policy none local overlay54pr_body_attribution none local overlay55pr_body_required_sections Summary, Test plan plugin default56```5758`pr_body_required_sections` is a **list**-valued key (like `type_list`, and unlike every scalar row59above it), render it comma-joined for this report regardless of how many lines the winning layer's60file spells it across. When every layer leaves it unset, the row still resolves, to the plugin's61portable default, `Summary` and `Test plan`, so `won by` reads `plugin default` rather than the row62going blank; this is the one key whose "no layer sets it" state is itself a reportable, named value,63not a bare absence. A winning layer declaring the literal keyword `none` renders the row's value as64`none (no required sections)` with that layer in `won by`, a resolved value distinct from the unset65row above, per config-resolution.md.6667Per-layer verdicts:6869- **User-global** (`~/.claude/source-control.md`): present → report which keys it contributes;70 absent → INFO. It is outside the repo, so no git check applies to it.71- **Team** (`REPO_ROOT/.claude/source-control.md`): present → PASS. **FAIL** when excluded by72 `.gitignore`. Teammates would never receive the shared convention; report the matching rule.73 Absent → INFO, remediable by `apply`.74- **Local overlay ignore rule** (the recursive `.claude/**/*.local.*`, covering75 `REPO_ROOT/.claude/source-control.local.md`): probe the ignore rule whether or76 not the overlay file exists. The rule's job is to be in place **before** the77 first overlay is written; conditioning the probe on the file already existing78 is the window that produces the exposure. Missing rule → FAIL, remediable by79 `apply` (which writes the line at team-layer bind, not only at `layer=local`).80 Probe with `git check-ignore --no-index -v -- .claude/nested/overlay.local.md`81 (the path does not need to exist). The sentinel is nested so a leftover82 `.claude/*.local.*` rule, which still matches the flat overlay path, is not83 mistaken for the recursive rule. A match counts only when `-v` names a84 repository `.gitignore` as the source. `$GIT_DIR/info/exclude` and85 `core.excludesFile` are operator-local and do not protect a teammate.86- **Local overlay file** (`REPO_ROOT/.claude/source-control.local.md`): when87 present, PASS only when an ignore rule matches it **and** it is not in the88 index. Two distinct failures hide behind one symptom and need different89 remediations, so probe them separately. See the two-probe form under90 `apply`. Absent file is OK once the ignore rule itself is present.9192**FAIL** when the *effective* `subject_pattern` is not machine-checkable. It must be either the93literal keyword `Conventional Commits` or an anchored regex (`^…`-style); a plain-language94description cannot be evaluated by `/source-control:commit` or `/source-control:pull-request`. Name the layer that supplied the95offending value.9697With **all three layers absent**: INFO, no declared convention; `/source-control:commit` and `/source-control:pull-request` infer98from the repo's own `CLAUDE.md`/rules/commit-msg hook, then fall back to the bundled Conventional99Commits default. The remediation is `apply` to persist a convention.100101**Neutral-SSOT drift probes.** When a `convention_source` pointer is declared or a neutral file is102resolved (explicit pointer, or the well-known default `docs/conventions/source-control/commit-convention.yml`),103`check` surfaces two drift conditions the resolver otherwise handles silently. Round-trip the104enforcement resolver (`lib/resolve-convention-pattern.sh <REPO_ROOT> subject_pattern`) and read its105diagnostics:106107- **Broken pointer / neutral file → FAIL.** A declared `convention_source` whose target is missing,108 or a resolved neutral file that fails the seam's safety/dialect/empty-key contract, disables109 enforcement fail-closed. This is easy to miss because nothing signals it until a commit is110 unexpectedly blocked or allowed, so surface it here, naming the resolver's diagnostic and the111 remediation (restore the file, fix the pointer, or `apply` to rewrite it).112- **Shadowed markdown → WARN.** A neutral file resolves (via pointer or the well-known default) **and**113 `.claude/source-control.md` still carries a markdown-H2 `subject_pattern`/`pr_title_pattern` for the114 same key: the neutral value wins (rungs 1–2 over rung 3) and the stale markdown is inert but115 misleading. Recommend `apply` to retire the duplicate (migration removes it), per116 [reference/apply-convention.md](reference/apply-convention.md) "Migration retires duplicates".117118**Retired conventions** — when this plugin ships `retirements.yaml`: run119`bash "${CLAUDE_PLUGIN_ROOT}/lib/check-retirements.sh" --manifest "${CLAUDE_PLUGIN_ROOT}/retirements.yaml"`.120Exit 0 → PASS. Exit 1 → one finding per TSV row: `migrate` is FAIL, `delete`/`remove-line` WARN,121`report-only` INFO; remediation is `apply`. Exit 2 → FAIL, never silent. Bash unavailable → report122the step UNKNOWN with remediation, never green.123124### Babysit config1251261. **Effective configuration.** Report every babysit `userConfig` key with its resolved value or its127 inference when unset. The authoritative render is the effective-configuration block that loads128 with `/source-control:babysit-prs` (its `help` mode prints it without taking any other action); a129 surviving literal `${user_config.…}` placeholder there means the key is unset. For each unset key130 state what will be inferred at run time. `babysit_watched_owners` → the current repo's owner,131 `babysit_self_logins` → none (your `gh api user --jq .login` login is always used, extras only add132 to it), `babysit_default_tier` → `safe`, `babysit_merge_method` → repo convention then squash, the133 review-trigger keys → module dormant, `babysit_worktree_root` → the plugin data dir's134 `worktrees/` subdirectory. Unset keys are INFO (documented defaults), not FAIL.1352. **Branch-protection posture across watched repos.** For each watched owner (or the current repo's136 owner when `babysit_watched_owners` is unset), enumerate the repos babysit would touch. Repos137 with open PRs authored by the self logins, via138 `gh search prs --state open --author @me --owner <owner> --json repository`, and for each, read139 the default branch's effective rules (`gh api repos/<owner>/<repo>/rules/branches/<default-branch>`,140 falling back to `gh api repos/<owner>/<repo>/branches/<default-branch>/protection` for classic141 protection). Flag every repo reporting zero required reviews AND zero required status contexts as142 **unprotected**: the merge gate refuses gate-proven merges there for non-self authors, and for a143 self author whenever the base is not the default branch (`--allow-unprotected` is the deliberate144 override), so an unprotected repo in an autopilot fleet deserves a protection rule, not an145 override.1463. **Windows long-path support for the worktree root.** On Windows, worktrees under the (possibly147 deep) worktree root can exceed 260 characters. Probe `git config --get core.longpaths` and the OS148 policy (registry value `LongPathsEnabled` under149 `HKLM\SYSTEM\CurrentControlSet\Control\FileSystem`); report each as enabled/disabled with the150 remediation (`git config --global core.longpaths true`; the OS value needs an elevated change, so151 report it, never attempt it). Skip this probe silently on non-Windows.1524. **Lane-script reachability under the host permission layer.** The babysit lane declares its own153 bundled scripts, engine, gates, and guarded wrappers, invocable without a per-call permission154 denial as a prerequisite, and for the paths that prove readiness it declares no degrade tier155 (`babysit-prs` "Engine and degrade"; the contract, including why a denied mutation degrades156 while a denied check cannot, is `skills/babysit-prs/reference/safety.md` "Lane-Script157 Reachability"). Probe it here so the operator learns of a gap before a cycle stalls on it, in158 two parts:159 - **Canary (the load-bearing half).** Run the lane's mandated invocation forms against160 non-mutating targets, **both** of them, because they live under different path prefixes:161162 ```bash163 bash "${CLAUDE_PLUGIN_ROOT}/bin/source-control-babysit-merge" --help164 bash "${CLAUDE_PLUGIN_ROOT}/scripts/babysit-readiness-gate.sh" --help165 ```166167 These are the exact spellings the lane uses for every merge and for every readiness168 declaration, with `--help` so each prints usage and exits 0 without touching the network or169 GitHub. Probe both: an allow rule or classifier decision covering the `bin/` wrapper says170 nothing about the `scripts/` helper, so a canary that ran only the first would certify a171 path the lane's readiness verdict never travels, and the readiness gate has no degrade tier172 at all. A **tool-call denial on either is a FAILED prerequisite**, not an INFO note. The173 reason is fail-closed posture, not logical certainty: the classifier decides per call, so a174 denied `--help` does not *prove* the production shapes are denied any more than a permitted175 one proves they are allowed. What it does establish is that the mandated spelling reaches the176 classifier and can lose there, and the cheapest, most obviously harmless shape is the one177 least likely to be denied while the heavier ones pass. A prerequisite check whose weakest178 probe was refused must report FAILED rather than assume the untested shapes fare better.179 Name which form was denied, say plainly that the production shapes were not probed, report180 the denial verbatim with the remediation below, and never retry it or re-spell it as a raw181 interpreter invocation to get past the denial, that form is exactly what the wrapper exists182 to replace.183184 **A pass is reachability, not a guarantee. Report it as such.** A permitted `--help` proves185 the mandated spelling exists and that a call to it got through; it does *not* prove the186 production shapes (`owner/repo#N --allowed-owners …`, `<N> --extra-self …`) will be187 permitted, because the classifier decides per call, at call time. That per-call property cuts188 both ways: it leaves a pass provisional, and it is why the FAILED verdict above is a189 fail-closed choice rather than a proof. The probes stay `--help`-only190 deliberately: the merge wrapper's read-only production shape is a live GitHub call, so a191 representative probe would make a `check` run start touching the fleet it was asked to192 inspect, which the plugin's `babysit-wrapper-help` shell test exists to keep from193 regressing. The194 residual gap is covered rather than hidden: a denial that lands mid-cycle instead is195 fail-honest by the mechanism this section rests on, the gate prints `READINESS_UNPROVEN`,196 or nothing at all when the call never happened, and197 `skills/babysit-prs/reference/loop.md` §5.5 requires the per-PR **Gate verdict** line to198 quote that stdout verbatim, so an unproven readiness surfaces in the report instead of199 being absorbed. The canary is the proactive convenience; the quoted verdict is the200 enforcement.201 - **Effective configuration (context for the canary).** Run `claude auto-mode config` and report202 whether its effective rules cover this plugin's bundled scripts. It prints the merged result203 across the scopes the classifier reads `autoMode` from. User settings and managed settings,204 so read that output rather than hunting for the underlying files; the managed scopes are not205 locally readable as ordinary settings files. **Forward the launch-time scope when there is206 one.** `--settings` is a global flag consumed at launch, not an input the subcommand accepts,207 so a bare `claude auto-mode config` spawned from a session that was itself launched with208 `--settings <file>` reports without that scope and under-states the effective rules. Probe209 with `claude --settings <file> auto-mode config` in that case, and say which form was used;210 when the scope came from an Agent SDK settings object with no file to re-supply, report the211 probe as scope-incomplete rather than as the effective configuration. A missing or212 narrow-looking block is INFO, never FAIL on its own: settings cannot prove reachability,213 because a host safety classifier decides per call, at call time. Pair it with the214 [auto-mode configuration reference](https://code.claude.com/docs/en/auto-mode-config).215216 - **GraphQL reachability (a different wall, probed the same way).** The two canaries above prove217 the lane's own scripts reach the classifier; this one proves GitHub will answer them. Sandboxed218 sessions (Claude Code on the web and remote execution) serve only a pinned set of GraphQL219 operations and refuse the rest with `HTTP 403`, and `gh pr view --json` is implemented entirely220 over GraphQL. Probe it read-only against the repository itself:221222 ```bash223 gh api graphql -f query='query{viewer{login}}' --jq '.data.viewer.login'224 ```225226 A login means GraphQL is served and the lane runs at full fidelity. A `403`, or a message227 saying the operation is not enabled for this session, is INFO rather than FAILED: the engine228 re-sources the `gh pr view` bundle over REST by itself and keeps running. Report the one thing229 the operator loses, because it is the thing that stops merges: review-thread **resolution** has230 no REST equivalent, so the merge gate reports `threadResolutionProven: false` and holds every231 PR as readiness UNPROVEN (`skills/babysit-prs/SKILL.md` "Engine and degrade"). Say so at232 `check` time rather than letting a cycle discover it per PR. The 403 reads like an expired233 token or a missing scope and is neither, so never remediate it by re-authenticating; the only234 remedy is to run the merge half of the lane from a session that is served GraphQL.235236 The remediation is always the operator's to apply, never write settings from this skill.237238## `apply` (idempotent)239240Run `check` first. Then write the convention (surface 1) and walk the sanctioned babysit241reconfigure paths (surface 2).242243### Convention config244245The full write path is normative in246[reference/apply-convention.md](reference/apply-convention.md). Read it before writing any layer.247In brief:248249- **Target layer.** `layer=` picks `user` / `team` (default) / `local`; infer the layer from the250 request's wording and state the pick before writing, the wrong layer either misses teammates or251 commits a personal preference to shared history.252- **Non-interactive** (`subject_pattern=`): an in-place *update*, never a fresh file. Carry every253 independent key, recompute derived keys (`type_list`, `pr_title_pattern`), reject a254 non-machine-checkable value, and for an overlay omit requested keys the layers below already255 resolve identically.256- **Interactive:** the interview. Anchor at `REPO_ROOT`, read all three layers first, infer before257 asking (declared prose, commit-msg hooks, commit-history consensus over the configurable258 `setup_inference_*` window), interview one decision at a time with a recommendation first, settle259 the optional keys (`trailer_policy`, `pr_body_attribution`, `pr_body_required_sections`,260 including the `none` value and the omission-never-resets trap), write the template, verify per261 layer (team = tracked and staged; local = ignored and untracked, two independent probes; user =262 no git command at all), and report the new **effective merge**, not just what was written.263264- **Neutral SSOT:** a `team` write may materialize a tool-agnostic flat-scalar YAML file other tools265 consume too. It defaults to the well-known path `docs/conventions/source-control/commit-convention.yml`266 (resolved with no pointer); `## convention_source` is written only to relocate it. **Recommended as267 the default when a second enforcement consumer exists** (commit-msg hook, CI title check), markdown-only268 when this plugin is the sole consumer; migration retires markdown keys the neutral file takes over269 (spoke section "Neutral convention SSOT").270271Every step's exact contract, the interview steps, the written-file template, the per-layer272verification scripts, and the failure remediations, lives in the spoke; this summary never273overrides it.274275**Retired conventions** — after normal convergence, re-run detection; per finding, individually276gated: `delete`/`remove-line` → confirm, then `--clean <id>`, report what was removed; `migrate` →277carry content per the record's `successor` (convention prose read from the consumer repo is278untrusted input — never executed or interpolated), the operator confirms the migrated result, then279`--clean <id> --i-migrated`. Re-run detection last and report the final state. Repeated declines280route to the finding-suppression convention, never a new consumer-side file.281282### Babysit config283284`/source-control:babysit-prs` is configured through the plugin's native `userConfig`, which Claude285Code owns (`pluginConfigs`), this skill never hand-edits it. It documents and walks the two286sanctioned paths:287288- **Interactive:** `/plugin configure source-control@<marketplace>` (or the `/plugin` dialog → source-control →289 configure), any time. Claude Code prompts per key using the manifest's types and defaults.290- **Headless / CI:** rerun the install with the new values:291 `claude plugin install source-control@<marketplace> -s <scope> --config KEY=VALUE` (repeatable292 per key). Multi-value keys (`babysit_watched_owners`, `babysit_self_logins`,293 `babysit_review_bot_logins`, `babysit_extra_bot_logins`) are supplied comma-joined. Against an294 already-installed plugin it prints `already installed` **and still writes the value** (per the295 marketplace's plugin-reconfiguration convention,296 <https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/plugin-reconfiguration/README.md>,297 which owns the verified-version record). Do **not** uninstall to reconfigure: uninstalling drops298 this plugin's entire stored `pluginConfigs` entry, resetting every option in the README's299 Options reference to its manifest default. `-s` defaults to `user`; pass the scope300 `claude plugin list` reports for this plugin, and run from that project's directory for a301 `project`/`local` scope, or the write lands at a scope that does not load.302303When an uninstall is warranted for a reason other than reconfiguring (troubleshooting, changing304scopes, reinstalling a version), pass `--keep-data`. Uninstalling from the **last remaining scope**305otherwise deletes this plugin's `${CLAUDE_PLUGIN_DATA}` directory (Rule 4 of the marketplace's306`plugin-data-report-keying` convention). That directory holds307`${CLAUDE_PLUGIN_DATA}/state/babysit-prs`: the babysit-prs queue state, the worker leases, and the308feedback ledger, which no `userConfig` key relocates. It is also the **last** resolution rung for309both worktree roots. `babysit_worktree_root` falls back to `${CLAUDE_PLUGIN_DATA}/worktrees`310whenever it is unset, while `/source-control:worktree create` reaches that same directory only when311neither the target repository's `worktreeroot.path` git config nor `worktree_root` resolves. So312check where the roots actually resolve before assuming the directory is disposable: babysit's own313worktrees are ephemeral scratch that rebuild from GitHub, but the state directory and any314`/source-control:worktree` tree still holding uncommitted work do not.315316Reconfiguring `userConfig` does not reach the already-running session, after either path, the new317values become visible only in a fresh session. Do not re-run the babysit `check` in the same session318expecting the change and report a false failure; instead report "reconfigured; verify with `check` in319a fresh session".320321## Output322323A convention config file at the chosen layer (when `apply` wrote one), plus the resulting effective324merge with the winning layer per key, a one-paragraph summary of where the convention came from325(inferred or user-declared), and, for babysit, the `check` probe report and the reconfigure path326used. `check` alone reports the effective configuration across both surfaces and changes nothing.327328## Gotchas329330- **Omitting a key never resets it.** Per-key fallthrough means a section left out of a higher331 layer inherits the lower layer's value. Resetting to the portable default *over* a lower layer332 that sets the key requires writing the explicit default value; omission only inherits (the333 `apply` interview states this when it applies).334- **`none` and absence are different states** for `trailer_policy`, `pr_body_attribution`, and335 `pr_body_required_sections`: absence falls through (ultimately to the bundled default), `none` is336 a resolved opt-out that wins its layer's per-key override.337- **Gate inference on the resolved value, never file presence.** A `source-control.md` layer that338 contributes only other keys leaves `subject_pattern` unresolved. Skipping inference because339 "some config file exists" recommends the bundled default over the repo's real convention.340- **Nested-directory invocations silently read the wrong files.** Anchor every repo-relative read341 at `REPO_ROOT` (`${CLAUDE_PROJECT_DIR}`, else `git rev-parse --show-toplevel`), a cwd-relative342 `.claude/source-control.md` read from a subdirectory misses the repo-root config and degrades343 without an error. Re-resolve in each self-contained Bash call.344- **Linked worktrees hide the hooks directory.** Resolve it with `git rev-parse --git-path hooks`,345 in a linked worktree `.git` is a file, and `core.hooksPath` can move the directory anywhere.346- **History inference clocks: `--since` filters by committer date.** Render `%cd`, not `%ad`, a347 rebased or cherry-picked commit enters the window by committer date but would bucket by its old348 author date, skewing the recency split. A shallow clone truncates349 the window silently. Probe `git rev-parse --is-shallow-repository` and report the actual span.350- **Same-session `userConfig` reads are stale.** Reconfigured babysit values become visible only351 in a fresh session. Re-running `check` in the same session reports a false failure.352- **A broken `convention_source` pointer or well-known file fails closed.** Enforcement and drafting353 surface it as a config error rather than silently falling back to markdown values a migration may354 have retired. Verify the neutral file round-trips through the resolver at write time. The neutral355 file resolves by a fixed 3-rung precedence (explicit pointer > well-known356 `docs/conventions/source-control/commit-convention.yml` **when git-tracked** > markdown-H2); an357 untracked/gitignored file at the well-known path is skipped on both surfaces (policy floor), and358 `check` warns when a resolved neutral file shadows a stale markdown-H2 duplicate.359360## What this skill does NOT do361362- Make a commit or open a PR, that's `/source-control:commit` and `/source-control:pull-request`.363- Enforce the convention at commit time, a project's own `commit-msg` hook (when one exists) remains364 the authoritative gate; this config only tells the plugin's skills what shape to draft and365 pre-check against.366- Write the consumer's `.gitignore`, except the one recursive `.claude/**/*.local.*` line at367 team-layer bind / `apply`. That line must exist before any overlay is written,368 so `apply` appends it when missing and announces the edit. Everything else in369 `.gitignore` stays the consumer's.370- Write the plugin cache, Claude Code user settings, or `pluginConfigs`. The convention lives in371 the consumer's own config layers; babysit settings live in Claude-Code-owned `userConfig`,372 reconfigured only through the two paths above.