From c0a5969cabfdfc0c1d692855b2b09022f80d78d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 21:01:10 +0000 Subject: [PATCH 1/7] agent(skills): rewrite commit-craft for agents, with a checker Commit messages here come from Claude Code, Crush and Codex, and each harness ships its own default for length and trailers. commit-craft was Chris Beams' seven rules written for a person, with generic examples. It triggered only when someone asked for a message, not when an agent committed on its own, and its last step pushed straight to main. This is the same skill as on develop (c42e3bf on the claude/fervent-feynman-sfduqg branch), at main's path. It defines one format, `(): ` with a required body in a fixed order (why, what and how, verification, what is left), and a procedure that ends with `jj describe --stdin`. scripts/check-message enforces the mechanical rules: subject shape, case, length and tense, the blank line, a body, and a 72-column wrap. An agent runs it before it sets a message, so a model that knows the rules less well still produces the same shape. The only change from develop is the line that says where the skill sits: here, tools/agents/skills, deployed whole to ~/.agents/skills by agent-skills.nix, whose comment now describes it. Tested in a git-only sandbox (no nix, no jj): develop's bats suite for the checker passes against this copy (10 tests), shellcheck 0.11 passes, and check-skill-frontmatter.sh accepts the skill (492 bytes). main has no bats or shellcheck job, so CI does not run the checker's tests here. Not evaluated with nix; the .nix change is a comment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Soievfadhy2k17VT4VecnT --- home-manager/modules/tools/agent-skills.nix | 3 +- tools/agents/skills/commit-craft/SKILL.md | 440 ++++++------------ .../commit-craft/references/examples.md | 94 ++++ .../skills/commit-craft/scripts/check-message | 71 +++ 4 files changed, 308 insertions(+), 300 deletions(-) create mode 100644 tools/agents/skills/commit-craft/references/examples.md create mode 100755 tools/agents/skills/commit-craft/scripts/check-message diff --git a/home-manager/modules/tools/agent-skills.nix b/home-manager/modules/tools/agent-skills.nix index 9855cef0..7e0a19ba 100644 --- a/home-manager/modules/tools/agent-skills.nix +++ b/home-manager/modules/tools/agent-skills.nix @@ -35,7 +35,8 @@ # Repo skills: # brag-doc - promo-packet impact entries from raw work notes # cf-now - private file sharing from Cloudflare R2 (pre-signed URLs) -# commit-craft - commit-message craft + jj describe/push workflow +# commit-craft - the commit message format, with a checker script +# (scripts/check-message) every message must pass # entity-level-git - Ataraxy Labs stack: sem (entity diff/blame/impact), # weave (semantic merge driver), inspect (review triage) # failure-doc - failures as deliberate-learning records diff --git a/tools/agents/skills/commit-craft/SKILL.md b/tools/agents/skills/commit-craft/SKILL.md index 475c611a..d70af03a 100644 --- a/tools/agents/skills/commit-craft/SKILL.md +++ b/tools/agents/skills/commit-craft/SKILL.md @@ -1,336 +1,178 @@ --- name: commit-craft -description: Write high-quality git commit messages following established conventions. Use when the user asks to write, review, improve, or critique commit messages, wants to prepare commits for pushing to git from jujutsu, or needs guidance on commit message structure and content. Also use when asked about commit message best practices or the seven rules. +description: > + The commit message format for Alyssa's repositories. Use every time you + write or change a commit message: before `jj describe`, `jj new -m`, + `jj commit`, `jj squash -m`, `git commit` or `git commit --amend`, when + asked to "commit this", and when reviewing or rewording existing messages. + Applies in any harness (Claude Code, Crush, Codex). Has a checker script + that every message must pass before it is set. The jujutsu skill covers + the VCS commands; this skill covers what the message says. --- # Commit Craft -Write clear, professional commit messages that follow established conventions from Chris Beams' "How to Write a Git Commit Message." +This skill defines one commit message format. Follow it exactly. The goal is +that every commit reads the same no matter which agent or model wrote it. -## The Seven Rules +## Which convention wins -### 1. Separate subject from body with a blank line +1. A convention the repository documents (CONTRIBUTING, CLAUDE.md, AGENTS.md, + a commitlint config) wins. +2. If there is none, but `git log --format=%s -30` shows a different consistent + style, follow that history. +3. Otherwise, and always in the dotfiles repository, use this skill. -If your commit needs explanation beyond the subject line, add a blank line after the subject, then write the body. +## Procedure -**Why:** Many git tools expect this format. `git log --oneline`, `git shortlog`, and email-based workflows all rely on this separation. +Do these steps in order for every commit. -``` -Add caching layer to user authentication - -The previous implementation made a database call on every -request to verify user credentials. This change introduces -a Redis-backed cache with a 5-minute TTL to reduce database -load during high-traffic periods. -``` - -**When to skip the body:** Simple, self-explanatory changes don't need additional context. - -``` -Fix typo in README -``` - ---- - -### 2. Limit the subject line to 50 characters - -Keep subjects concise. Aim for 50 characters; treat 72 as a hard limit. - -**Why:** GitHub truncates subjects longer than 72 characters. The 50-character guideline ensures readability in all contexts. - -**How to achieve this:** -- Focus on the essential action and target -- Remove unnecessary words ("this commit," "I have") -- Use abbreviations where standard (HTTP, API, DB) - -**Too long:** -``` -Update the user authentication module to use bcrypt instead of the old MD5 hashing algorithm -``` - -**Better:** -``` -Replace MD5 with bcrypt for password hashing -``` - ---- - -### 3. Capitalize the subject line - -Start the subject with a capital letter. - -**Why:** Basic grammar convention that maintains consistency across the project. - -**Wrong:** -``` -fix authentication bug in login handler -``` - -**Right:** -``` -Fix authentication bug in login handler -``` - ---- - -### 4. Do not end the subject line with a period - -Omit trailing punctuation from the subject. - -**Why:** Subject lines are titles, not sentences. The period wastes precious character space. - -**Wrong:** -``` -Add rate limiting to API endpoints. -``` - -**Right:** -``` -Add rate limiting to API endpoints -``` - ---- - -### 5. Use the imperative mood in the subject line - -Write subjects as commands: "Fix bug" not "Fixed bug" or "Fixes bug." - -**Why:** Git itself uses imperative mood (e.g., "Merge branch" not "Merged branch"). This convention reads as "If applied, this commit will [your subject line]." - -**Test:** Your subject should complete the sentence: "If applied, this commit will..." - -**Wrong:** -``` -Fixed the authentication timeout issue -Added support for OAuth2 -Updating dependencies to latest versions -``` - -**Right:** -``` -Fix authentication timeout issue -Add support for OAuth2 -Update dependencies to latest versions -``` - -**Common imperative verbs:** -- Add, Remove, Fix, Update, Refactor -- Implement, Extract, Merge, Revert -- Optimize, Document, Deprecate - ---- - -### 6. Wrap the body at 72 characters - -Hard-wrap body text at 72 characters. - -**Why:** Git never wraps text automatically. The 72-character limit ensures proper display in terminals and UI tools with padding. - -**How:** Configure your editor to wrap at 72 characters, or manually add line breaks. - ---- - -### 7. Use the body to explain what and why, not how +1. Read the full change: `jj diff --git` (or `git diff --staged`). Read the + task or conversation that caused it. You need to know *why*, not only + *what*. +2. If the change does two unrelated things, split it first (see the jujutsu + skill). A subject that needs "and" is a sign of two commits. +3. Pick the area and scope (below). +4. Write the message to a file outside the repository (your scratchpad, or + `$TMPDIR`). Never write it inside the working copy: jj snapshots it. +5. Run the checker on the file and fix every problem it prints. Repeat until + it prints nothing: -The body should explain: -- **What** changed (if not obvious from the diff) -- **Why** the change was necessary -- **What** problem it solves or requirement it fulfills - -The body should NOT explain: -- **How** you implemented the change (the diff shows this) - -**Example:** - -``` -Refactor user session management to use Redis - -Previous implementation stored sessions in PostgreSQL, causing -performance degradation when session count exceeded 10k. Under -load testing, login response times increased from 200ms to 3s. - -Redis provides O(1) lookups and automatic expiration, resolving -the performance issue. Load tests now show consistent 150ms -response times regardless of session count. - -This change maintains backward compatibility by preserving the -session schema. The migration script handles existing sessions. -``` - -**What makes this good:** -- Explains the problem (performance degradation) -- Provides context (specific metrics) -- Justifies the solution (Redis benefits) -- Notes important details (backward compatibility) - ---- - -## Workflow Integration - -### For jujutsu users preparing to push to git - -When you've completed work in jujutsu and are ready to create permanent git commits: - -1. **Review your jujutsu changes:** ```bash - jj log - jj diff + /scripts/check-message /path/to/msg.txt ``` -2. **Craft your commit message:** - - Write subject line (imperative, <50 chars) - - Add body if needed (what and why) - - Use `jj describe` to set the message: - ```bash - jj describe -m "Add user profile caching - - Reduces database queries by 60% during profile page - loads. Cache TTL set to 10 minutes based on analytics - showing most users refresh profiles within this window." - ``` + `` is the directory that holds this SKILL.md. Installed, it is + `~/.agents/skills/commit-craft`; in the dotfiles checkout it is + `tools/agents/skills/commit-craft`. +6. Set the message from the file: -3. **Verify before pushing:** ```bash - jj log # Check your commit message - jj bookmark set main # Point bookmark at commit - jj git push + jj describe --stdin < /path/to/msg.txt # jj + git commit -F /path/to/msg.txt # git only ``` ---- - -## Quick Reference +7. Check what was stored: + `jj log -r @ --no-graph -T description | /scripts/check-message`. + +## Subject line + +Format: `(): `. The `()` part is optional. + +```text +hm(zsh): compinit -i, instead of re-owning dirs in CI +ci(nix): secure the runner dirs compaudit flags +just: global justfile with the HUID task recipes +devcontainer(nix): apply home-manager on create +``` + +Hard rules (the checker enforces them): -**Subject line checklist:** -- [ ] Imperative mood ("Add feature" not "Added feature") -- [ ] Capitalized first letter -- [ ] Under 50 characters (72 hard limit) -- [ ] No period at the end -- [ ] Completes: "If applied, this commit will..." +- `area` is lowercase: letters, digits and `-`. +- One space after the colon. The summary starts with a lowercase letter, + unless its first word is a name that is always written in capitals + (`HUID`, `README`). +- 72 characters at most. Aim for 50 to 60. +- No period at the end. +- The summary says what the commit does or adds. Use an imperative verb + (`secure the runner dirs ...`) or a noun phrase for the thing added + (`global justfile with ...`). Do not use past tense (`fixed`, `added`), the + third person (`fixes`, `adds`), or "this commit". -**Body checklist (when needed):** -- [ ] Blank line after subject -- [ ] Wrapped at 72 characters -- [ ] Explains what and why, not how -- [ ] Provides context for the change -- [ ] Notes any important side effects or considerations +Choosing the area and scope: ---- - -## Common Patterns +- Reuse an area that already exists. List them with: -### Bug fixes -``` -Fix race condition in payment processing + ```bash + git log --no-merges --format=%s | + sed -nE 's/^([a-z0-9-]+(\([^)]*\))?):.*/\1/p' | sort | uniq -c | sort -rn + ``` -Under high load, concurrent requests could process the same -payment twice. Added transaction-level locking to ensure -payment atomicity. -``` +- The area is the part of the repository or the tool the change belongs to. + Examples from the dotfiles: `hm` (home-manager), `ci` (`.github/`), `mise`, + `nix`, `devcontainer`, `just`, `agent(skills)`, `docs`. +- The scope narrows the area: `ci(nix)` is the Nix workflow, `hm(zsh)` is + home-manager's zsh. Leave it out when the area is already narrow enough. +- Make a new area only for something new at the top level of the repository. -### New features -``` -Add export functionality to reports dashboard +## Body -Users can now export reports as CSV or PDF. Export happens -asynchronously with email notification upon completion to -handle large datasets without blocking the UI. -``` +A body is required. Wrap it at 72 characters. Indented lines (pasted output, +code) and lines that contain a URL can be longer. -### Refactoring -``` -Extract authentication logic into middleware +Write the paragraphs in this order. Skip a paragraph only when it has nothing +to say. -Authentication was duplicated across 15 route handlers. -Consolidating into middleware reduces code by 200 lines -and ensures consistent auth checks across all routes. -``` +1. **Why.** The problem or the need. Give the evidence: the error text, the CI + run, the task or issue. Say what was wrong before this commit, not what + you did about it. +2. **What and how.** What the commit changes, in terms of behavior. Say why + it is done this way when there was another choice, and name the choice you + did not take. Name files and settings when that helps a reader find them. + Do not walk through the diff line by line. +3. **Verification.** Start with "Tested in ..." or "Verified in ..." and name + the environment. Say what you ran and what you saw. Then list the checks + that pass (`shellcheck`, `nix flake check`, the bats suites). Also say what + you did *not* run, and why. Never claim a check you did not run. +4. **What is left.** Follow-up work, known limits, a task that tracks them, or + an instruction for the person who merges ("a fix for vmp, to squash or + rebase as you choose"). -### Performance improvements -``` -Optimize database queries in user listing +Language: -Replaced N+1 query pattern with eager loading. Page load -time reduced from 2.5s to 400ms for listings with 100+ users. -``` +- Plain words and short sentences. One idea per sentence. +- Past tense for what was wrong before. Present tense for what the code does + now. +- Refer to other commits by their jj change ID (the short prefix `jj log` + shows, for example `vmp`), because it survives rewrites. Use the git hash + only in a repository without jj. +- Use backticks for commands, file names and identifiers. Use a hyphen list + for parallel items. Do not use headings, bold or emoji. +- Do not write filler: "This commit", "comprehensive", "robust", + "seamlessly", "Additionally", "In order to". Do not praise the change. -### Breaking changes -``` -Remove deprecated OAuth1 support +## Trailers -OAuth1 has been deprecated since v2.0 (2023-06). All clients -have migrated to OAuth2. Removing legacy code reduces -maintenance burden and attack surface. +Trailers go in the last paragraph, one `Key: value` per line, after a blank +line. -BREAKING CHANGE: OAuth1 endpoints no longer available. -``` +- Keep the attribution trailers your harness tells you to add (for example + `Co-Authored-By:` and `Claude-Session:`). Do not invent others. +- Put attribution only in trailers. Never put a "Generated with ..." line or + an emoji banner in the body. +- `Closes #N` or `Refs #N` goes in the body's last paragraph, not in the + trailers, unless the repository uses a different convention. ---- - -## Additional Guidelines - -### Atomic commits - -Each commit should represent one logical change. If you're tempted to use "and" in your subject, you probably need multiple commits. - -**Too broad:** -``` -Add user profiles and fix login bug and update dependencies -``` - -**Better as separate commits:** -``` -Add user profile pages -Fix login redirect after password reset -Update authentication dependencies -``` - -### References - -Link to issue trackers, pull requests, or documentation when relevant: - -``` -Fix memory leak in background job processor - -Jobs weren't properly releasing database connections after -completion, causing connection pool exhaustion after ~6 hours -of operation. - -Fixes #1234 -See: docs/architecture/background-jobs.md -``` - -### Conventional Commits (optional extension) - -Some projects use conventional commits format: - -``` -(): - - -``` - -Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` - -``` -feat(auth): add two-factor authentication support - -Users can now enable 2FA using TOTP apps like Google -Authenticator. Recovery codes are generated and encrypted -at rest. -``` - -Only use this format if the project requires it. - ---- +## Example -## When to Use This Skill +A real commit from the dotfiles, wrapped at 72: -Use this skill when: -- Writing new commit messages -- Reviewing/improving existing commit messages -- Preparing jujutsu changes for git push -- Squashing commits and need consolidated message -- Creating pull request with clean commit history -- Getting feedback on commit message quality +```text +ci(nix): secure the runner dirs compaudit flags -The goal is professional, maintainable commit history that helps future developers (including yourself) understand the evolution of the codebase. +nix.yml's home job failed in run 36364995815: in the interactive zsh, +compinit found completion dirs it considers insecure (owned by, or +writable by, someone other than root and the user). With no terminal to +answer its "continue?" prompt it aborted, so the compinit check failed. +The devcontainer image has no such dirs, which is why the local run +passed. + +A step before the check now lists what compaudit flags and hands it to +root (chown root:root, chmod go-w), printing the list so the log shows +which of the runner's dirs they were. + +Reproduced in the devcontainer base image by giving another user +/usr/local/share/zsh: compinit aborted and the check failed as in CI; +after the step, all ten checks pass. + +Co-Authored-By: +``` + +More examples, with what each one does well: `references/examples.md`. + +## Rewording existing commits + +To fix a message on another commit, use +`jj describe -r --stdin < msg.txt`. Run the checker on it first. +Do not reword commits that are already on a remote branch someone else uses +unless you are asked to. diff --git a/tools/agents/skills/commit-craft/references/examples.md b/tools/agents/skills/commit-craft/references/examples.md new file mode 100644 index 00000000..d7fdda73 --- /dev/null +++ b/tools/agents/skills/commit-craft/references/examples.md @@ -0,0 +1,94 @@ +# Examples + +Real commits from the dotfiles repository, rewrapped at 72 characters and +with their trailers removed. Each one has a note on what to copy from it. + +## A fix to another change, with a note for the person who merges + +```text +ci(tasks): test the global HUID task recipes + +vmp moved `task` and `task-edit` out of the repo justfile into the +global one (tools/just) and their logic into tasks/scripts, but +huid-tasks.bats still copied the repo justfile and ran `just task`: 7 +of its 8 tests failed with no such recipe. + +The suite now runs tools/just/justfile the way `just -g` does (from the +test's temp dir), with tasks/scripts on PATH as mise links them and +HOME in a temp dir so no local.just joins in. Two tests cover what vmp +added: without a tasks/ dir a task goes to .tasks/, and TASKS_DIR +overrides both. + +A dangling fix for vmolorpo, to squash or rebase as you choose. All +bats suites (26) pass; shellcheck and editorconfig-checker too. +``` + +Copy: the first paragraph names the change that caused the problem by its +jj change ID (`vmp`) and gives the exact failure ("7 of its 8 tests +failed"). The last paragraph tells the reader what to do with the commit. + +## A noun-phrase subject, and an honest list of what was not run + +```text +jj-stash: vendor and install through mise + +tools/jj-stash holds jj-stash, its README and its three test scripts, +unchanged from alycda/jj at 1f1aafa. tools/jj-stash/UPSTREAM records +the commit. mise links the command into ~/.local/bin. + +It needs bash 4+, and mise's registry has no bash. On a Mac with no +Nix, `env bash` is /bin/bash 3.2 and the script fails; with nix-darwin +it resolves to the system bash 5.x, and the devcontainers have +Debian's. + +Tested in a lab copy of this repo through the mise-installed link: +push hides a three-commit chain, and list, index and the exclude rule +work. pop restores identical change and commit IDs and leaves no refs; +drop forgets a stash and prints the recovery `jj new`. The vendored +tests are pinned to a lab copy of ~/WIP/jj-dotfiles and were not run. +``` + +Copy: the verification paragraph says what was observed, command by +command, and ends with what was *not* run and why. The second paragraph is +a known limit, stated as a fact with the platforms it affects. + +## Behavior first, then verification that matches it + +```text +devcontainer(nix): apply home-manager on create + +The Nix devcontainer switches to the home-manager profile in +postCreateCommand ($USER set, since lifecycle commands leave it +unset). The Nix feature now only enables flakes; its jj/just package +list is gone, since the profile provides every tool. postAttachCommand +calls just by its profile path, because lifecycle commands get a plain +/bin/sh PATH. + +Tested with a fresh build through the devcontainer CLI: postCreate +activates the profile. postAttach reaches the recipe, which stops at +"no VS Code CLI" as expected without an attached VS Code. An +interactive zsh has all eleven tools, EDITOR=hx, `..`, revsets.fix=@, +cheat jj/fix and the shared git config: the same checks as the mise +container. +``` + +Copy: every "because" in the first paragraph explains a choice a reader +would otherwise question. Each claim in the first paragraph has a matching +check in the second. + +## What not to write + +```text +Updated agent skills. +``` + +The area is missing, the summary is capitalised, it is in the past tense, +it ends with a period, and there is no body. A reader learns nothing that +`jj diff --stat` would not show. + +```text +docs(agents): Instructions for Claude +``` + +The summary is capitalised and there is no body. Which instructions, and +why now? diff --git a/tools/agents/skills/commit-craft/scripts/check-message b/tools/agents/skills/commit-craft/scripts/check-message new file mode 100755 index 00000000..8bfe862e --- /dev/null +++ b/tools/agents/skills/commit-craft/scripts/check-message @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# check-message: check a commit message against the mechanical rules in +# commit-craft's SKILL.md. Reads FILE, or stdin when there is no argument. +# Prints one line per problem and exits 1; prints nothing and exits 0 when the +# message passes. It can't judge whether the body says the right things: that +# part is the skill's, not this script's. +# +# check-message msg.txt +# jj log -r @ --no-graph -T description | check-message +# git log -1 --format=%B | check-message +set -euo pipefail + +max=72 +# A first word that reports the past instead of saying what the commit does. +past='^(added|adds|fixed|fixes|updated|updates|removed|removes|changed|changes|moved|moves|renamed|refactored|implemented|created|deleted|this)$' +subject_re='^([a-z0-9][a-z0-9-]*)(\([^()]+\))?: (.+)$' +trailer_re='^[A-Za-z][A-Za-z0-9-]*: ' + +mapfile -t lines < <(cat -- "${1:-/dev/stdin}") +problems=() +problem() { problems+=("line $1: $2"); } + +# Drop trailing blank lines, which jj and git both add. +n=${#lines[@]} +while [ "$n" -gt 0 ] && [ -z "${lines[n - 1]//[[:space:]]/}" ]; do n=$((n - 1)); done + +if [ "$n" -eq 0 ]; then + echo "line 1: the message is empty" + exit 1 +fi + +subject="${lines[0]}" +if [[ $subject =~ $subject_re ]]; then + summary="${BASH_REMATCH[3]}" + first="${summary%% *}" + # "Add ..." is a capitalised word; "HUID ..." or "README ..." is a name. + [[ $first =~ ^[A-Z][a-z] ]] && problem 1 "the summary starts lowercase (\"${first,}\", not \"$first\")" + [[ ${first,,} =~ $past ]] && problem 1 "\"$first\": say what the commit does, not what happened" +else + problem 1 "the subject is (): , with a lowercase area and the scope optional" +fi +[ "${#subject}" -gt "$max" ] && problem 1 "the subject is ${#subject} characters, more than $max" +[[ $subject == *. ]] && problem 1 "the subject ends with a period" + +# The trailers are the last paragraph, when every line in it is Key: value. +end=$n +start=$n +while [ "$start" -gt 1 ] && [ -n "${lines[start - 1]}" ]; do start=$((start - 1)); done +if [ "$start" -gt 1 ]; then + all=1 + for ((i = start; i < n; i++)); do [[ ${lines[i]} =~ $trailer_re ]] || all=0; done + [ "$all" -eq 1 ] && end=$((start - 1)) +fi + +if [ "$n" -gt 1 ] && [ -n "${lines[1]}" ]; then + problem 2 "a blank line separates the subject from the body" +fi + +body=0 +for ((i = 1; i < end; i++)); do + line="${lines[i]}" + [ -n "$line" ] && body=1 + # Indented lines (pasted output, code) and URLs keep their length. + [[ $line == [[:space:]]* || $line == *://* ]] && continue + [ "${#line}" -gt "$max" ] && problem $((i + 1)) "${#line} characters, more than $max: wrap the body at $max" +done +[ "$body" -eq 0 ] && problem 2 "there is no body: say why, and how it was verified" + +[ "${#problems[@]}" -eq 0 ] && exit 0 +printf '%s\n' "${problems[@]}" +exit 1 From 5c1973e84f885ea5073f846a0e41c768c9f35834 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 21:01:20 +0000 Subject: [PATCH 2/7] agent(skills): one owner for the commit message format Four places described a commit message, and they disagreed. CLAUDE.md had a subject-plus-bulleted-body template. The jujutsu skill asked for sentence case and conventional `feat:` prefixes. ste100 named "the seven rules" and a 50-character subject as commit-craft's. commit-craft itself was the fourth. CLAUDE.md's Commit Message Format now points at commit-craft by path, so an agent that only reads the always-loaded file (Codex reads it through AGENTS.md) still finds the skill. It keeps two things specific to this repository: a body says what was learned when the change came out of a mistake, and because PRs are squash-merged, the PR title is the subject on main and the branch commits make up its body, so both follow the format. The jujutsu skill drops its format and keeps the commands: its `-m` rule gains `jj describe --stdin < file` for messages with a body, and the intent message from "describe first" is a subject line that the full message replaces before a push. version-notes says why agents use `--stdin` now. ste100's handoff names commit-craft's format and checker, and says to run the checker again after a rewording pass. Same change as 6bb1247 on develop's branch, applied to main's copies: the jujutsu skill here is still the 0.44 one, so its edits were made by hand rather than ported. Checked with rumdl, which main's CI does not run: CLAUDE.md and the jujutsu skill gain three warnings that match their existing style (bare code fences, the long Last updated line), and ste100 stays clean. check-skill-frontmatter.sh passes for all skills. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Soievfadhy2k17VT4VecnT --- CLAUDE.md | 35 ++++++++++--------- tools/agents/skills/jujutsu/SKILL.md | 23 ++++++++---- .../jujutsu/references/version-notes.md | 3 +- tools/agents/skills/ste100/SKILL.md | 9 ++--- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 188f0928..3acd9d52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,22 +87,24 @@ jj log -r '::' --limit 10 ### Commit Message Format -``` -Short imperative summary (50 chars or less) - -Longer explanation of the change and why it was made. Focus on: -- What problem does this solve? -- What alternatives were considered? -- What did you learn? -- What tradeoffs were made? - -Specific changes: -- Bullet points for key modifications -- File paths and what changed -- Configuration decisions - -[Optional: Link to resources or documentation] -``` +Every commit message follows the **commit-craft** skill +(`tools/agents/skills/commit-craft/SKILL.md`, installed at +`~/.agents/skills/commit-craft`). Read it before writing or changing any +message, in any harness, even when skills are not loaded for you +automatically. It defines the format, `(): ` plus a +body in a fixed order, and has a checker, `scripts/check-message`, that each +message must pass before it is set. The jujutsu skill covers only the +commands that set the message. + +Two additions for this repository: + +- When a change came out of a mistake or a dead end, the body's "what and + how" paragraph also says what was learned. That is the story the history + is meant to tell. +- PRs here are squash-merged, so the PR title becomes the commit subject on + `main`. Write the title in the same subject format; GitHub appends ` (#N)`. + The squash body is every branch commit's message in turn (each subject as a + `* ` line), so branch commits follow the format too. ## Repository Structure @@ -867,6 +869,7 @@ This document should evolve as patterns emerge. When you: **Add it here** and commit with a message explaining what prompted the addition. --- +*Last updated: 2026-10-01 - Replaced the inline commit message format with a pointer to the commit-craft skill, rewritten for agents rather than people, with a checker script, so Claude Code, Crush and Codex produce the same shape; the jujutsu skill no longer carries its own (conflicting) format* *Last updated: 2026-09-29 - The scheduled flake update broke on `nix/build-skill` not existing: nix-skills was renamed to agents.nix, history rewritten and restructured. `lib/skills-sh.nix` now goes through upstream's public overlay, which the restructure made cheap (measured), instead of the internals it used to avoid a then-18s eval; supabase-postgres-best-practices is pinned by hand because the index dropped its repo* *Last updated: 2026-09-21 - Ghost (#56): ghost.build is shutting down, so venari now runs a server for its OpenAPI contract from the alycda/ghost fork, with the CLI packaged from that fork and driven by an env-setting wrapper; recorded the two client-side assumptions (`tsdb` dbname, viper's empty-env handling) that the "no CLI patch needed" plan missed* *Last updated: 2026-09-21 - Added "prebuilt binaries in a persisted `$HOME` are image-scoped state" to Tools Nix Can't Fully Manage, after a rustup toolchain in the devhome volume survived an image rebuild and left `cargo` erroring ENOENT for a loader that no longer existed — with the corollary that an activation step guarded on "is it installed" can never repair state that went bad in place (#80)* diff --git a/tools/agents/skills/jujutsu/SKILL.md b/tools/agents/skills/jujutsu/SKILL.md index cc1188e0..0cfda4e5 100644 --- a/tools/agents/skills/jujutsu/SKILL.md +++ b/tools/agents/skills/jujutsu/SKILL.md @@ -105,7 +105,7 @@ The implication is in the next section. ## Agent Environment Rules -### Always use `-m` for messages +### Always pass the message: `-m` or `--stdin` Editor prompts hang in non-interactive environments. Every command that accepts `-m` must get one inline: @@ -117,6 +117,14 @@ jj commit -m "message" # NOT: jj commit jj squash -m "message" # NOT: jj squash (opens editor for combined description) ``` +A finished commit's message has a body, which is awkward in `-m`. Write it to a file +outside the working copy, check it, and pass it on stdin, as the **commit-craft** skill +describes: + +``` +jj describe --stdin < /path/to/msg.txt +``` + ### Avoid interactive commands These open TUIs and hang. Use alternatives: @@ -126,7 +134,7 @@ These open TUIs and hang. Use alternatives: | `jj split` (no args) | `jj split ...` with explicit fileset, or `jj squash`/`jj restore` patterns | | `jj squash -i` | `jj squash ...` with explicit fileset | | `jj resolve` | Edit conflict markers directly in the affected files, then `jj st` to verify | -| `jj describe` (no `-m`) | `jj describe -m "..."` | +| `jj describe` (no `-m`) | `jj describe -m "..."` or `jj describe --stdin < file` | ### Verify after mutation @@ -171,7 +179,9 @@ jj new -m "What I'm about to do" ``` This pattern surfaces intent before code exists. If Alyssa hasn't told Claude what to -commit message-wise, ask once and then proceed. +commit message-wise, ask once and then proceed. The intent message is only a subject +line, in commit-craft's subject format. Replace it with the full message (commit-craft) +when the change is done, before it is pushed. ### Atomic commits @@ -179,10 +189,9 @@ One logical change per commit. If a commit description starts to need "and" to c everything, that's the signal to split. Use the squash/restore patterns to peel out unrelated changes rather than mashing them in. -Commit message format (Alyssa's convention, matches the team's): -- Imperative verb, sentence case, no trailing period -- `feat:` / `fix:` / `refactor:` / `docs:` / `chore:` prefixes when conventional commits - are in use in the repo +What the message says (subject format, body, trailers) is the **commit-craft** skill's, +not this one's. Load it before writing any message. This skill covers only the commands +that set it. ### Viewing history diff --git a/tools/agents/skills/jujutsu/references/version-notes.md b/tools/agents/skills/jujutsu/references/version-notes.md index 37cd9fef..76cd582f 100644 --- a/tools/agents/skills/jujutsu/references/version-notes.md +++ b/tools/agents/skills/jujutsu/references/version-notes.md @@ -192,7 +192,8 @@ never the right move. ### Message via stdin `jj describe --stdin` reads the description from stdin. Available on the message-taking -commands; not usually relevant for agent use, which passes `-m`. +commands. Agents use it for finished messages, which have a body: commit-craft writes +the message to a file, checks it, then runs `jj describe --stdin < file`. --- diff --git a/tools/agents/skills/ste100/SKILL.md b/tools/agents/skills/ste100/SKILL.md index 3b809f5d..8509e093 100644 --- a/tools/agents/skills/ste100/SKILL.md +++ b/tools/agents/skills/ste100/SKILL.md @@ -92,10 +92,11 @@ This skill runs after the skills that own structure, never instead of them. - **communication-bridge** owns audience and format. Run it first, then run ste100 on its output only when the target surface is in the strict or STE-flavored rows above. Never on `--format=pushback` output. -- **commit-craft** owns the seven rules (subject line, wrap, what-and-why). - ste100 is a sentence-level pass on the body afterward, in STE-flavored - mode. It does not touch the subject line. The 50-character limit already - forces one reading. +- **commit-craft** owns the message format (subject line, body order, wrap) + and its checker. ste100 is a sentence-level pass on the body afterward, in + STE-flavored mode. It does not touch the subject line. Its length limit + already forces one reading. Run commit-craft's checker again after the + pass, since rewording can change the wrap. - **brag-doc** and **failure-doc** produce records in Alyssa's voice. Do not run ste100 on their output. If she asks, say why and offer a strict pass on any quoted command or symptom inside the entry instead. From 4f1e34bf194c02f2fcacfb6c130a506306f2e33b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 21:56:44 +0000 Subject: [PATCH 3/7] agent(skills): commit-craft names commits by subject, not change ID Review on #197 found two faults in the first example and in the rules behind it. The example named another commit by its jj change ID, which GitHub cannot resolve, so the reference is lost once the commit is pushed. It also ended with "to squash or rebase as you choose", an instruction to Alyssa inside a commit that she is the author of. The skill told agents to do both. The skill now says to name another commit by its subject line in quotes. A subject survives a rebase, a squash and the move to GitHub, and `git log --grep` finds it. A git hash is allowed only for a commit already on the default branch, and a PR number is preferred for merged work. A new rule says to write as the author and to put notes for the reviewer in the PR body. The "what is left" paragraph no longer offers an instruction for the person who merges. The example is rewritten to follow both rules, with a note that says what was removed and why. Checked: the rewritten example passes check-message, the checker's bats suite passes (10 tests), and check-skill-frontmatter.sh and rumdl pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Soievfadhy2k17VT4VecnT --- tools/agents/skills/commit-craft/SKILL.md | 17 ++++++---- .../commit-craft/references/examples.md | 31 +++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/tools/agents/skills/commit-craft/SKILL.md b/tools/agents/skills/commit-craft/SKILL.md index d70af03a..1ae0201d 100644 --- a/tools/agents/skills/commit-craft/SKILL.md +++ b/tools/agents/skills/commit-craft/SKILL.md @@ -114,18 +114,23 @@ to say. the environment. Say what you ran and what you saw. Then list the checks that pass (`shellcheck`, `nix flake check`, the bats suites). Also say what you did *not* run, and why. Never claim a check you did not run. -4. **What is left.** Follow-up work, known limits, a task that tracks them, or - an instruction for the person who merges ("a fix for vmp, to squash or - rebase as you choose"). +4. **What is left.** Follow-up work, known limits, and the task or issue that + tracks them. Language: - Plain words and short sentences. One idea per sentence. - Past tense for what was wrong before. Present tense for what the code does now. -- Refer to other commits by their jj change ID (the short prefix `jj log` - shows, for example `vmp`), because it survives rewrites. Use the git hash - only in a repository without jj. +- Write as the author. The commit is Alyssa's even when an AI trailer is + on it, so do not address her or the reviewer ("as you choose", "you can + squash this"). Put notes for the reviewer in the PR body. +- Refer to another commit by its subject line, in quotes. A subject + survives a rebase, a squash and the move to GitHub, and + `git log --grep` finds it. Never use a jj change ID: GitHub cannot + resolve it. Use a git hash only for a commit already on the default + branch, which is not rewritten. For merged work, prefer the PR number + (`#N`), which GitHub links. - Use backticks for commands, file names and identifiers. Use a hyphen list for parallel items. Do not use headings, bold or emoji. - Do not write filler: "This commit", "comprehensive", "robust", diff --git a/tools/agents/skills/commit-craft/references/examples.md b/tools/agents/skills/commit-craft/references/examples.md index d7fdda73..ff262897 100644 --- a/tools/agents/skills/commit-craft/references/examples.md +++ b/tools/agents/skills/commit-craft/references/examples.md @@ -3,29 +3,34 @@ Real commits from the dotfiles repository, rewrapped at 72 characters and with their trailers removed. Each one has a note on what to copy from it. -## A fix to another change, with a note for the person who merges +## A fix to another change, named by its subject ```text ci(tasks): test the global HUID task recipes -vmp moved `task` and `task-edit` out of the repo justfile into the -global one (tools/just) and their logic into tasks/scripts, but -huid-tasks.bats still copied the repo justfile and ran `just task`: 7 -of its 8 tests failed with no such recipe. +"just: global justfile with the HUID task recipes" moved `task` and +`task-edit` out of the repo justfile into the global one (tools/just) +and their logic into tasks/scripts, but huid-tasks.bats still copied +the repo justfile and ran `just task`: 7 of its 8 tests failed with no +such recipe. The suite now runs tools/just/justfile the way `just -g` does (from the test's temp dir), with tasks/scripts on PATH as mise links them and -HOME in a temp dir so no local.just joins in. Two tests cover what vmp -added: without a tasks/ dir a task goes to .tasks/, and TASKS_DIR -overrides both. +HOME in a temp dir so no local.just joins in. Two tests cover what that +commit added: without a tasks/ dir a task goes to .tasks/, and +TASKS_DIR overrides both. -A dangling fix for vmolorpo, to squash or rebase as you choose. All -bats suites (26) pass; shellcheck and editorconfig-checker too. +All bats suites (26) pass; shellcheck and editorconfig-checker too. ``` -Copy: the first paragraph names the change that caused the problem by its -jj change ID (`vmp`) and gives the exact failure ("7 of its 8 tests -failed"). The last paragraph tells the reader what to do with the commit. +Copy: the first paragraph names the commit that caused the problem by its +subject line, which survives a rebase, a squash and the move to GitHub, and +it gives the exact failure ("7 of its 8 tests failed"). + +The original named that commit by its jj change ID and ended with "a +dangling fix for vmolorpo, to squash or rebase as you choose". Both are +removed here. GitHub cannot resolve a change ID, and the commit is the +author's, so it does not give the author instructions. ## A noun-phrase subject, and an honest list of what was not run From e86ed4bbe38d3acc4bd25466b3b7160ba0f3c40b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 22:13:54 +0000 Subject: [PATCH 4/7] vc: check the commit message format in jj, git and CI The commit message format was only in the commit-craft skill. A skill works only when an agent loads it and follows it, and Alyssa writes commits too. Configuration applies to every commit, whoever writes it. This is the same change as "vc: check the commit message format in jj, git and CI" on the develop line, fitted to how main installs config. The checker moves out of the skill to tools/commit/check-commit-msg. A new script, tools/commit/check-commits, runs it on each commit in a jj revset or a git range and skips merges. home-manager installs both as one package (common.nix). The account without Nix gets them on PATH from mise's [env], from the checkout bootstrap.sh makes. The checks run in four places: - jj: tools/jujutsu/config is new on main. A new description opens with the format as `JJ:` lines, and a `jj push` alias runs `check-commits --jj` on the stack that is not on trunk yet before it runs `jj git push`. jj runs no git hooks, so the check happens before the push. home-manager links the file to ~/.config/jj/conf.d/dotfiles.toml; bootstrap.sh links the same path. - git: a `commit-msg` hook in tools/git/hooks, for the dotfiles checkouts only, because `core.hooksPath` replaces .git/hooks for the whole repo. tools/git/config has the `includeIf` for the account without Nix, which includes that file from the checkout. home-manager copies it into the store alone, where the relative include finds nothing, so git.nix adds the same conditional include. - CI: a new commit-messages workflow checks every commit on a pull request. PRs here are squash-merged and the squash body is made of the branch commits' messages, so all of them are checked. Tested in a git-only sandbox, with jj 0.45.1 and nix added. The develop line's tests/commit.bats, pointed at these files, passes (14 tests). alyssa@dev-x86 evaluates, with the GitHub inputs fetched through git+https (the sandbox blocks the GitHub API). Its git config has both `includeIf` blocks, and the commit-checks package builds and passes check-commits on this branch. In a scratch HOME laid out like the account without Nix, a bad `git commit` fails in the checkout. mise 2026.8.6 puts tools/commit on PATH. statix, deadnix, shellcheck, actionlint and taplo pass. The darwin configurations were not evaluated, and main has no CI job for the bats suite. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Soievfadhy2k17VT4VecnT --- .github/workflows/commit-messages.yml | 26 ++++++++++ home-manager/modules/common.nix | 37 +++++++++----- home-manager/modules/git.nix | 12 ++++- .../check-message => commit/check-commit-msg} | 19 ++++---- tools/commit/check-commits | 48 +++++++++++++++++++ tools/git/config | 10 ++++ tools/git/dotfiles-repo.gitconfig | 8 ++++ tools/git/hooks/commit-msg | 15 ++++++ tools/jujutsu/config | 35 ++++++++++++++ tools/mise/bootstrap.sh | 3 ++ tools/mise/config.toml | 6 +++ 11 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/commit-messages.yml rename tools/{agents/skills/commit-craft/scripts/check-message => commit/check-commit-msg} (79%) create mode 100755 tools/commit/check-commits create mode 100644 tools/git/dotfiles-repo.gitconfig create mode 100755 tools/git/hooks/commit-msg create mode 100644 tools/jujutsu/config diff --git a/.github/workflows/commit-messages.yml b/.github/workflows/commit-messages.yml new file mode 100644 index 00000000..134f860f --- /dev/null +++ b/.github/workflows/commit-messages.yml @@ -0,0 +1,26 @@ +# Every commit on a pull request follows the commit-craft format +# (tools/commit/check-commit-msg). PRs here are squash-merged and the squash +# body is the branch commits' messages, so they are checked, not only the +# title. Pull requests only: a direct push can carry older commits. +name: commit messages + +on: + pull_request: + +permissions: + contents: read + +jobs: + check: + name: Check commit messages + runs-on: ubuntu-latest + steps: + # Pinned to a commit; the comment is the tag. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - name: Check commit messages + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: tools/commit/check-commits "$BASE..$HEAD" diff --git a/home-manager/modules/common.nix b/home-manager/modules/common.nix index f9318c15..3edf4f74 100644 --- a/home-manager/modules/common.nix +++ b/home-manager/modules/common.nix @@ -4,6 +4,14 @@ let # Core packages shared with devShells (defined in lib/core-packages.nix) # docker on OSX is installed by homebrew (Docker Desktop/Orbstack) corePackages = import ../../lib/core-packages.nix pkgs; + + # The commit message checks (tools/commit), on PATH for jj's `jj push` + # alias. One package, because check-commits looks for check-commit-msg + # next to itself. + commitChecks = pkgs.runCommandLocal "commit-checks" { } '' + mkdir -p $out/bin + cp ${../../tools/commit}/* $out/bin/ + ''; in { imports = [ @@ -39,7 +47,7 @@ in # Core packages across all profiles # Note: helix is configured via ./tools/helix.nix (programs.helix) # Note: claude-code CLI installed here (binary only; global rules managed by ./tools/claude-code.nix) - packages = corePackages ++ [ pkgs.claude-code pkgs.presenterm ]; + packages = corePackages ++ [ pkgs.claude-code pkgs.presenterm commitChecks ]; # Set helix as default editor sessionVariables = { @@ -75,14 +83,21 @@ in }; }; - # Invocable cheatsheets as just's GLOBAL justfile: `just -g ` from - # anywhere (verified on just 1.58: -g recipes run with the invocation - # directory as cwd). The shim imports the store copy by absolute path, and - # `import?` leaves an optional hook so a profile or machine can layer its - # own recipes (e.g. the work profile's ditto-worktree recipes) by writing - # ~/.config/just/local.just instead of fighting over this file. - xdg.configFile."just/justfile".text = '' - import '${../../tools/just}/global.justfile' - import? '~/.config/just/local.just' - ''; + xdg.configFile = { + # Invocable cheatsheets as just's GLOBAL justfile: `just -g ` from + # anywhere (verified on just 1.58: -g recipes run with the invocation + # directory as cwd). The shim imports the store copy by absolute path, and + # `import?` leaves an optional hook so a profile or machine can layer its + # own recipes (e.g. the work profile's ditto-worktree recipes) by writing + # ~/.config/just/local.just instead of fighting over this file. + "just/justfile".text = '' + import '${../../tools/just}/global.justfile' + import? '~/.config/just/local.just' + ''; + + # jj settings shared with the account without Nix: the commit message + # draft template and the `jj push` alias. conf.d, not config.toml, which + # `jj config set --user` writes. + "jj/conf.d/dotfiles.toml".source = ../../tools/jujutsu/config; + }; } \ No newline at end of file diff --git a/home-manager/modules/git.nix b/home-manager/modules/git.nix index 374c4e1c..41df4190 100644 --- a/home-manager/modules/git.nix +++ b/home-manager/modules/git.nix @@ -44,7 +44,17 @@ # Settings that work without Nix (defaultBranch, pull/push behaviour, # aliases) live in tools/git/config, which the mise account includes # from ~/.gitconfig. Only what needs Nix or a secret stays below. - includes = [ { path = ../../tools/git/config; } ]; + includes = [ + { path = ../../tools/git/config; } + ] + # The commit-msg hook, in the dotfiles checkouts only (core.hooksPath + # replaces .git/hooks for the whole repo). tools/git/config has the same + # includeIf for the account without Nix, but its relative path finds + # nothing in the store copy, so the include is repeated here. + ++ map (dir: { + path = ../../tools/git/dotfiles-repo.gitconfig; + condition = "gitdir:${dir}"; + }) [ "~/dotfiles/" "/workspaces/dotfiles/" ]; settings = { include.path = "${config.home.homeDirectory}/.local/share/agenix/git-config"; diff --git a/tools/agents/skills/commit-craft/scripts/check-message b/tools/commit/check-commit-msg similarity index 79% rename from tools/agents/skills/commit-craft/scripts/check-message rename to tools/commit/check-commit-msg index 8bfe862e..d74b2597 100755 --- a/tools/agents/skills/commit-craft/scripts/check-message +++ b/tools/commit/check-commit-msg @@ -1,13 +1,16 @@ #!/usr/bin/env bash -# check-message: check a commit message against the mechanical rules in -# commit-craft's SKILL.md. Reads FILE, or stdin when there is no argument. -# Prints one line per problem and exits 1; prints nothing and exits 0 when the -# message passes. It can't judge whether the body says the right things: that -# part is the skill's, not this script's. +# check-commit-msg: check one commit message against the format in the +# commit-craft skill. Reads FILE, or stdin when there is no argument. Prints +# one line per problem and exits 1; prints nothing and exits 0 when the +# message passes. It checks the shape only. Whether the body says the right +# things is the skill's job. # -# check-message msg.txt -# jj log -r @ --no-graph -T description | check-message -# git log -1 --format=%B | check-message +# check-commit-msg msg.txt +# jj log -r @ --no-graph -T description | check-commit-msg +# git log -1 --format=%B | check-commit-msg +# +# check-commits runs it over a range of commits; tools/git/hooks/commit-msg +# runs it on every `git commit` in this checkout. set -euo pipefail max=72 diff --git a/tools/commit/check-commits b/tools/commit/check-commits new file mode 100755 index 00000000..fa4b77a2 --- /dev/null +++ b/tools/commit/check-commits @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# check-commits: run check-commit-msg on every commit in a range, and print +# the problems under each commit that fails. Exits 1 if any commit fails. +# Merge commits are skipped: their subject is the tool's, not the author's. +# +# check-commits --jj [REVSET] jj commits. The default is the stack under +# @ that is not on trunk yet, without an empty +# undescribed @. The `jj push` alias uses it. +# check-commits RANGE git commits, e.g. origin/main..HEAD. CI +# uses this form. +set -euo pipefail + +# check-commit-msg sits next to this script in the checkout. home-manager +# copies each file into the store on its own, so fall back to PATH. +check="$(dirname "$(readlink -f "$0")")/check-commit-msg" +[ -x "$check" ] || check="check-commit-msg" + +stack='(trunk()..@) ~ merges() ~ (empty() & description(exact:""))' +failed=0 + +report() { # id subject message + local out + if ! out="$("$check" <<<"$3")"; then + printf '%s %s\n' "$1" "$2" + printf '%s\n' "$out" | sed 's/^/ /' + failed=1 + fi +} + +if [ "${1:-}" = --jj ]; then + # Assigned first, so a bad revset or a missing repo stops the script. + ids="$(jj log --no-graph --reversed -r "${2:-$stack}" -T 'commit_id.short() ++ "\n"')" + for id in $ids; do + report "$id" "$(jj log --no-graph -r "$id" -T 'description.first_line()')" \ + "$(jj log --no-graph -r "$id" -T description)" + done +elif [ $# -eq 1 ]; then + ids="$(git rev-list --no-merges --reverse "$1")" + for id in $ids; do + report "$(git rev-parse --short "$id")" "$(git log -1 --format=%s "$id")" \ + "$(git log -1 --format=%B "$id")" + done +else + echo "usage: check-commits --jj [REVSET] | check-commits " >&2 + exit 2 +fi + +exit "$failed" diff --git a/tools/git/config b/tools/git/config index d03b9d1a..5d49bc51 100644 --- a/tools/git/config +++ b/tools/git/config @@ -37,3 +37,13 @@ l = log --color --graph --format='%C(magenta)%h%Creset %s %C(cyan)<%an>%Creset' -n 20 --abbrev-commit # Short SHA, refs, subject, relative commit time, author lg = log --color --graph --format='%C(magenta)%h%Creset -%C(green)%d%Creset %s %C(yellow)(%cr) %C(cyan)<%an>%Creset' --abbrev-commit -- + +# The commit-msg hook (tools/git/hooks), in the dotfiles checkouts only. For +# the account without Nix, ~/.gitconfig includes this file from the checkout, +# so the relative path finds dotfiles-repo.gitconfig next to it. home-manager +# copies this file into the store alone, where git skips the missing file; +# git.nix adds the same conditional include for those accounts. +[includeIf "gitdir:~/dotfiles/"] + path = dotfiles-repo.gitconfig +[includeIf "gitdir:/workspaces/dotfiles/"] + path = dotfiles-repo.gitconfig diff --git a/tools/git/dotfiles-repo.gitconfig b/tools/git/dotfiles-repo.gitconfig new file mode 100644 index 00000000..b1bbd663 --- /dev/null +++ b/tools/git/dotfiles-repo.gitconfig @@ -0,0 +1,8 @@ +# Git settings for the dotfiles checkouts only. tools/git/config includes this +# file for them on the account without Nix; home-manager/modules/git.nix does +# for the others. + +# A relative hooksPath is relative to the top of the working tree. It replaces +# .git/hooks for the whole repo, which is why it is not set for every repo. +[core] + hooksPath = tools/git/hooks diff --git a/tools/git/hooks/commit-msg b/tools/git/hooks/commit-msg new file mode 100755 index 00000000..95f9633d --- /dev/null +++ b/tools/git/hooks/commit-msg @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# commit-msg: refuse a commit whose message fails check-commit-msg (the format +# in the commit-craft skill). Git runs it in this checkout only: +# dotfiles-repo.gitconfig sets core.hooksPath to this directory, and +# tools/git/config includes that file for the dotfiles checkouts. jj runs no +# git hooks; for jj, `jj push` does the same check. +set -euo pipefail + +root="$(git rev-parse --show-toplevel)" +# Git has not removed the comment lines yet when an editor wrote the file. +if ! out="$(git stripspace --strip-comments <"$1" | "$root/tools/commit/check-commit-msg")"; then + printf '%s\n' "$out" >&2 + echo "commit-msg: the message does not follow the commit-craft format" >&2 + exit 1 +fi diff --git a/tools/jujutsu/config b/tools/jujutsu/config new file mode 100644 index 00000000..e8e912c4 --- /dev/null +++ b/tools/jujutsu/config @@ -0,0 +1,35 @@ +# jj settings for every repo, shared by both kinds of account. home-manager +# links this file to ~/.config/jj/conf.d/dotfiles.toml; tools/mise/bootstrap.sh +# links the same path for the account without Nix. conf.d is merged over +# config.toml, so never put a [user] block here: it would beat +# `jj config set --user` (and the mise identity task). + +# The commit message format (the commit-craft skill) for every repo. A new +# description opens with the format as JJ: lines, which jj drops on save. +[template-aliases] +default_commit_description = ''' +"\n\n" ++ +"JJ: Subject: (): . Lowercase summary, no period,\n" ++ +"JJ: 72 characters at most. Then a blank line, then the body, wrapped at 72.\n" ++ +"JJ: Body, in this order:\n" ++ +"JJ: 1. Why: the problem or need, with the evidence.\n" ++ +"JJ: 2. What and how: what changes, and why this way.\n" ++ +"JJ: 3. Verification: what you ran and saw, and what you did not run.\n" ++ +"JJ: 4. What is left: follow-up work and known limits.\n" +''' + +# `jj push`: check the messages of the stack that is not on trunk yet, then +# `jj git push` with the same arguments. jj runs no git hooks, so this is +# where the format is enforced. check-commits is on PATH through home-manager, +# or mise's [env] on the account without Nix. `jj git push` still works when +# a repo has its own format. +[aliases] +push = [ + "util", + "exec", + "--", + "sh", + "-c", + "check-commits --jj && exec jj git push \"$@\"", + "jj-push", +] diff --git a/tools/mise/bootstrap.sh b/tools/mise/bootstrap.sh index a5bb7286..2b263b42 100755 --- a/tools/mise/bootstrap.sh +++ b/tools/mise/bootstrap.sh @@ -83,6 +83,9 @@ fi # 3. Config shared with the Nix accounts, used in place from the checkout. link "$HOME/.config/mise" "$DOTFILES/tools/mise" link "$HOME/.config/helix" "$DOTFILES/tools/helix" +# The commit message draft template and the `jj push` alias. conf.d, not +# config.toml, which `jj config set --user` (the identity task) writes. +link "$HOME/.config/jj/conf.d/dotfiles.toml" "$DOTFILES/tools/jujutsu/config" gitconfig="$DOTFILES/tools/git/config" if git config --global --get-all include.path 2>/dev/null | grep -qxF "$gitconfig"; then diff --git a/tools/mise/config.toml b/tools/mise/config.toml index 293f46ac..ecb1f3c6 100644 --- a/tools/mise/config.toml +++ b/tools/mise/config.toml @@ -22,6 +22,12 @@ # Keep this list short. A tool belongs here only if this account really uses # it. Anything the containers also need goes in lib/core-packages.nix. +# The commit message checks, on PATH for jj's `jj push` alias (jj config +# from tools/jujutsu, which bootstrap.sh links). The checkout is where +# bootstrap.sh puts it by default. +[env] +_.path = ["{{env.HOME}}/dotfiles/tools/commit"] + [tools] claude = "latest" # aqua:anthropics/claude-code helix = "latest" # aqua:helix-editor/helix From 88dda6b5f35d422e1fab7d7612096d734b86d260 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 22:14:04 +0000 Subject: [PATCH 5/7] agent(skills): commit-craft as a layer over ste100 The skill had copied its language rules and examples from this repo's history. That history has no convention, so the skill made the noise a rule. It also carried the checker that is now in tools/commit, and it told agents to follow a repo's history when the history looked consistent. The skill now follows the ste100 pattern: it adds only what is specific to commits, and it gives the wording to another skill. The body is a ste100 "commit body" surface, in STE-flavored mode. When ste100 is not installed, the agent says so and does not reconstruct its rules. The skill keeps the subject format, the order of the body paragraphs, the trailers, and two rules from the review of #197: write as the author, and name another commit by its subject line, not by a jj change ID. A table says which check runs where. The rule to copy a repo's history is gone. references/examples.md is gone, and so are the language rules. One example and two bad subjects stay in SKILL.md. SKILL.md is the same file as on the develop line. ste100's handoff names `check-commit-msg`. The jujutsu skill's push section says to push with `jj push` in a repo that uses this format. CLAUDE.md says where the checks run, and agent-skills.nix's comment says what the skill now covers. Tested: the develop line's tests/commit.bats passes against these files (14 tests, including the skill's example and bad subjects), and check-skill-frontmatter.sh accepts the skill (508 bytes). ste100 is not installed in this sandbox, so this message and the skill's example were not given a ste100 pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Soievfadhy2k17VT4VecnT --- CLAUDE.md | 11 +- home-manager/modules/tools/agent-skills.nix | 4 +- tools/agents/skills/commit-craft/SKILL.md | 216 +++++++++--------- .../commit-craft/references/examples.md | 99 -------- tools/agents/skills/jujutsu/SKILL.md | 8 +- tools/agents/skills/ste100/SKILL.md | 10 +- 6 files changed, 127 insertions(+), 221 deletions(-) delete mode 100644 tools/agents/skills/commit-craft/references/examples.md diff --git a/CLAUDE.md b/CLAUDE.md index 3acd9d52..64ee5d5c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,9 +92,12 @@ Every commit message follows the **commit-craft** skill `~/.agents/skills/commit-craft`). Read it before writing or changing any message, in any harness, even when skills are not loaded for you automatically. It defines the format, `(): ` plus a -body in a fixed order, and has a checker, `scripts/check-message`, that each -message must pass before it is set. The jujutsu skill covers only the -commands that set the message. +body in a fixed order, and gives the body's wording to the ste100 skill. The +shape is enforced outside the skill, by the scripts in `tools/commit/`: run +`check-commit-msg` on a message before you set it, and push with `jj push`, +which checks the stack first (`tools/jujutsu/config`). A `git commit` in this +checkout runs the same check as a hook, and CI checks every commit on a pull +request. The jujutsu skill covers only the commands that set the message. Two additions for this repository: @@ -869,7 +872,7 @@ This document should evolve as patterns emerge. When you: **Add it here** and commit with a message explaining what prompted the addition. --- -*Last updated: 2026-10-01 - Replaced the inline commit message format with a pointer to the commit-craft skill, rewritten for agents rather than people, with a checker script, so Claude Code, Crush and Codex produce the same shape; the jujutsu skill no longer carries its own (conflicting) format* +*Last updated: 2026-10-01 - Replaced the inline commit message format with a pointer to the commit-craft skill, and moved the enforcement out of the skill into configuration (a jj draft template and `jj push` alias, a git hook for this checkout, a CI job), because a skill only works when an agent loads it and the format must hold for every commit; the skill keeps what no check can judge and gives the body's wording to ste100* *Last updated: 2026-09-29 - The scheduled flake update broke on `nix/build-skill` not existing: nix-skills was renamed to agents.nix, history rewritten and restructured. `lib/skills-sh.nix` now goes through upstream's public overlay, which the restructure made cheap (measured), instead of the internals it used to avoid a then-18s eval; supabase-postgres-best-practices is pinned by hand because the index dropped its repo* *Last updated: 2026-09-21 - Ghost (#56): ghost.build is shutting down, so venari now runs a server for its OpenAPI contract from the alycda/ghost fork, with the CLI packaged from that fork and driven by an env-setting wrapper; recorded the two client-side assumptions (`tsdb` dbname, viper's empty-env handling) that the "no CLI patch needed" plan missed* *Last updated: 2026-09-21 - Added "prebuilt binaries in a persisted `$HOME` are image-scoped state" to Tools Nix Can't Fully Manage, after a rustup toolchain in the devhome volume survived an image rebuild and left `cargo` erroring ENOENT for a loader that no longer existed — with the corollary that an activation step guarded on "is it installed" can never repair state that went bad in place (#80)* diff --git a/home-manager/modules/tools/agent-skills.nix b/home-manager/modules/tools/agent-skills.nix index 7e0a19ba..9c714b6a 100644 --- a/home-manager/modules/tools/agent-skills.nix +++ b/home-manager/modules/tools/agent-skills.nix @@ -35,8 +35,8 @@ # Repo skills: # brag-doc - promo-packet impact entries from raw work notes # cf-now - private file sharing from Cloudflare R2 (pre-signed URLs) -# commit-craft - the commit message format, with a checker script -# (scripts/check-message) every message must pass +# commit-craft - what a commit message says; the shape is checked +# by tools/commit (jj push, git hook, CI) # entity-level-git - Ataraxy Labs stack: sem (entity diff/blame/impact), # weave (semantic merge driver), inspect (review triage) # failure-doc - failures as deliberate-learning records diff --git a/tools/agents/skills/commit-craft/SKILL.md b/tools/agents/skills/commit-craft/SKILL.md index 1ae0201d..47a75652 100644 --- a/tools/agents/skills/commit-craft/SKILL.md +++ b/tools/agents/skills/commit-craft/SKILL.md @@ -5,68 +5,67 @@ description: > write or change a commit message: before `jj describe`, `jj new -m`, `jj commit`, `jj squash -m`, `git commit` or `git commit --amend`, when asked to "commit this", and when reviewing or rewording existing messages. - Applies in any harness (Claude Code, Crush, Codex). Has a checker script - that every message must pass before it is set. The jujutsu skill covers - the VCS commands; this skill covers what the message says. + Applies in any harness (Claude Code, Crush, Codex). The shape is checked by + check-commit-msg, the jj push alias, a git hook and CI; this skill covers + what those checks cannot judge. The wording follows the ste100 skill. --- # Commit Craft -This skill defines one commit message format. Follow it exactly. The goal is -that every commit reads the same no matter which agent or model wrote it. +The commit path enforces the shape of a message. This skill covers the part +no check can judge: what the message says. It adds no language rules of its +own. The body's wording comes from the ste100 skill. + +## What is enforced, and where + +| Where | What runs | +| --- | --- | +| `check-commit-msg FILE` (or stdin) | The shape rules below. Run it yourself before you set a message. | +| `jj describe` with an editor | The editor opens with the format as `JJ:` lines. | +| `jj push` (alias) | `check-commits --jj` on the stack that is not on trunk yet, then `jj git push` with the same arguments. | +| `git commit` in the dotfiles checkouts | The `commit-msg` hook in `tools/git/hooks`. | +| A pull request on the dotfiles repo | CI runs `check-commits` on the PR's commits. | + +The scripts are in `tools/commit/` in the dotfiles repo, and on PATH through +mise or home-manager. jj runs no git hooks, so in a jj repo push with +`jj push`, not `jj git push`. ## Which convention wins 1. A convention the repository documents (CONTRIBUTING, CLAUDE.md, AGENTS.md, - a commitlint config) wins. -2. If there is none, but `git log --format=%s -30` shows a different consistent - style, follow that history. -3. Otherwise, and always in the dotfiles repository, use this skill. + a commitlint config) wins. Push with `jj git push` there, because + `jj push` checks this skill's format. +2. Otherwise, use this skill. Do not copy the style of the repository's + history. ## Procedure Do these steps in order for every commit. -1. Read the full change: `jj diff --git` (or `git diff --staged`). Read the - task or conversation that caused it. You need to know *why*, not only - *what*. +1. Read the full change (`jj diff --git` or `git diff --staged`) and the task + that caused it. You must know why the change exists, not only what it is. 2. If the change does two unrelated things, split it first (see the jujutsu skill). A subject that needs "and" is a sign of two commits. -3. Pick the area and scope (below). -4. Write the message to a file outside the repository (your scratchpad, or - `$TMPDIR`). Never write it inside the working copy: jj snapshots it. -5. Run the checker on the file and fix every problem it prints. Repeat until - it prints nothing: - - ```bash - /scripts/check-message /path/to/msg.txt - ``` - - `` is the directory that holds this SKILL.md. Installed, it is - `~/.agents/skills/commit-craft`; in the dotfiles checkout it is - `tools/agents/skills/commit-craft`. -6. Set the message from the file: - - ```bash - jj describe --stdin < /path/to/msg.txt # jj - git commit -F /path/to/msg.txt # git only - ``` - -7. Check what was stored: - `jj log -r @ --no-graph -T description | /scripts/check-message`. +3. Write the message to a file outside the working copy (your scratchpad, or + `$TMPDIR`). jj snapshots every file in the working copy. +4. Apply the ste100 skill to the body (see Language). +5. Run `check-commit-msg /path/to/msg.txt`. Fix every problem it prints, and + run it again until it prints nothing. ste100 can change line lengths, so + run it after step 4. +6. Set the message from the file: `jj describe --stdin < /path/to/msg.txt`, + or `git commit -F /path/to/msg.txt` with git only. +7. Push with `jj push`. ## Subject line Format: `(): `. The `()` part is optional. ```text -hm(zsh): compinit -i, instead of re-owning dirs in CI ci(nix): secure the runner dirs compaudit flags -just: global justfile with the HUID task recipes devcontainer(nix): apply home-manager on create ``` -Hard rules (the checker enforces them): +`check-commit-msg` enforces these rules: - `area` is lowercase: letters, digits and `-`. - One space after the colon. The summary starts with a lowercase letter, @@ -74,67 +73,58 @@ Hard rules (the checker enforces them): (`HUID`, `README`). - 72 characters at most. Aim for 50 to 60. - No period at the end. -- The summary says what the commit does or adds. Use an imperative verb - (`secure the runner dirs ...`) or a noun phrase for the thing added - (`global justfile with ...`). Do not use past tense (`fixed`, `added`), the - third person (`fixes`, `adds`), or "this commit". - -Choosing the area and scope: +- The first word of the summary is not past tense (`fixed`, `added`), third + person (`fixes`, `adds`) or "this". -- Reuse an area that already exists. List them with: +These rules are yours to apply: - ```bash - git log --no-merges --format=%s | - sed -nE 's/^([a-z0-9-]+(\([^)]*\))?):.*/\1/p' | sort | uniq -c | sort -rn - ``` - -- The area is the part of the repository or the tool the change belongs to. - Examples from the dotfiles: `hm` (home-manager), `ci` (`.github/`), `mise`, - `nix`, `devcontainer`, `just`, `agent(skills)`, `docs`. -- The scope narrows the area: `ci(nix)` is the Nix workflow, `hm(zsh)` is - home-manager's zsh. Leave it out when the area is already narrow enough. -- Make a new area only for something new at the top level of the repository. +- The summary says what the commit does (`secure the runner dirs ...`) or + names what it adds (`global justfile with ...`). +- The area is the part of the repository or the tool the change belongs to: + `hm` (home-manager), `ci` (`.github/`), `mise`, `nix`, `devcontainer`, + `just`, `agent(skills)`, `docs`. Reuse an existing area. Make a new one + only for something new at the top level of the repository. +- The scope narrows the area: `ci(nix)` is the Nix workflow. Leave it out + when the area is narrow enough. ## Body -A body is required. Wrap it at 72 characters. Indented lines (pasted output, -code) and lines that contain a URL can be longer. - -Write the paragraphs in this order. Skip a paragraph only when it has nothing -to say. - -1. **Why.** The problem or the need. Give the evidence: the error text, the CI - run, the task or issue. Say what was wrong before this commit, not what - you did about it. -2. **What and how.** What the commit changes, in terms of behavior. Say why - it is done this way when there was another choice, and name the choice you - did not take. Name files and settings when that helps a reader find them. - Do not walk through the diff line by line. -3. **Verification.** Start with "Tested in ..." or "Verified in ..." and name - the environment. Say what you ran and what you saw. Then list the checks - that pass (`shellcheck`, `nix flake check`, the bats suites). Also say what - you did *not* run, and why. Never claim a check you did not run. +A body is required. Wrap it at 72 characters. Indented lines and lines with a +URL can be longer. Write these paragraphs in this order. Leave one out only +when it has nothing to say. + +1. **Why.** The problem or the need, with the evidence: the error text, the + CI run, the task or issue. Say what was wrong before this commit. +2. **What and how.** What the change does to behavior. When there was + another option, name it and say why it was not used. Do not walk through + the diff line by line. +3. **Verification.** Name the environment. Say what you ran and what you + saw, then which checks pass. Say what you did not run, and why. Never + claim a check you did not run. 4. **What is left.** Follow-up work, known limits, and the task or issue that tracks them. -Language: - -- Plain words and short sentences. One idea per sentence. -- Past tense for what was wrong before. Present tense for what the code does - now. -- Write as the author. The commit is Alyssa's even when an AI trailer is - on it, so do not address her or the reviewer ("as you choose", "you can - squash this"). Put notes for the reviewer in the PR body. -- Refer to another commit by its subject line, in quotes. A subject - survives a rebase, a squash and the move to GitHub, and - `git log --grep` finds it. Never use a jj change ID: GitHub cannot - resolve it. Use a git hash only for a commit already on the default - branch, which is not rewritten. For merged work, prefer the PR number - (`#N`), which GitHub links. +## Language + +The body is a "commit body" surface in the ste100 skill: use its +STE-flavored mode. Do not apply ste100 to the subject line. Read +`~/.agents/skills/ste100/SKILL.md` before you write the body. If it is not +installed, say so in your reply. Do not reconstruct its rules from memory. + +These rules are about the commit, not the language, so they apply either +way: + +- Write as the author. The commit is Alyssa's, also when an AI trailer is on + it. Do not address her or the reviewer ("as you choose", "you can squash + this"). Put notes for the reviewer in the PR body. +- Refer to another commit by its subject line, in quotes. A subject stays + correct after a rebase, a squash and a push to GitHub, and + `git log --grep` finds it. Do not use a jj change ID: GitHub cannot + resolve it. Use a git hash only for a commit on the default branch, which + is not rewritten. For merged work, use the PR number (`#N`), which GitHub + links. - Use backticks for commands, file names and identifiers. Use a hyphen list for parallel items. Do not use headings, bold or emoji. -- Do not write filler: "This commit", "comprehensive", "robust", - "seamlessly", "Additionally", "In order to". Do not praise the change. ## Trailers @@ -142,42 +132,48 @@ Trailers go in the last paragraph, one `Key: value` per line, after a blank line. - Keep the attribution trailers your harness tells you to add (for example - `Co-Authored-By:` and `Claude-Session:`). Do not invent others. -- Put attribution only in trailers. Never put a "Generated with ..." line or - an emoji banner in the body. -- `Closes #N` or `Refs #N` goes in the body's last paragraph, not in the - trailers, unless the repository uses a different convention. + `Co-Authored-By:`). Do not invent others. +- Put attribution only in trailers. Never put a "Generated with ..." line in + the body. ## Example -A real commit from the dotfiles, wrapped at 72: +This example shows the shape. ste100 decides the wording. ```text ci(nix): secure the runner dirs compaudit flags -nix.yml's home job failed in run 36364995815: in the interactive zsh, -compinit found completion dirs it considers insecure (owned by, or -writable by, someone other than root and the user). With no terminal to -answer its "continue?" prompt it aborted, so the compinit check failed. -The devcontainer image has no such dirs, which is why the local run -passed. +The home job in nix.yml failed in run 36364995815. In the interactive +zsh, compinit found completion dirs that it thinks are not secure: dirs +that a user other than root or the owner owns or can write to. No +terminal was available to answer its "continue?" prompt, so compinit +stopped and the compinit check failed. The devcontainer image has no +such dirs, so the local run passed. -A step before the check now lists what compaudit flags and hands it to -root (chown root:root, chmod go-w), printing the list so the log shows -which of the runner's dirs they were. +A new step runs before the check. It lists the dirs that compaudit +flags, and makes root their owner (chown root:root, chmod go-w). The +step prints the list, so the log shows which runner dirs they were. -Reproduced in the devcontainer base image by giving another user -/usr/local/share/zsh: compinit aborted and the check failed as in CI; -after the step, all ten checks pass. +Reproduced in the devcontainer base image: another user got +/usr/local/share/zsh, compinit stopped, and the check failed as in CI. +After the step, all ten checks pass. Co-Authored-By: ``` -More examples, with what each one does well: `references/examples.md`. +These subjects fail `check-commit-msg`: + +```text +Updated agent skills. +``` + +```text +docs(agents): Instructions for Claude +``` ## Rewording existing commits -To fix a message on another commit, use -`jj describe -r --stdin < msg.txt`. Run the checker on it first. -Do not reword commits that are already on a remote branch someone else uses +To change the message of another commit, run +`jj describe -r --stdin < msg.txt`. Run `check-commit-msg` on the +file first. Do not reword commits on a remote branch that other people use, unless you are asked to. diff --git a/tools/agents/skills/commit-craft/references/examples.md b/tools/agents/skills/commit-craft/references/examples.md deleted file mode 100644 index ff262897..00000000 --- a/tools/agents/skills/commit-craft/references/examples.md +++ /dev/null @@ -1,99 +0,0 @@ -# Examples - -Real commits from the dotfiles repository, rewrapped at 72 characters and -with their trailers removed. Each one has a note on what to copy from it. - -## A fix to another change, named by its subject - -```text -ci(tasks): test the global HUID task recipes - -"just: global justfile with the HUID task recipes" moved `task` and -`task-edit` out of the repo justfile into the global one (tools/just) -and their logic into tasks/scripts, but huid-tasks.bats still copied -the repo justfile and ran `just task`: 7 of its 8 tests failed with no -such recipe. - -The suite now runs tools/just/justfile the way `just -g` does (from the -test's temp dir), with tasks/scripts on PATH as mise links them and -HOME in a temp dir so no local.just joins in. Two tests cover what that -commit added: without a tasks/ dir a task goes to .tasks/, and -TASKS_DIR overrides both. - -All bats suites (26) pass; shellcheck and editorconfig-checker too. -``` - -Copy: the first paragraph names the commit that caused the problem by its -subject line, which survives a rebase, a squash and the move to GitHub, and -it gives the exact failure ("7 of its 8 tests failed"). - -The original named that commit by its jj change ID and ended with "a -dangling fix for vmolorpo, to squash or rebase as you choose". Both are -removed here. GitHub cannot resolve a change ID, and the commit is the -author's, so it does not give the author instructions. - -## A noun-phrase subject, and an honest list of what was not run - -```text -jj-stash: vendor and install through mise - -tools/jj-stash holds jj-stash, its README and its three test scripts, -unchanged from alycda/jj at 1f1aafa. tools/jj-stash/UPSTREAM records -the commit. mise links the command into ~/.local/bin. - -It needs bash 4+, and mise's registry has no bash. On a Mac with no -Nix, `env bash` is /bin/bash 3.2 and the script fails; with nix-darwin -it resolves to the system bash 5.x, and the devcontainers have -Debian's. - -Tested in a lab copy of this repo through the mise-installed link: -push hides a three-commit chain, and list, index and the exclude rule -work. pop restores identical change and commit IDs and leaves no refs; -drop forgets a stash and prints the recovery `jj new`. The vendored -tests are pinned to a lab copy of ~/WIP/jj-dotfiles and were not run. -``` - -Copy: the verification paragraph says what was observed, command by -command, and ends with what was *not* run and why. The second paragraph is -a known limit, stated as a fact with the platforms it affects. - -## Behavior first, then verification that matches it - -```text -devcontainer(nix): apply home-manager on create - -The Nix devcontainer switches to the home-manager profile in -postCreateCommand ($USER set, since lifecycle commands leave it -unset). The Nix feature now only enables flakes; its jj/just package -list is gone, since the profile provides every tool. postAttachCommand -calls just by its profile path, because lifecycle commands get a plain -/bin/sh PATH. - -Tested with a fresh build through the devcontainer CLI: postCreate -activates the profile. postAttach reaches the recipe, which stops at -"no VS Code CLI" as expected without an attached VS Code. An -interactive zsh has all eleven tools, EDITOR=hx, `..`, revsets.fix=@, -cheat jj/fix and the shared git config: the same checks as the mise -container. -``` - -Copy: every "because" in the first paragraph explains a choice a reader -would otherwise question. Each claim in the first paragraph has a matching -check in the second. - -## What not to write - -```text -Updated agent skills. -``` - -The area is missing, the summary is capitalised, it is in the past tense, -it ends with a period, and there is no body. A reader learns nothing that -`jj diff --stat` would not show. - -```text -docs(agents): Instructions for Claude -``` - -The summary is capitalised and there is no body. Which instructions, and -why now? diff --git a/tools/agents/skills/jujutsu/SKILL.md b/tools/agents/skills/jujutsu/SKILL.md index 0cfda4e5..d051154d 100644 --- a/tools/agents/skills/jujutsu/SKILL.md +++ b/tools/agents/skills/jujutsu/SKILL.md @@ -570,12 +570,18 @@ jj git push --change @- # same, for parent (common when @ `jj git push` uses force-with-lease semantics by default. Safe in normal use. +In a repo that uses the commit-craft format, push with `jj push` instead. It is an +alias from the dotfiles' jj config: it runs `check-commits --jj` on the stack that is +not on trunk yet, and runs `jj git push` with the same arguments only when every +message passes. jj runs no git hooks, so this is the check before a push. + ### Before pushing 1. `jj log -r 'trunk()..@'` — confirm what's in the stack 2. `jj bookmark list` — confirm the bookmark points where intended 3. `jj diff --git -r ` — review final content -4. `jj git push -b ` — push only after Alyssa explicitly says so +4. `jj push -b ` (or `jj git push -b ` in a repo with its own + commit format) — push only after Alyssa explicitly says so **Never push unprompted.** Pushing is an Alyssa-only decision. diff --git a/tools/agents/skills/ste100/SKILL.md b/tools/agents/skills/ste100/SKILL.md index 8509e093..8d6df2ed 100644 --- a/tools/agents/skills/ste100/SKILL.md +++ b/tools/agents/skills/ste100/SKILL.md @@ -92,11 +92,11 @@ This skill runs after the skills that own structure, never instead of them. - **communication-bridge** owns audience and format. Run it first, then run ste100 on its output only when the target surface is in the strict or STE-flavored rows above. Never on `--format=pushback` output. -- **commit-craft** owns the message format (subject line, body order, wrap) - and its checker. ste100 is a sentence-level pass on the body afterward, in - STE-flavored mode. It does not touch the subject line. Its length limit - already forces one reading. Run commit-craft's checker again after the - pass, since rewording can change the wrap. +- **commit-craft** owns the message format (subject line, body order, wrap). + ste100 is a sentence-level pass on the body afterward, in STE-flavored + mode. It does not touch the subject line. Its length limit already forces + one reading. Run `check-commit-msg` again after the pass, since rewording + can change the wrap. - **brag-doc** and **failure-doc** produce records in Alyssa's voice. Do not run ste100 on their output. If she asks, say why and offer a strict pass on any quoted command or symptom inside the entry instead. From a2b14317598c9607638fa92846008be53e2158da Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 22:16:24 +0000 Subject: [PATCH 6/7] agent(skills): commit-craft takes its facts from sem commit-craft told agents to read `jj diff --git` and work out what changed. In a large or mixed diff that is where messages go wrong: formatter churn sits next to the real change, and entity names get paraphrased. The entity-level-git skill already has a tool for this. sem now supplies the facts, ste100 the words, and commit-craft the order. A new Facts section says to start with `sem diff --no-cosmetics`, to name each entity as sem names it, to cover the cosmetic edits in one sentence, and to use `sem impact --tests` to find the tests for the verification paragraph. It points to entity-level-git for sem's language gaps, and says to fall back to `jj diff` without sem and to say so. The split step gains two signs of two commits from sem's output. Tested with sem 0.25.0 (upstream's release binary, sha256 checked against its checksums.txt) in a scratch colocated jj repo: `sem diff` shows `@` against `@-`, `sem diff ^ ` shows one commit, `--no-cosmetics` drops a whitespace-only edit, Nix bindings come out as entities (`home.packages`), and `sem impact --tests` finds a test that names the function. The develop line's tests/commit.bats still passes (14 tests). Same change as on the develop line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Soievfadhy2k17VT4VecnT --- tools/agents/skills/commit-craft/SKILL.md | 40 ++++++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/tools/agents/skills/commit-craft/SKILL.md b/tools/agents/skills/commit-craft/SKILL.md index 47a75652..189f4037 100644 --- a/tools/agents/skills/commit-craft/SKILL.md +++ b/tools/agents/skills/commit-craft/SKILL.md @@ -7,14 +7,17 @@ description: > asked to "commit this", and when reviewing or rewording existing messages. Applies in any harness (Claude Code, Crush, Codex). The shape is checked by check-commit-msg, the jj push alias, a git hook and CI; this skill covers - what those checks cannot judge. The wording follows the ste100 skill. + what those checks cannot judge. The facts come from sem (entity-level-git) + and the wording from the ste100 skill. --- # Commit Craft The commit path enforces the shape of a message. This skill covers the part no check can judge: what the message says. It adds no language rules of its -own. The body's wording comes from the ste100 skill. +own. Two other skills do the work it would otherwise repeat: sem, from the +entity-level-git skill, gives the facts (which entities changed), and ste100 +gives the words. This skill gives the order. ## What is enforced, and where @@ -42,10 +45,14 @@ mise or home-manager. jj runs no git hooks, so in a jj repo push with Do these steps in order for every commit. -1. Read the full change (`jj diff --git` or `git diff --staged`) and the task - that caused it. You must know why the change exists, not only what it is. +1. Read the full change and the task that caused it. You must know why the + change exists, not only what it is. If `sem` is installed, start with + `sem diff --no-cosmetics` (see Facts). Then read `jj diff --git` (or + `git diff --staged`) for the detail. 2. If the change does two unrelated things, split it first (see the jujutsu - skill). A subject that needs "and" is a sign of two commits. + skill). Signs of two commits: a subject that needs "and", two groups of + entities in `sem diff` with no relation, or formatting-only edits (sem + marks them `[cosmetic]`) next to real ones. 3. Write the message to a file outside the working copy (your scratchpad, or `$TMPDIR`). jj snapshots every file in the working copy. 4. Apply the ste100 skill to the body (see Language). @@ -104,6 +111,29 @@ when it has nothing to say. 4. **What is left.** Follow-up work, known limits, and the task or issue that tracks them. +## Facts + +Where `sem` is installed (the entity-level-git skill says where), it lists +the entities a change touches by name: functions, classes, Nix bindings such +as `home.packages`. Use it for the facts in the body. + +- `sem diff --no-cosmetics` in a colocated jj repo shows `@` against `@-`. + For another commit, run `sem diff ^ ` with its git commit + ID (`jj log -r --no-graph -T commit_id`). +- The "what and how" paragraph covers the entities that list shows. If the + commit also has formatting-only edits, say so in one sentence. +- Name each entity as sem names it, in backticks. ste100 treats a technical + name as one word with one spelling, so do not paraphrase it. +- For the verification paragraph, `sem impact --tests` lists the + tests that depend on a changed entity. Run them and name them. A + dependent that no test covers goes in "what is left". +- sem's language coverage is uneven. When it returns `chunk` entities or an + empty dependency graph (Dart, for one), use `jj diff` and do not cite + `sem impact`. The entity-level-git skill lists the known gaps. +- Without sem (most sandboxes and devcontainers), use `jj diff`, and say in + your reply that sem was not available. Do not install it to write a + commit message. + ## Language The body is a "commit body" surface in the ste100 skill: use its From a7f3d635a3a4398888f1aa6feb9a7b3ea6a91102 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 23:05:34 +0000 Subject: [PATCH 7/7] agent(skills): commit-craft works beside ce-commit The compound-engineering plugin is enabled on these machines, and its ce-commit skill also triggers on "commit this". The two skills disagreed. ce-commit adds a body only when the reason is not obvious, and without CLAUDE.md in context it copies the recent log. The hook and `jj push` then refuse its commits. ce-commit also had rules that commit-craft lacked: when to stop, a subject that names the outcome, and paths named on `git commit`. commit-craft gains four of those rules: - Done and stop: done when each change is a commit that passes check-commit-msg and none of the task's changes are left; stop when there is nothing to commit, with no empty commit. - The summary names the outcome, not the files, with a bad and a good example. - With git only, stage files by name and name them again on `git commit -F msg -- `, because a commit without paths takes everything already staged. Never `git add -A`. - A plan unit ID from compound-engineering can end the subject, as in `(U3)`, when the commit implements one unit. A new section says how the two skills work together. ce-commit's step 3 follows the project convention, which here is commit-craft, so the format, the required body and the ste100 wording apply. The agent runs check-commit-msg on ce-commit's message file, and in a jj repository it uses the jujutsu skill's commands instead of ce-commit's `git checkout -b` and staging. CLAUDE.md says the same in one sentence. The push step said to push with `jj push` every time. That disagreed with the jujutsu skill, which says never to push unless Alyssa asks. It now says to push only when asked. Tested: the develop line's tests/commit.bats passes against these files (15 tests, one new for the plan unit suffix), check-skill-frontmatter.sh accepts the skill, and rumdl passes on it. ce-commit was read at compound-engineering-plugin main; its behavior beside this skill was not run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Soievfadhy2k17VT4VecnT --- CLAUDE.md | 3 ++ tools/agents/skills/commit-craft/SKILL.md | 52 ++++++++++++++++++++--- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 64ee5d5c..4e51b1fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,6 +98,9 @@ shape is enforced outside the skill, by the scripts in `tools/commit/`: run which checks the stack first (`tools/jujutsu/config`). A `git commit` in this checkout runs the same check as a hook, and CI checks every commit on a pull request. The jujutsu skill covers only the commands that set the message. +The same applies when compound-engineering's `ce-commit` makes the commit: +commit-craft is the project convention its step 3 defers to, so the body is +required even where ce-commit would leave it out. Two additions for this repository: diff --git a/tools/agents/skills/commit-craft/SKILL.md b/tools/agents/skills/commit-craft/SKILL.md index 189f4037..d67fb800 100644 --- a/tools/agents/skills/commit-craft/SKILL.md +++ b/tools/agents/skills/commit-craft/SKILL.md @@ -19,6 +19,11 @@ own. Two other skills do the work it would otherwise repeat: sem, from the entity-level-git skill, gives the facts (which entities changed), and ste100 gives the words. This skill gives the order. +**Done when** each logical change is a commit whose message passes +`check-commit-msg`, and `jj st` (or `git status`) shows none of the task's +changes left. **Stop when** there is nothing to commit: say so, and do not +make an empty commit. + ## What is enforced, and where | Where | What runs | @@ -59,9 +64,19 @@ Do these steps in order for every commit. 5. Run `check-commit-msg /path/to/msg.txt`. Fix every problem it prints, and run it again until it prints nothing. ste100 can change line lengths, so run it after step 4. -6. Set the message from the file: `jj describe --stdin < /path/to/msg.txt`, - or `git commit -F /path/to/msg.txt` with git only. -7. Push with `jj push`. +6. Set the message from the file: `jj describe --stdin < /path/to/msg.txt`. + With git only, stage the commit's files by name and name them again on + the commit: + + ```bash + git add path/one path/two + git commit -F /path/to/msg.txt -- path/one path/two + ``` + + Never `git add -A` or `git add .`. A `git commit` without paths takes + everything already staged, including changes that are not part of this + commit. jj has no staging area, so this step is git only. +7. Push only when Alyssa asks, and then with `jj push`. ## Subject line @@ -85,8 +100,15 @@ devcontainer(nix): apply home-manager on create These rules are yours to apply: -- The summary says what the commit does (`secure the runner dirs ...`) or - names what it adds (`global justfile with ...`). +- The summary names the outcome: what is now possible or fixed. It does not + list files. `ci(nix): update nix.yml` says nothing a diff does not; + `ci(nix): stop compinit aborting on the runner` says what changed for the + reader. A noun phrase is correct when the commit adds one thing + (`global justfile with the HUID task recipes`). +- When the commit implements one unit of a compound-engineering plan and the + unit ID is already known, end the subject with it in parentheses: + `hm(zsh): add the history options (U3)`. Do not look for a plan to find + one. Leave it out when the commit spans units. - The area is the part of the repository or the tool the change belongs to: `hm` (home-manager), `ci` (`.github/`), `mise`, `nix`, `devcontainer`, `just`, `agent(skills)`, `docs`. Reuse an existing area. Make a new one @@ -201,6 +223,26 @@ Updated agent skills. docs(agents): Instructions for Claude ``` +## With compound-engineering's ce-commit + +The compound-engineering plugin is enabled here, and its `ce-commit` skill +also triggers on "commit this". Its step 3 follows "project commit +conventions already in context". In a repository that uses this skill, that +convention is this skill. When ce-commit or another compound-engineering +skill makes the commit: + +- The subject format, the required body and the ste100 wording apply. + ce-commit says to add a body only when the reason is not obvious. Here the + body is always required, and the hook and `jj push` refuse a commit + without one. +- Run `check-commit-msg` on the message file that ce-commit writes, before + `git commit -F`. +- In a jj repository, use the jujutsu skill's commands for ce-commit's git + steps. Do not run `git checkout -b` or stage files: jj has no staging + area, and a bookmark is made only when you push. +- ce-commit's done and stop rules, and its rule to name paths on + `git commit`, are the same as this skill's. + ## Rewording existing commits To change the message of another commit, run