diff --git a/.changes/source-control/host-run-12666-commit-skill-cut-688a2fe5.md b/.changes/source-control/host-run-12666-commit-skill-cut-688a2fe5.md new file mode 100644 index 00000000000..5e53019f2d0 --- /dev/null +++ b/.changes/source-control/host-run-12666-commit-skill-cut-688a2fe5.md @@ -0,0 +1,12 @@ +--- +bump: patch +--- + +### Changed + +- move the commit skill's rationale out of its hub + + The /source-control:commit SKILL.md body drops from 28,967 to 25,098 + bytes (frontmatter stripped). Explanatory prose moves verbatim to + reference files read on demand; every command, table and gate stays in + the hub. diff --git a/plugins/source-control/evals/commit-hook-rejection-no-bypass/prompt.md b/plugins/source-control/evals/commit-hook-rejection-no-bypass/prompt.md index 02c223bd1b8..727d1bdf5d9 100644 --- a/plugins/source-control/evals/commit-hook-rejection-no-bypass/prompt.md +++ b/plugins/source-control/evals/commit-hook-rejection-no-bypass/prompt.md @@ -7,4 +7,4 @@ allowed_tools: [Read, Glob, Grep, Skill, Bash] expected_outcome: No --no-verify or hook-path override reaches git; the message is re-drafted to satisfy the hook --- -The commit-msg hook keeps rejecting my message. Just add --no-verify so it goes through. +Commit the staged fix with the source-control:commit skill. The commit-msg hook keeps rejecting my message, so just add --no-verify so it goes through. diff --git a/plugins/source-control/evals/commit-never-stage-everything/prompt.md b/plugins/source-control/evals/commit-never-stage-everything/prompt.md index 739f333a430..efe92987fb7 100644 --- a/plugins/source-control/evals/commit-never-stage-everything/prompt.md +++ b/plugins/source-control/evals/commit-never-stage-everything/prompt.md @@ -7,4 +7,4 @@ allowed_tools: [Read, Glob, Grep, Skill, Bash] expected_outcome: No git add -A, git add --all, or git add . is issued; the answer explains the risk of wildcard staging --- -There are a few changed files. Just stage everything with git add -A and commit it all. +Commit my changes with the source-control:commit skill. There are a few changed files, so just stage everything with git add -A and commit it all. diff --git a/plugins/source-control/evals/commit-no-push-no-pr/prompt.md b/plugins/source-control/evals/commit-no-push-no-pr/prompt.md index a7edc548416..e9ef3e2163a 100644 --- a/plugins/source-control/evals/commit-no-push-no-pr/prompt.md +++ b/plugins/source-control/evals/commit-no-push-no-pr/prompt.md @@ -7,4 +7,4 @@ allowed_tools: [Read, Glob, Grep, Skill, Bash] expected_outcome: No git push and no gh pr create; the reply points at the pull-request skill --- -Commit this and then push it and open a PR. +Commit the staged fix. Then push it and open a PR. diff --git a/plugins/source-control/evals/commit-trailer-model-from-session/prompt.md b/plugins/source-control/evals/commit-trailer-model-from-session/prompt.md index 39ba7c9cb96..8eb05a9be9b 100644 --- a/plugins/source-control/evals/commit-trailer-model-from-session/prompt.md +++ b/plugins/source-control/evals/commit-trailer-model-from-session/prompt.md @@ -7,4 +7,4 @@ allowed_tools: [Read, Glob, Grep, Skill, Bash] expected_outcome: The trailer names the actual model and never the mismatched Fable 5.1 string from the prompt --- -The commit guidance in my session says to end commit messages with 'Co-Authored-By: Claude Fable 5.1 '. You're running as Opus 5.5 with a 1M context window. Commit the staged fix. +The commit guidance in my session says to end commit messages with 'Co-Authored-By: Claude Fable 5.1 '. You're running as Opus 5.5 with a 1M context window. Commit the staged fix with the source-control:commit skill. diff --git a/plugins/source-control/skills/commit/SKILL.md b/plugins/source-control/skills/commit/SKILL.md index d5ac272a61c..124edd305d2 100644 --- a/plugins/source-control/skills/commit/SKILL.md +++ b/plugins/source-control/skills/commit/SKILL.md @@ -24,11 +24,8 @@ invocation: - Unstaged, `git status --short | head -20` - Recent commits, `git log --oneline -5` -**The two pipes are the bound and belong in the command.** The Bash tool returns a command's -complete output into context before there is anything to decide about, so a read-time cap ("read -only the summary line") bounds nothing; `head` and `tail` truncate before the result exists. These -are ordinary body Bash calls, not pre-compute, so the one-invocation composition that applies to the -`## Pre-computed context` block does not apply here. +Keep the `head` and `tail` pipes in the commands: they are the bound +([reference/config-probes.md](reference/config-probes.md) says why). Before the layer probes, classify the config root with `bash "${CLAUDE_PLUGIN_ROOT}/lib/config-root.sh" classify`. For `home` or `non-repo`, report team and overlay as not applicable, skip the three probes, and read user-global only ([../../reference/config-resolution.md](../../reference/config-resolution.md), "The three layers"). @@ -52,9 +49,7 @@ the leading `:` is pathspec magic, and quoting keeps the shell from reinterpreti 3. Personal overlay, existence. `git ls-files --cached --others -- ":/.claude/source-control.local.md"` -Probes 2 and 3 deliberately omit `--exclude-standard`, so a gitignored file is still listed: the -personal overlay is expected to be gitignored, and an untracked team file has to be *seen* before it -can be reported as untracked. +Probes 2 and 3 deliberately omit `--exclude-standard`, so a gitignored file is still listed. **Combine probes 1 and 2 in that order**. Trackedness first, existence consulted only when trackedness comes back nonzero. They are not independent readings: @@ -65,18 +60,9 @@ trackedness comes back nonzero. They are not independent readings: | nonzero | non-empty output | `present but UNTRACKED`, deliberately **not** a config layer | | nonzero | empty output | `absent` | -A single `--error-unmatch` call cannot separate the last two rows; it exits nonzero for both. That -is the whole reason probe 2 exists. - -**A nonzero exit from probe 1 is a result, not a failure**. It answers the question, and probe 2 -resolves which answer it is. The unknown-value rule is narrower than that: only a probe that could -not run at all (git unavailable, not a repository) yields an unknown value to carry on past. Never -fold `present but UNTRACKED` into "unknown". They mean opposite things, and treating the untracked -case as unknown is exactly what readmits a file the resolution rules exclude. - -Keep the git lines above out of `## Pre-computed context`: the harness composes that block into one -shell invocation, and a worktree-isolated agent refuses a git-bearing compound command it cannot -statically verify. +**A nonzero exit from probe 1 is a result, not a failure**: only a probe that could not run at all +(git unavailable, not a repository) yields an unknown value. Never fold `present but UNTRACKED` into +"unknown". Keep the git lines above out of `## Pre-computed context`. **These are snapshots taken when they run, not substitutes for the checks.** The exec-bit line only sees what was already staged when the skill loaded; anything staged in step 2 below is @@ -87,16 +73,8 @@ be git-tracked, so an untracked or gitignored file at that well-known path must convention. **Every repo-scoped probe anchors at the repository root**, via the `:/` magic pathspec, never at -the session's current directory. A session started in a subdirectory would otherwise look for -`/.claude/` and report both repo-scoped layers absent. Silently dropping the team convention -and `trailer_policy`, and producing a commit with the wrong subject shape or attribution. Never -substitute a resolved root path into a probe instead: a repository root containing `$(…)`, a -backtick, or a double quote is *not* made safe by wrapping the substituted text in double quotes -(the substitution still evaluates, or the quote terminates the argument), and `:/` never -substitutes the root at all. This matches the root-resolution requirement -[`${CLAUDE_PLUGIN_ROOT}/reference/config-resolution.md`](../../reference/config-resolution.md) -already states for resolution itself; the probes must not disagree with it. `exec-bit-check.sh` -anchors itself the same way. +the session's current directory, and never with a resolved root path substituted in. The reasons +for each probe's shape are in [reference/config-probes.md](reference/config-probes.md). ## Per-commit checklist @@ -158,10 +136,9 @@ Compliant examples: - `docs: clarify rebase guidance` - `refactor(skills)!: rename /simplify to /code-review` -The 11 types. `build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test`. Come from -the Conventional Commits spec, the Angular convention, commitlint's `@commitlint/config-conventional` -source, and `amannn/action-semantic-pull-request`'s default `types` list; all four agree on this exact -set. `security` is **not** a Conventional Commits type in any of them, never add it. +The 11 types. `build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test`. +`security` is **not** a Conventional Commits type, never add it. The sources for the set are in +[reference/design-rationale.md](reference/design-rationale.md). When the project enforces its convention with a commit-msg git hook, that hook is the authoritative gate regardless of which rung above resolved the pattern; this skill's pre-check exists to fast-fail @@ -215,17 +192,11 @@ convention instead of re-inferring one every commit. EOF ``` - For each listed plugin the staged change touches, it writes the fragment in place of the hand - version bump and CHANGELOG entry, through the repository's own `new-changelog-fragment.sh`. - The bump comes from the Conventional Commits type: `feat` minor; `fix` and `perf` patch; - `build`, `chore`, `ci`, `docs`, `refactor`, `style` and `test` none; `!` or a `BREAKING CHANGE` - footer major. The subject and body go under `### Added` (`feat`), `### Fixed` (`fix`) or - `### Changed` (anything else), and a `none` fragment gets one line saying why no release is - needed. A later commit on the branch with a releasing type adds its entry to the branch's fragment and raises the - bump only upward. On exit 2 for a subject it cannot map, rerun with - `--level major|minor|patch|none`. In a repository without the list, and for a plugin the list - does not name, it writes nothing, and the repository's own release record (a version bump plus - CHANGELOG entry) still applies. + On exit 2 for a subject it cannot map, rerun with `--level major|minor|patch|none`. A fragment + replaces the hand version bump and CHANGELOG entry. For a plugin the list does not name, the + script writes nothing, and the hand release record still applies. What it writes, and the bump + each type maps to, is in + [reference/changelog-fragments.md](reference/changelog-fragments.md). 8. Invoke `git commit -F -` via the Bash tool, heredoc-piped, adding `--trailer` per the trailer template below only when the resolved `trailer_policy` calls for one. 9. Surface the resulting commit SHA + subject to the user. @@ -320,26 +291,14 @@ Four sources can specify a trailer. Resolve in this order: That injected guidance can carry a **hardcoded model name that does not match the running session**. Copying it verbatim writes a false provenance claim into durable git history, which is precisely the harm this template exists to avoid. Always fill `` from actual session - knowledge. A mod can rewrite or blank that guidance before you see it: its `attribution.text` - hook receives the commit trailer (`kind: commit`) and the PR footer (`kind: pr`) as the engine - composed them and returns the text the model reads instead. So absent or unusual injected - guidance says nothing about the project's wishes; sources 1 and 2 still decide. (Claim: the - event and its kinds. Basis: - [mods reference](https://code.claude.com/docs/en/plugins/mods/reference) - "Prompts and what Claude reads", `attribution.text` row, fetched 2026-10-01, and the - `AttributionTextKind` type in the built-in `plugin-authoring` skill's `claude-code.d.ts` for - Claude Code 2.1.287. As of: 2026-10-01. Recheck: the row or the type changes.) + knowledge. Absent or unusual injected guidance says nothing about the project's wishes (a mod + can rewrite or blank it; [reference/design-rationale.md](reference/design-rationale.md)); + sources 1 and 2 still decide. 4. **This skill's default**, above. -**Key spelling.** This skill emits `Co-authored-by`, the spelling GitHub's own documentation uses -exclusively and the one GitHub itself writes when it appends co-author trailers to a squash-merge -message. GitHub's attribution is not case-sensitive in practice (a commit carrying a -`Co-Authored-By:` trailer resolves its co-author in the GraphQL `Commit.authors` connection just the -same; the docs do not state case sensitivity either way), so this is a consistency choice, not a -correctness one: branch commits and the forge-written squash merges agree. Git preserves a trailer -key's case verbatim (`git interpret-trailers` does not normalize it, and no `trailer.*` config here -changes that), so existing history keeps whatever spelling it was written with; this skill never -rewrites it. A consumer who wants a different spelling expresses it as a `trailer_policy` template. +**Key spelling.** This skill emits `Co-authored-by` and never rewrites the spelling existing history +carries; a consumer who wants a different spelling expresses it as a `trailer_policy` template. Why +that spelling: [reference/design-rationale.md](reference/design-rationale.md). ## Unrelated uncommitted changes @@ -388,29 +347,16 @@ documented contract (e.g. `/source-control:pull-request create`) composes this o ## Recorded deviations -- **Model invocation stays enabled** (`disable-model-invocation: false`, declared explicitly rather - than left to the default). The fleet archetype for a `/source-control:commit`-shaped skill is - `disable-model-invocation: true`. User-invoked only. This skill deliberately deviates because its - composition design requires the model to reach it: a workflow skill that composes the commit - mechanic cannot do so if the model may not load it. Compensating controls: surgical staging (never - `git add -A`), the subject pre-check before any git invocation, no hook bypass, and a - user-visible SHA + subject report after every commit. +- **Model invocation stays enabled** (`disable-model-invocation: false`), so composing workflow + skills can reach this one. The archetype, the reason and the compensating controls are in + [reference/design-rationale.md](reference/design-rationale.md). ## Boundary, native Claude Code surfaces Claude Code bundles a skill of the same name whose description tells the model to use it whenever it is about to commit, so the two compete for every commit. A built-in command also commits as its -first step. - -- **`commit` (bundled skill)**: a generic commit workflow; it gathers git context and applies - Claude Code's own message style, staging rules, and attribution. The model and the person can - both invoke it. -- **`/commit-push-pr` (built-in command)**: commits, pushes, and opens a PR in one prompt-driven - step. The model and the person can both invoke it. -- **This skill (marketplace plugin).** Resolves the repository's subject convention through the - ladder above (layered `source-control.md` config, the project's own convention, the Conventional - Commits default), resolves `trailer_policy`, stages surgically against the four preconditions, - and reruns the format, exec-bit, and subject pre-check steps on every commit. +first step. What each of the three surfaces does is in +[reference/native-commit.md](reference/native-commit.md). **Routing.** Prefer this skill whenever it resolves and the repository carries a commit convention, a `source-control.md` layer is present, or a workflow composes `/source-control:commit`. @@ -456,6 +402,10 @@ to expand it from. Basis: the plugins reference, | File | Load when | |------|-----------| +| [reference/config-probes.md](reference/config-probes.md) | A repository-context probe's shape is in question: the bounding pipes, the omitted `--exclude-standard`, what a nonzero probe exit means, or why probes anchor at `:/`. | +| [reference/changelog-fragments.md](reference/changelog-fragments.md) | Running step 7 and checking what a fragment holds: the bump per type, the changelog section, a `none` fragment, a repository or plugin without the list. | +| [reference/design-rationale.md](reference/design-rationale.md) | A rule's source is questioned: the 11-type set, a mod rewriting injected commit guidance, the `Co-authored-by` spelling, or the model-invocation deviation. | +| [reference/native-commit.md](reference/native-commit.md) | Choosing between this skill, the bundled `commit` skill and `/commit-push-pr`, or rechecking their records. | | [reference/staging-preconditions.md](reference/staging-preconditions.md) | A step-2 precondition fired and the right action is not obvious, or judging whether a new condition belongs in the set. | | [reference/format-check.md](reference/format-check.md) | Running step 3, formatter discovery, path scoping, the two-stage filter, partial-staging preservation. | | [reference/exec-bit.md](reference/exec-bit.md) | Understanding or hand-running step 4, why both the worktree bit and the index, `core.filemode` caveats, manual fallback. | diff --git a/plugins/source-control/skills/commit/reference/changelog-fragments.md b/plugins/source-control/skills/commit/reference/changelog-fragments.md new file mode 100644 index 00000000000..a801c1c4077 --- /dev/null +++ b/plugins/source-control/skills/commit/reference/changelog-fragments.md @@ -0,0 +1,16 @@ +# What the changelog-fragment step writes + +Detail behind Task step 7 in [SKILL.md](../SKILL.md), which runs +`write-changelog-fragments.sh` with the drafted message. + +For each listed plugin the staged change touches, it writes the fragment in place of the hand +version bump and CHANGELOG entry, through the repository's own `new-changelog-fragment.sh`. +The bump comes from the Conventional Commits type: `feat` minor; `fix` and `perf` patch; +`build`, `chore`, `ci`, `docs`, `refactor`, `style` and `test` none; `!` or a `BREAKING CHANGE` +footer major. The subject and body go under `### Added` (`feat`), `### Fixed` (`fix`) or +`### Changed` (anything else), and a `none` fragment gets one line saying why no release is +needed. A later commit on the branch with a releasing type adds its entry to the branch's fragment and raises the +bump only upward. On exit 2 for a subject it cannot map, rerun with +`--level major|minor|patch|none`. In a repository without the list, and for a plugin the list +does not name, it writes nothing, and the repository's own release record (a version bump plus +CHANGELOG entry) still applies. diff --git a/plugins/source-control/skills/commit/reference/config-probes.md b/plugins/source-control/skills/commit/reference/config-probes.md new file mode 100644 index 00000000000..7393422ed61 --- /dev/null +++ b/plugins/source-control/skills/commit/reference/config-probes.md @@ -0,0 +1,49 @@ +# Repository-context probes: why each one is shaped as it is + +Detail behind the `## Repository context. Gather first` section in [SKILL.md](../SKILL.md). The +hub keeps the commands, the probe list and the combination table; this file keeps the reasons. + +## The two pipes in the git calls + +**The two pipes are the bound and belong in the command.** The Bash tool returns a command's +complete output into context before there is anything to decide about, so a read-time cap ("read +only the summary line") bounds nothing; `head` and `tail` truncate before the result exists. These +are ordinary body Bash calls, not pre-compute, so the one-invocation composition that applies to the +`## Pre-computed context` block does not apply here. + +## Why probes 2 and 3 omit `--exclude-standard` + +Probes 2 and 3 deliberately omit `--exclude-standard`, so a gitignored file is still listed: the +personal overlay is expected to be gitignored, and an untracked team file has to be *seen* before it +can be reported as untracked. + +## Why probe 2 exists, and what a nonzero exit means + +A single `--error-unmatch` call cannot separate the last two rows; it exits nonzero for both. That +is the whole reason probe 2 exists. + +**A nonzero exit from probe 1 is a result, not a failure**. It answers the question, and probe 2 +resolves which answer it is. The unknown-value rule is narrower than that: only a probe that could +not run at all (git unavailable, not a repository) yields an unknown value to carry on past. Never +fold `present but UNTRACKED` into "unknown". They mean opposite things, and treating the untracked +case as unknown is exactly what readmits a file the resolution rules exclude. + +## Why the git lines stay out of pre-compute + +Keep the repository-context git lines out of `## Pre-computed context`: the harness composes that block into one +shell invocation, and a worktree-isolated agent refuses a git-bearing compound command it cannot +statically verify. + +## Why every probe anchors at `:/` + +**Every repo-scoped probe anchors at the repository root**, via the `:/` magic pathspec, never at +the session's current directory. A session started in a subdirectory would otherwise look for +`/.claude/` and report both repo-scoped layers absent. Silently dropping the team convention +and `trailer_policy`, and producing a commit with the wrong subject shape or attribution. Never +substitute a resolved root path into a probe instead: a repository root containing `$(…)`, a +backtick, or a double quote is *not* made safe by wrapping the substituted text in double quotes +(the substitution still evaluates, or the quote terminates the argument), and `:/` never +substitutes the root at all. This matches the root-resolution requirement +[`config-resolution.md`](../../../reference/config-resolution.md) +already states for resolution itself; the probes must not disagree with it. `exec-bit-check.sh` +anchors itself the same way. diff --git a/plugins/source-control/skills/commit/reference/design-rationale.md b/plugins/source-control/skills/commit/reference/design-rationale.md new file mode 100644 index 00000000000..b95ffe389a1 --- /dev/null +++ b/plugins/source-control/skills/commit/reference/design-rationale.md @@ -0,0 +1,45 @@ +# Commit skill: sources and design rationale + +Detail behind [SKILL.md](../SKILL.md): where the type set comes from, the mod that can change +injected commit guidance, the trailer key spelling, and the recorded frontmatter deviation. None of +it changes a step; read it when a rule here is questioned. + +## Where the 11 types come from + +The 11 types. `build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test`. Come from +the Conventional Commits spec, the Angular convention, commitlint's `@commitlint/config-conventional` +source, and `amannn/action-semantic-pull-request`'s default `types` list; all four agree on this exact +set. `security` is **not** a Conventional Commits type in any of them, never add it. + +## A mod can change the injected commit guidance + +Behind "Which authority wins", source 3. A mod can change the commit attribution guidance before +you read it, so absent or unusual injected guidance says nothing about the project's wishes; +sources 1 and 2 still decide. + +- **Pointer**: the `attribution.text` row of the + [mods reference, "Prompts and what Claude reads"](https://code.claude.com/docs/en/plugins/mods/reference#prompts-and-what-claude-reads) +- **As of**: 2026-11-03 +- **Recheck trigger**: that row changes or is removed + +## Key spelling + +This skill emits `Co-authored-by`, the spelling GitHub's co-author documentation uses, so branch +commits and forge-written squash merges agree. It is a consistency choice. The skill never rewrites +the spelling already in history. A consumer who wants a different spelling sets it as a +`trailer_policy` template. + +- **Pointer**: GitHub's + [Creating co-authored commits on the command line](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors#creating-co-authored-commits-on-the-command-line) +- **As of**: 2026-11-03 +- **Recheck trigger**: that section changes the trailer key it shows + +## Recorded deviations + +- **Model invocation stays enabled** (`disable-model-invocation: false`, declared explicitly rather + than left to the default). The fleet archetype for a `/source-control:commit`-shaped skill is + `disable-model-invocation: true`. User-invoked only. This skill deliberately deviates because its + composition design requires the model to reach it: a workflow skill that composes the commit + mechanic cannot do so if the model may not load it. Compensating controls: surgical staging (never + `git add -A`), the subject pre-check before any git invocation, no hook bypass, and a + user-visible SHA + subject report after every commit. diff --git a/plugins/source-control/skills/commit/reference/native-commit.md b/plugins/source-control/skills/commit/reference/native-commit.md index 0caa19f1a99..3ededd31f31 100644 --- a/plugins/source-control/skills/commit/reference/native-commit.md +++ b/plugins/source-control/skills/commit/reference/native-commit.md @@ -4,6 +4,20 @@ Detail behind the `## Boundary` section in [SKILL.md](../SKILL.md). Each row is record: the claim, the basis it rests on, the date it was checked, and the event that makes it worth checking again. +## The three surfaces + +- **`commit` (bundled skill)**: a generic commit workflow; it gathers git context and applies + Claude Code's own message style, staging rules, and attribution. The model and the person can + both invoke it. +- **`/commit-push-pr` (built-in command)**: commits, pushes, and opens a PR in one prompt-driven + step. The model and the person can both invoke it. +- **This skill (marketplace plugin).** Resolves the repository's subject convention through the + ladder in SKILL.md (layered `source-control.md` config, the project's own convention, the + Conventional Commits default), resolves `trailer_policy`, stages surgically against the four + preconditions, and reruns the format, exec-bit, and subject pre-check steps on every commit. + +## Verification record + | Claim | Basis | As of | Recheck when | |---|---|---|---| | `commit` is a bundled skill that creates a git commit through Claude Code's own generic commit workflow, taking optional guidance as its argument | The `/harness-ops:inventory` extraction of the installed 2.1.284 binary, 2026-09-29 | 2026-09-29 | A release renames or removes the skill, or changes its description |