ai: Implement commit discipline framework
Adds a new skill that enforces disciplined commit practices: contextual
messages documenting why changes were made (not just what changed), batching
verification, and preventing rationalizations that lead to lazy commits.
Updates the /commit command to invoke this skill before committing, ensuring
all commits follow the discipline framework.
Assisted-by: Claude Haiku 4.5 via Claude Code
diff --git a/ai/AI.md b/ai/AI.md
index 1c67b939..3048078f 100644
--- a/ai/AI.md
+++ b/ai/AI.md
@@ -43,22 +43,27 @@ unverified claim without labeling it."
## Writing
-Use the elements-of-style writing skill for documentation, commit
-messages, PR descriptions, and user-facing text. Apply it before
-considering prose complete.
+Invoke the elements-of-style writing skill before drafting prose for humans:
+documentation, commit messages, PR descriptions, and user-facing text. Do not
+rationalize skipping it—apply proactively.
-Style rules: Avoid the "**bold**: explanation" pattern in lists. Use
-plain text or structured prose instead.
+Avoid the "**bold**: explanation" pattern in lists. Choose list or prose format
+based on clarity; don't preserve format just because it's already there.
+
+Use lists for distinct, independent items (comparable options, sequential steps,
+standalone points). Use prose paragraphs when explaining relationships between
+ideas, cause-and-effect, or when items depend on each other. Use tables for
+multi-column data.
Examples to avoid:
- **Option A**: explanation text here
- **Foo**: description follows
-Acceptable alternatives:
-- Use plain list items without bold labels
-- Use prose paragraphs for explanations
-- Use tables for multi-column comparisons
-- Bold text is acceptable in headers and inline emphasis
+Use instead:
+- Plain list items without bold labels for distinct options
+- Prose paragraphs when choices relate to each other
+- Tables for multi-column comparisons
+- Bold only in headers and inline emphasis
## Documentation Grounding
diff --git a/ai/claude/commands/commit.md b/ai/claude/commands/commit.md
index 7ec7d6be..cbda1d83 100644
--- a/ai/claude/commands/commit.md
+++ b/ai/claude/commands/commit.md
@@ -1,34 +1,18 @@
# Commit Changes
-Commit changes using jj. Arguments can specify a fileset to include in
-the commit.
+Invoke the `committing-with-discipline` skill before committing. This
+command helps you create commits with proper context, verification, and
+discipline.
Usage:
- `/commit` - Commit all current changes
-- `/commit <fileset>` - Commit only files matching the fileset
- expression
+- `/commit <fileset>` - Commit only files matching the fileset expression
-When committing:
-1. Check if a fileset was provided as arguments
-2. If a fileset is provided, use `jj commit <fileset>` to commit only
+Before committing:
+1. Use the `committing-with-discipline` skill to ensure proper discipline
+2. Check if a fileset was provided as arguments
+3. If a fileset is provided, use `jj commit <fileset>` to commit only
matching files
-3. If no fileset is provided, use `jj commit` to commit all changes
-4. Use any available writing skills for assistance with commit message style
-and conventions (e.g., elements-of-style)
-5. Draft a clear, concise commit message:
- - Start with a single sentence that captures the change's purpose
- - Focus on why the change was made, not what changed (the diff shows
- what changed)
- - CRITICAL: Do NOT include information that is obvious from reading
- the diff. The reviewer has the diff; your message should provide
- context and reasoning that cannot be inferred from the code changes
- alone
- - Only use a bulleted list if multiple distinct reasons or changes
- require explanation; otherwise keep the message to one paragraph
- - Avoid describing file changes, added/removed lines, or renamed
- variables unless the reason for these changes is non-obvious
- - Follow repository conventions
-6. Include the "Assisted-by" footer with the current model and tool
-being used
-7. Use a HEREDOC for the commit message to ensure proper formatting
-8. Run `jj log` after committing to verify success
+4. If no fileset is provided, use `jj commit` to commit all changes
+5. Include the "Assisted-by" footer with the current model and tool being used
+6. Use a HEREDOC for the commit message to ensure proper formatting
diff --git a/ai/claude/skills/committing-with-discipline/SKILL.md b/ai/claude/skills/committing-with-discipline/SKILL.md
new file mode 100644
index 00000000..6a0cb699
--- /dev/null
+++ b/ai/claude/skills/committing-with-discipline/SKILL.md
@@ -0,0 +1,257 @@
+---
+name: committing-with-discipline
+description: |
+ Use when creating or reviewing commits, before using git/jj to commit -
+ enforces contextual commit messages that document reasoning (not just what
+ changed) and verifies commits contain exactly intended changes
+---
+
+# Committing with Discipline
+
+## Overview
+
+A commit message should document *why* a change was made, not *what* changed.
+The diff already shows what. Your message adds the context, reasoning, and
+decisions the code alone cannot convey. This skill prevents the
+rationalizations that lead to lazy commits: vague messages, batched unrelated
+changes, skipped verification, and stripped-down messages for "obvious"
+changes.
+
+## When to Use
+
+- Before running `jj commit` or `git commit`
+- When reviewing a commit before pushing
+- When tempted to batch unrelated changes
+- When the change feels "obvious" and doesn't need explanation
+- Before closing the work - verification is part of discipline
+
+## Core Principle
+
+**Commit messages are for future readers, not current writers.**
+
+A change that's obvious to you now won't be obvious:
+- To future you (in 6 months, what was the tradeoff?)
+- To someone reviewing the codebase
+- During debugging when you need to understand *why* a line exists
+
+## Red Flags - STOP and Rethink
+
+If you catch yourself thinking any of these, stop and apply the discipline:
+
+- "This is obvious/self-explanatory" → If obvious, explain why it's the right approach
+- "It's just cleanup" → Cleanup of unrelated code should be separate commit(s)
+- "I'll remember this later" → You won't. The commit message is for later
+- "It's blocking other work" → Blocking is a reason to commit, not to batch unrelated changes
+- "It's a quick fix" → Quick doesn't exempt you from explaining it
+- "No one else works on this" → Lower standards = unmaintainable code later
+- "I can amend later" → Amending forces history rewrites. Do it right the first time
+- "The diff is small/obvious" → Document the *decision*, not the size
+
+All of these mean: Stop. Follow the discipline. Your future self will thank you.
+
+## The Discipline
+
+### 1. Check What You're Committing (MANDATORY)
+
+```bash
+# Before ANY commit, see exactly what will be committed
+jj diff # See all changes
+jj status # See file list and states
+```
+
+**The question:** Do all these files belong together in one logical change?
+
+**If no:** Create separate commits. Batching different logical changes is never justified.
+
+**Reality check:** If you can't explain why these files changed together in 1-2
+sentences, they don't belong in the same commit.
+
+**No exceptions:**
+- Not even if "they're both quick changes"
+- Not even if "one builds on the other"
+- Not even if "I'm under time pressure"
+- Not even if "the files are related"
+
+Different logical changes = different commits. Period.
+
+### 2. Understand the Context
+
+Before writing your message, answer these questions:
+
+- What problem does this change solve?
+- Why is this the right approach (vs alternatives)?
+- Are there relevant decisions, tradeoffs, or constraints?
+- Will there be questions when reviewing this commit in 6 months?
+
+**Write these answers down as your message draft.** Don't trust your memory.
+
+### 3. Apply Writing Discipline (MANDATORY)
+
+**Before drafting any commit message, use the `elements-of-style:writing-clearly-and-concisely`
+skill.** This is not optional. Commit messages are prose for humans; the skill
+applies unconditionally.
+
+Key principles from Strunk for commit messages:
+
+- Use active voice (Rule 10): "Fixed X" not "X was fixed"
+- Put statements in positive form (Rule 11): State what you did, not what was wrong
+- Use definite, specific language (Rule 12): Avoid vague descriptions
+- Omit needless words (Rule 13): Strip anything visible in the diff
+- Place emphatic words at the end (Rule 18): Key reasoning should be prominent
+
+### 4. Draft the Message
+
+The diff shows what changed. Your message explains why:
+
+**Structure:**
+- **Opening line:** Succinct imperative + why (5-10 words). Must stand alone.
+- **Reasoning (if needed):** Why this approach? What alternatives were
+ considered? What constraints apply?
+- **Gotchas (if any):** Edge cases, known limitations, follow-up work needed
+
+**CRITICAL:** Do NOT include anything visible in the diff. Strip ruthlessly.
+
+**Test for bad messages:** If you could write your commit message after reading
+only the diff (without context), you are describing changes. Rewrite it to
+explain reasoning instead.
+
+**Bad example - describing the diff:**
+```
+Updated config file to use new API endpoint in lib/config.js
+Changed ENDPOINT_URL constant from v1 to v2
+Modified 3 lines: added new constant, updated old constant, added comment
+```
+
+(All visible in the diff. Reader sees what changed without needing your message.)
+
+**Bad example - listing what the code does:**
+```
+Added caching layer to user service
+
+Adds cache initialization, TTL configuration, and invalidation logic.
+Cache stores user objects by ID with 5-minute expiry.
+```
+
+(The diff shows the code. What's missing: why add caching now?)
+
+**Better - explaining the reasoning:**
+```
+Use API v2 endpoint to support new authentication requirements
+
+API v1 doesn't support OAuth scopes needed for permission boundaries.
+v2 adds automatic retry logic, eliminating our previous custom retry handler.
+Tested against staging environment (see #1234).
+```
+
+Message explains the business/technical decision, not the code itself.
+
+### 5. Add Metadata Footer
+
+Include attribution for assisted commits:
+
+```
+Assisted-by: Claude <model> via Claude Code
+```
+
+Use a HEREDOC for proper formatting:
+
+```bash
+jj commit -m "$(cat <<'EOF'
+Your message here.
+
+Assisted-by: Claude Sonnet 4.5 via Claude Code
+EOF
+)"
+```
+
+### 6. Verify the Commit (MANDATORY)
+
+After committing, verify it contains exactly what you intended:
+
+```bash
+jj log --limit 1 # See the commit message
+jj show # See the full diff of what you just committed
+```
+
+**The question:** Does this commit contain exactly (and only) what you intended?
+
+**If no:** You made a mistake. The commit is saved, but you can:
+- Amend it: `jj describe -r @` (if not yet pushed)
+- Or create a follow-up commit fixing the issue
+
+**Verify every single commit. No exceptions:**
+- Not even if you're confident it's correct
+- Not even if the change is small
+- Not even if you're in a hurry
+- Verification finds mistakes. If you skip verification, you will push mistakes.
+
+**This is not optional.** Every. Single. Time.
+
+## Common Mistakes
+
+| Mistake | Why It Matters | Fix |
+|---------|----------------|-----|
+| Message describes files, not changes | Reader is lost about intent | Explain the *why* not the *what* |
+| Batching unrelated changes | Future debugging is confused | One logical change per commit |
+| "Quick fix" messages ("typo", "cleanup") | No context for future readers | Explain why, even if quick |
+| Skipping verification | Wrong files committed (or secrets leaked) | Always run `jj show` after committing |
+| Including obvious details | Wastes reader time | Trust the diff; add only non-obvious context |
+| Amending commits after push | Breaks history for others | Do it right the first time |
+
+## Example: Discipline in Action
+
+### Scenario
+You've fixed a caching bug in src/services/cache.ts. It was a simple one-line change.
+
+### The Temptation
+"One line change, obvious fix, let's commit: 'fix cache bug'"
+
+### The Discipline
+1. **Verify:** `jj status` → only cache.ts modified ✓
+2. **Understand:** Why did this bug exist? What was the wrong behavior? How does the fix work?
+3. **Write context:** "TTL wasn't being compared correctly. The bug was
+ comparing timestamp (number) to TTL duration (also number), confusing
+ milliseconds with seconds. Fixed by normalizing to milliseconds before
+ comparison."
+4. **Draft message:**
+ ```
+ Fix cache TTL comparison by normalizing to milliseconds
+
+ Cache entries were expiring prematurely because TTL duration and
+ timestamp were compared without unit conversion. Both are numbers
+ but TTL is in seconds (from config) while timestamp is in
+ milliseconds (Date.now()).
+
+ Now both are normalized to milliseconds before comparison.
+ ```
+5. **Verify:** `jj show` → confirms only the TTL comparison line changed ✓
+
+**Result:** Future debugger sees not just the fix, but *why* it was needed. No
+"why did they do this?" comments in code reviews.
+
+## Implementation
+
+When using this skill:
+1. Before ANY commit, run `jj diff` and `jj status`
+2. Verify all files in the commit belong together logically
+3. Answer the "why" questions before writing the message
+4. Draft your message explaining the *decision*, not the *diff*
+5. Use HEREDOC for formatting + attribution
+6. After committing, run `jj show` to verify exactly what was committed
+
+## Rationalizations to Close
+
+| Rationalization | Counter |
+|-----------------|---------|
+| "The diff is small, doesn't need explanation" | Small changes still need context. "Why is this line here?" matters more when code is simple |
+| "This is obvious to anyone reading the code" | You're wrong. Commit message is for future-you who forgot, and reviewers missing context |
+| "I don't have time for a real message" | You don't have time to debug this later when the context is gone |
+| "I'll fix the message later with amend" | Amend after push breaks history. Do it right now. Amend only before pushing |
+| "No one else works on this file" | Lower standards = abandoned codebase. Apply discipline to everything |
+| "This file changed, let me just include it" | Include it only if it belongs with this logical change. When in doubt, separate commits |
+| "I already verified it manually" | Manual verification is error-prone. Run `jj show`. No shortcuts |
+| "The message can be figured out from the diff" | The *what* can be figured out. The *why* cannot. Message documents decisions |
+| "These are both quick changes, I'll batch them" | Quick ≠ related. Different logical changes need different commits |
+| "I'll remember why I did this" | You won't. Write it down in the commit message now |
+| "The message format doesn't matter much" | Format matters less than context, but use HEREDOC for clean formatting |
+| "I've already reviewed the code, skip jj show" | Reviewing code ≠ verifying commit contents. Run `jj show` every time |