| name | ring:committing-changes |
| description | Commit changes with scope allowlist enforcement, atomic grouping, GPG-signed conventional commits, and trailer management. Detects the repo's PR-validation scope policy before proposing any message. Use when the user asks to commit or has changes ready to record. Skip when the working tree is clean or the user wants raw git commands without grouping. |
| allowed-tools | ["Bash","Read","Glob","Grep","AskUserQuestion"] |
Analyze changes, enforce scope policy, group them into coherent atomic commits, and create signed commits following repository conventions. This skill transforms a messy working directory into a clean, logical commit history โ with a scope that will actually pass PR validation.
โ HARD STOP โ READ SCOPE POLICY BEFORE ANYTHING ELSE
The scope is REQUIRED in every commit message. It MUST come from the repo's allowlist.
MUST detect the allowlist in Step 0 before analyzing or drafting any commit message. A commit with an invented or omitted scope will fail PR validation and block the PR.
Step 0 โ Detect Scope Policy
Many repos enforce an allowlist of valid scope values via a GitHub Actions workflow. Failing this check blocks the PR, so MUST detect it before proposing any commit message.
0.1 โ Locate the policy file
Check in this order:
.github/workflows/pr-validation.yml (primary)
.github/workflows/pr-title.yml
.github/workflows/commitlint.yml
.github/workflows/semantic-pull-request.yml
- Root configs:
commitlint.config.{js,cjs,mjs,ts}, .commitlintrc*
0.2 โ Extract the allowed scope list
Common forms to look for:
| Form | Example |
|---|
scopes: block (one per line) | Under amannn/action-semantic-pull-request |
scopes: a,b,c inline | Comma-separated on one line |
scope-enum rule | In commitlint config arrays |
Also note any type restrictions โ some repos limit types beyond the default Conventional Commits set.
0.3 โ Apply the policy
| Situation | Required Action |
|---|
| Policy found, scope is clear | Use only scopes from the allowlist |
| Policy found, scope is ambiguous | STOP and ask the user which allowed scope to use |
| No policy file found | MUST still include a scope โ ask the user what scope to use |
NEVER omit the scope. NEVER invent a scope not in the allowlist. A bare type: description is FORBIDDEN.
State the policy source and chosen scope to the user before proceeding.
Step 1 โ Gather Context
Run in parallel:
git status
git diff
git diff --cached
git log --oneline -10
Step 2 โ Analyze and Group Changes
For each changed file determine:
- Type:
feat, fix, chore, docs, refactor, test, style, perf, ci, build
- Scope: from the allowlist resolved in Step 0
- Logical group: what other files belong with this change?
Grouping Principles
| Principle | Description |
|---|
| Feature + Tests | Implementation and its tests go together |
| Config Changes | package.json, tsconfig, etc. grouped separately |
| Documentation | README, docs/ changes grouped together |
| Refactoring | Pure refactors (no behavior change) separate |
| Bug Fixes | Each fix is atomic with its test |
Single vs Multiple Commits
Single commit when:
- All changes belong to one coherent feature/fix
- User provides a specific message via argument
- Changes are minimal and related
Multiple commits when:
- Changes span different concerns (feature + docs + deps)
- Mix of features, fixes, and chores
- Better git history benefits future archaeology
Step 3 โ Determine Commit Order
Order matters for bisectability:
- Dependencies first โ so subsequent commits can use them
- Core changes โ implementation before consumers
- Tests with implementation โ keep them atomic
- Documentation last โ documents the final state
Step 4 โ Present Plan and Confirm
MUST get user confirmation before executing.
Proposed Commit Plan:
โโโโโโโโโโโโโโโโโโโโโ
Scope policy: .github/workflows/pr-validation.yml โ allowed scopes: [api, auth, docs, ci]
Chosen scope: auth
1. feat(auth): add OAuth2 refresh token support
- src/auth/oauth.ts (modified)
- src/auth/oauth.test.ts (modified)
2. chore(deps): update authentication dependencies
- package.json (modified)
- package-lock.json (modified)
3. docs(docs): update OAuth2 setup guide
- docs/auth/oauth-setup.md (modified)
Proceed with this plan? [Execute plan / Single commit / Let me review]
Use AskUserQuestion to confirm before proceeding.
Step 5 โ Draft Commit Messages
Every commit message MUST follow:
<type>(<scope>): <subject>
<body โ optional>
- Subject: max 50 characters, imperative mood ("add" not "added")
- Body: wrap at 72 characters, explain motivation/context
- Scope: REQUIRED, from the allowlist โ NEVER omit, NEVER invent
Step 6 โ Execute Commits
โ HARD STOP โ TRAILER RULES
THE MOST COMMON MISTAKE: Putting trailer text INSIDE the -m quotes.
git commit -m "feat(auth): add feature
X-Lerian-Ref: 0x1"
git commit -m "feat(auth): add feature" --trailer "X-Lerian-Ref: 0x1"
Before writing ANY git commit command, verify:
Required Command Structure
git commit -S \
-m "<type>(<scope>): <subject>" \
-m "<body if needed>" \
--trailer "X-Lerian-Ref: 0x1"
For each commit group, in order:
-
Stage only the files for this commit:
git add <file1> <file2> ...
-
Create signed commit with trailer:
git commit -S \
-m "<type>(<scope>): <subject>" \
-m "<body if needed>" \
--trailer "X-Lerian-Ref: 0x1"
If GPG signing fails: check git config user.signingkey and gpg --list-secret-keys.
If no usable key is found, STOP โ do NOT offer an unsigned path. Inform the user:
GPG signing is required. No usable signing key was found.
To proceed:
1. Generate a key: gpg --gen-key
2. Configure git: git config --global user.signingkey <key-id>
3. Re-run this skill.
Committing without -S is not an option โ Step 7 will reject unsigned commits.
MUST wait for the user to configure a key before continuing. NEVER drop -S silently or offer "unsigned" as a fallback.
- Repeat for each commit group.
Step 7 โ Verify Commits
First, resolve the range ref for verification. $BASE may be provided by an orchestrating skill (e.g., ring:shipping-changes). Resolve in this order:
if git rev-parse @{u} >/dev/null 2>&1; then
RANGE_REF="@{u}"
elif [ -n "$BASE" ]; then
RANGE_REF="origin/$BASE"
else
BASE=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null \
|| git remote show origin 2>/dev/null | grep 'HEAD branch' | awk '{print $NF}')
RANGE_REF="origin/$BASE"
fi
Then verify every commit in the batch:
git log --oneline "$RANGE_REF..HEAD"
for commit in $(git rev-list "$RANGE_REF..HEAD"); do
sig_status=$(git log -1 --format="%G?" "$commit")
echo "$sig_status" | grep -qE '^[GU]' \
|| { echo "Commit $commit: signature invalid or insufficient (status=$sig_status)"; exit 1; }
git log -1 --format="%(trailers)" "$commit" | grep -q '^X-Lerian-Ref: ' \
|| { echo "Commit $commit: X-Lerian-Ref trailer missing"; exit 1; }
done
git status
For each commit:
- Accept
G (good) or U (unknown validity). Reject X/Y (expired key), B (bad signature), E (missing key), N (unsigned).
- If the trailer
grep fails โ stop and report the missing trailer.
Why U is accepted: U means the commit is cryptographically signed with a valid key, but GPG has not established a trust chain for that key (e.g., the key was not signed by a trusted introducer). This is the normal state for freshly generated keys or keys imported from colleagues without manual trust assignment. The signature itself is valid โ it proves authorship. G additionally requires GPG's web-of-trust to vouch for the key identity, which is stricter than needed for commit attribution. Both are acceptable; only unsigned (N), bad (B), missing-key (E), and expired-key (X/Y) commits are rejected.
Note: when called from ring:shipping-changes, $BASE is already resolved in Phase 0 and propagated here โ the @{u} and standalone detection paths are only needed for standalone invocations.
Step 8 โ Offer Push
After successful commit, ask the user:
AskUserQuestion({
questions: [{
question: "Push commits to remote?",
header: "Push",
options: [
{ label: "Yes", description: "Push to current branch" },
{ label: "No", description: "Keep local only" }
]
}]
});
If yes:
git push
git push -u origin <current-branch>
Examples
Feature commit
git commit -S \
-m "feat(auth): add OAuth2 refresh token support" \
-m "Implements automatic token refresh when access token expires." \
--trailer "X-Lerian-Ref: 0x1"
Bug fix
git commit -S \
-m "fix(api): handle null response in user endpoint" \
--trailer "X-Lerian-Ref: 0x1"
Chore
git commit -S \
-m "chore(deps): update dependencies to latest versions" \
--trailer "X-Lerian-Ref: 0x1"
Anti-Patterns (FORBIDDEN)
git commit -m "feat: add feature"
git commit -m "feat(custom-scope): add feature"
git commit -m "feat(auth): add feature
X-Lerian-Ref: 0x1"
git commit -m "feat(auth): add feature
๐ค Generated with Claude"
git commit -S \
-m "feat(auth): add feature" \
--trailer "X-Lerian-Ref: 0x1"
Trailer Query Commands
git log --all --format="%H %s %(trailers:key=X-Lerian-Ref,valueonly)" | grep "0x1"
git log -1 --format="%(trailers)"
When User Provides Message
If the user provides a commit message as an argument:
- Use it as the subject/body
- Validate it has a scope from the allowlist โ if missing, ask which scope to use
- Create signed commit with trailer
Anti-Rationalization Table
| Rationalization | Why It's WRONG | Required Action |
|---|
| "I'll omit the scope for this one" | Every commit MUST carry a scope. A bare type: description fails PR validation. | MUST include scope from allowlist |
| "This scope isn't in the allowlist but it makes sense" | Invented scopes fail automated checks. The allowlist exists for a reason. | MUST use only allowlist scopes or ask user |
| "No policy file, so scope is optional" | Scope is always required. Without a policy, ask the user which scope to use. | MUST ask user for scope if no policy found |
| "I'll commit everything at once" | Mixed changes = messy history, hard to bisect/revert. | Analyze and group changes first |
| "Grouping takes too long" | Clean history saves hours of debugging later. | Always propose commit plan |
| "I'll put the trailer text in the message body" | --trailer is a GIT FLAG, not message text. | Use --trailer "X-Lerian-Ref: 0x1" as separate argument |
| "I'll skip GPG signing" | Unsigned commits fail Step 7 verification. There is no unsigned fallback path โ configure a key and retry. | MUST stop and instruct user to configure GPG key. NEVER drop -S |
| "HEREDOC will format trailers correctly" | HEREDOC puts everything in the message body. | Use --trailer flag, NOT HEREDOC |