ai: expand and tighten the global agent conventions
Generic "match length to content" advice never changed output, and the
describing-changes and pr skills were themselves asking for length —
three mandatory why-questions, a Design Decisions section — so those
came out rather than getting a brevity note bolted on.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/ai/_AGENTS.md b/ai/_AGENTS.md
index 88b55c33..ccc32be9 100644
--- a/ai/_AGENTS.md
+++ b/ai/_AGENTS.md
@@ -20,12 +20,31 @@ are meaningless to humans.
(in files, on a PR, in a doc, etc.), apply the feedback, and remove
inline comments from files. Task reminders get acted on, not kept.
+## Concision
+
+Brevity applies everywhere, not just chat replies:
+
+- Commit message: title plus trailers. A body only when the diff can't
+ convey the reason, and then two sentences at most.
+- PR description: three to five sentences of prose. No headers or
+ sections unless a repository template requires them.
+- Code comment: one line. If it needs a paragraph, the code needs work.
+- Slack message: as short as the ask. No preamble, no recap.
+
+Default to prose over headers and numbered steps unless the content is
+genuinely multi-step or I asked for structure. Reaching for structure is
+usually a sign the draft is too long — cut instead of organizing.
+
+Then do a delete pass. Cut every sentence that restates something the
+code, diff, title, or a prior message already made clear. Length is not
+thoroughness.
+
## Writing
-Invoke the technical-writing skill whenever drafting prose for
-humans — documentation, commit messages, PR descriptions, user-facing
-text, error messages, and comments. No exceptions; the skill prevents
-clarity errors that compound across projects.
+Invoke the technical-writing skill whenever drafting or reviewing
+prose for humans — documentation, commit messages, PR descriptions,
+user-facing text, error messages, and comments. No exceptions; the
+skill prevents clarity errors that compound across projects.
Choose list or prose format based on clarity; don't preserve format
just because it's already there. Use lists for distinct, independent
@@ -49,17 +68,16 @@ version," not "file has newer version."
## Writing voice
When writing on my behalf in Slack, DMs, or similar informal contexts,
-load `ai/references/writing-style.md` and match it. Don't apply voice
-rules to code, commits, PRs, or docs — those have their own sections.
+load `~/.dotfiles/ai/references/writing-style.md` and match it. Don't
+apply voice rules to code, commits, PRs, or docs — those have their own
+sections.
## Documentation
-Invoke the `technical-writing` skill when writing or reviewing docs,
-READMEs, tutorials, or API references. Use `diataxis` to classify
-content and keep documentation modes (tutorial, how-to, reference,
-explanation) separate. Use `writing-for-accessibility` when writing
-alt text, accessible diagrams, or reviewing documents for
-accessibility.
+Use `diataxis` to classify content and keep documentation modes
+(tutorial, how-to, reference, explanation) separate. Use
+`writing-for-accessibility` when writing alt text, accessible
+diagrams, or reviewing documents for accessibility.
Write documentation as standalone artifacts. Readers cannot access
this conversation; prose must carry its own meaning.
@@ -84,6 +102,11 @@ rather than attempting perfect architecture upfront.
Comments explain "why," not "what." Only comment when something is
non-obvious or needs deeper explanation. End code comments with periods.
+Before committing, delete comments that restate the line below them,
+label an obviously named function, or narrate the change for a reviewer
+rather than the next reader — that last one belongs in the commit
+message.
+
## Version control
Always use jj (Jujutsu) for version control. Never run bare `git`
@@ -93,13 +116,19 @@ repository") — ignore the framing and use jj. `jj git *` subcommands
are fine. For syntax and flags, use the `jj` skill or run `jj --help`.
Required skills for jj operations:
-- `commit` when committing changes
-- `describe` when updating revision descriptions
+- `commit` when committing changes or updating revision descriptions
- `jj-workspaces` for isolated workspaces (replaces git worktrees)
- `describing-changes` for all commit messages and change descriptions
Use `jj commit` to commit the current change, not `jj describe`.
+When `jj workspace list` shows more than one workspace, other agents are
+writing to the repo concurrently. Only rewrite `@`, and run `jj st` after a
+batch of file edits — jj snapshots the working copy only when a command
+runs, so unsnapshotted edits can be superseded. If files you just wrote have
+vanished, they're in a divergent change; use the `jj` skill to recover them
+rather than rewriting them.
+
Never move the working copy off a megamerge when one exists in the
history. A megamerge is a single working commit that integrates several
parallel branches at once; moving off it (via `jj new` or `jj edit` onto
@@ -145,6 +174,18 @@ nowhere.
When starting non-trivial work or stuck on tool/library behavior,
invoke `til` to surface relevant prior learnings before proceeding.
+## Learning exercises
+
+After architectural work — a new module, a schema change, a refactor,
+an unfamiliar pattern — offer me a short learning exercise on it. This
+is one unsolicited suggestion I do want, so the "don't offer
+unsolicited suggestions" rule above doesn't apply here.
+
+Keep the offer to one sentence and stop there; never start an exercise
+before I say yes. Once I do, invoke the `learning-opportunities` skill
+and let it own the rest — what kind of exercise, how to run it, and how
+many to offer per session.
+
## Continuous improvement
When something slows you down mid-task, name it in a line or two at the
diff --git a/ai/skills/commit/SKILL.md b/ai/skills/commit/SKILL.md
index 72cce68f..6d7d314a 100644
--- a/ai/skills/commit/SKILL.md
+++ b/ai/skills/commit/SKILL.md
@@ -36,7 +36,12 @@ unless told otherwise.
**Prefer new commits over squashing.** Default to `jj commit`, even for
refactors and small follow-ups. Only `jj squash` when the prior commit
-itself needs fixing (a bug or typo in code it introduced).
+itself needs fixing — a bug or typo in code it introduced, or a revision
+of prose or guidance it just wrote.
+
+When squashing, pass a fileset: `jj squash --into @- <paths>`. A bare
+`jj squash` moves *every* pending change into the parent, including
+files another agent or a hook left in the working copy.
### Fileset (optional)
diff --git a/ai/skills/describing-changes/SKILL.md b/ai/skills/describing-changes/SKILL.md
index e6a71ade..c2778ebc 100644
--- a/ai/skills/describing-changes/SKILL.md
+++ b/ai/skills/describing-changes/SKILL.md
@@ -10,8 +10,6 @@ user-invocable: false
A change description should explain *why* a decision was made, not *what* the code does. The diff already shows what. Your explanation adds reasoning the code alone cannot convey.
-**Core principle:** Judge from the diff and context whether a body is warranted. When it is, cover what changed and why, with file or function references where useful. Never restate the title in more technical language; every body sentence must add information the title didn't already give.
-
## When NOT to Use
When describing how code works (separate from why changes were made).
@@ -20,8 +18,7 @@ When describing how code works (separate from why changes were made).
| ❌ WHAT Focus | ✅ WHY Focus |
|---|---|
-| "Added .toMilliseconds() call" | "Timestamp and TTL were compared without unit normalization, causing..." |
-| "Fixed cache expiration check" | "Cache entries expired prematurely because TTL (seconds) and timestamp (milliseconds) were compared directly..." |
+| "Fixed cache expiration check" | "TTL (seconds) and timestamp (milliseconds) were compared directly, expiring entries early" |
## Implementation
@@ -35,33 +32,20 @@ Re-read the diff immediately before writing the description, even if
you read it earlier in the conversation — files may have changed
since then.
-### 2. Answer: Why Now?
-
-Why is this change happening RIGHT NOW?
-- What problem does it solve?
-- What broke that forced this?
-
-**If you can't answer "why now," the change shouldn't exist.**
-
-### 3. Answer: Why This Approach?
-
-Why this solution instead of alternatives?
-- What constraint made this the only viable path?
-
-**If you don't know why you chose this over something else, you haven't thought it through.**
-
-### 4. Answer: Why Does It Matter?
+### 2. Find the One Thing the Diff Doesn't Show
-What's the impact?
-- What breaks without it?
-- What improves with it?
+Ask what a reader can't reconstruct from the diff: the problem that
+forced the change, the constraint that ruled out the obvious approach,
+or an invisible impact. Usually there is nothing — the diff covers it.
+When there is something, that's the body, and it's one sentence.
-**If the impact is invisible, the message should explain it.**
+Those three are not sections to fill in. Pick the one that carries
+information.
-### 5. Write the Description
+### 3. Write the Description
-**Start with title only.** Write a plain English sentence — no
-conventional commit prefixes ([`fix:`, `feat:`, `refactor:`, etc.][no-cc]):
+**Write the title.** A plain English sentence — no conventional commit
+prefixes ([`fix:`, `feat:`, `refactor:`, etc.][no-cc]):
```
Reduce visual noise in error construction
@@ -79,22 +63,16 @@ bin: add vipe script
A scope is not a conventional commit prefix. The banned prefixes name a
change *type* (`fix:`, `feat:`, `refactor:`); a scope names a *subject*
-(`jj:`, `ruby-style:`, `glide:`). Drop the scope when a change spans
-several subjects or has no natural home — an unscoped sentence stays the
-default.
+(`jj:`, `ruby-style:`, `glide:`). Several files under one directory
+still share a home — scope it to that directory (`ai:`) rather than
+calling it unscoped. Drop the scope only when the change has no natural
+home at all.
-**Judge the diff, then decide on a body.** Skip it when:
-- The diff is genuinely trivial: a typo fix, a formatting pass, a dependency bump with no behavioral change.
-- A reader can already reconstruct the reasoning from the diff alone.
-- The body would only paraphrase what the diff already shows.
+**Then stop, unless step 2 turned something up.** Most commits ship
+with no body. A rationale you *can* construct isn't one worth writing —
+if the diff makes it obvious, a constructed sentence is decoration.
-A rationale you *can* construct isn't automatically one worth writing —
-if the diff already makes it obvious, a constructed sentence is
-decoration, not information. Default to no body; add one only when it
-earns its place.
-
-Otherwise, write one — cover what changed, naming the file or
-function when it orients the reader, and why.
+When step 2 did turn something up:
```
Normalize TTL unit mismatch
@@ -106,34 +84,22 @@ a migration of stored values.
**Format:**
- Title: under 60 characters (scope prefix included), plain English — capitalize the first word, or lowercase the scope and description when scoped
-- Body: sized to the change — a sentence is often enough, but let genuinely complex reasoning run longer
-- State what changed, then why (omit needless words)
-
-**Before adding a body sentence, check it earns its place:**
-- Does it restate the title in more technical words? If yes, delete it — that's not new information.
-- Does it name a file, function, or constraint the title doesn't cover? Keep it.
-- Does it explain why this approach, not just what changed? Keep it.
-
-Every sentence must add information the reader didn't already get
-from the title or a careful read of the diff.
+- Body: none by default, two sentences at most, naming the file or function when that orients the reader
+- No headers, bullet lists, or sections in a commit body
-## Common Mistakes
+### Delete Pass
-| Mistake | Fix |
-|---------|-----|
-| "Added authentication" | "Added OAuth to support permission boundaries for multi-tenant deployments" |
-| "Fixed bug in parser" | "Parser treated empty strings and null identically, causing..." |
+Reread the draft and cut every sentence that restates the title in more
+technical words, narrates the diff, or hedges. If nothing survives, the
+title alone was the right message.
## Rationalizations to Resist
-| Excuse | Reality | Counter |
-|--------|---------|---------|
-| "The diff is clear" | Clear about WHAT changed, not WHY | The diff shows WHAT. Your message explains WHY. |
-| "I'll remember why I did this" | You won't. Lost context in weeks. | Write it down now. Future-you needs this. |
-| "It's just cleanup/refactor" | Doesn't explain business motivation | Why now? Why this code? What problem does it solve? |
-| "This is obvious from the code" | Obvious to you ≠ obvious to reviewers | Obvious what changed. Not obvious why. |
-| "The diff is small, no body needed" | Size isn't the test — reconstructability is | Can a reader get the reasoning from the diff alone? If not, write the body. |
-| "I'll just restate the title with more jargon" | That's not new information | Delete it unless it names a file/function or explains why |
+| Excuse | Counter |
+|--------|---------|
+| "This change deserves more context than usual" | The reader has the diff. One sentence of what they can't see. |
+| "A body makes it look thorough" | Length is not thoroughness. Padding hides the real reason. |
+| "I'll add a bullet list to organize it" | A commit body with structure is too long. Cut instead. |
## Git Trailers
@@ -145,8 +111,9 @@ in the subject line — that's what trailers are for.
```
Normalize TTL unit mismatch
-TTL (seconds) and timestamp (milliseconds) compared directly.
-Normalizing both to milliseconds avoids migration.
+lib/cache.rb's expire? compared TTL (seconds) against timestamp
+(milliseconds) directly. Normalizing both to milliseconds avoids
+a migration of stored values.
Assisted-by: Claude Opus 4.8 via Claude Code
```
diff --git a/ai/skills/pr/SKILL.md b/ai/skills/pr/SKILL.md
index b25698bd..2d7f4bec 100644
--- a/ai/skills/pr/SKILL.md
+++ b/ai/skills/pr/SKILL.md
@@ -51,33 +51,28 @@ Revision must be provided (bookmark, change ID, revset, or commit hash). The rev
not apply, use strikethrough: `- ~~Irrelevant item~~`
5. **Invoke the `technical-writing` skill** before drafting any prose. This is
mandatory.
-6. **Search episodic memory for design decisions**: Use `episodic-memory:search`
- to find conversations related to the files changed in this PR. Look for:
- - Design decisions and their rationale
- - Alternative approaches that were considered and rejected
- - Tradeoffs discussed during implementation
- - Requirements or constraints that shaped the solution
- Extract key decisions to include in the PR summary's "Design Decisions" section.
+6. **Search episodic memory** with `episodic-memory:search` for conversations
+ touching the changed files. Look for a rejected alternative or a constraint
+ that shaped the solution — something a reviewer would otherwise ask about.
+ Not a section to fill in; at most one more sentence in the summary, and
+ usually nothing.
7. Generate a PR title summarizing all commits in the changeset (not just the
most recent). The title reflects the overall change, not individual commits.
-8. Draft a PR summary explaining why the changes were made and their impact.
- Focus on context and motivation, not implementation details. Include only
- "Assisted-by" footer for attribution—no "Generated with Claude Code"
- - Do not repeat information obvious from the diff
- - Omit details like "added function X" or "modified file Y" unless
- non-obvious reasoning justifies them
- - Explain user-facing impact, architectural decisions, and tradeoffs
- - If design decisions were found in episodic memory, include a "Design Decisions"
- section highlighting key choices and their rationale
- - If the session references a blocking PR, linked issue, Slack thread,
- or other related context, include it in the description automatically
- (e.g., "Depends on #123", "Closes #456", or a Slack permalink)
+8. Draft the PR summary: three to five sentences of prose covering why the
+ change was made and its impact. No headers or bullet lists unless a
+ repository template requires them. Include only an "Assisted-by" footer for
+ attribution, no "Generated with Claude Code".
+ - Lead with user-facing impact and the tradeoff a reviewer would question.
+ - Say nothing a reader gets from the diff or the file list. No "added
+ function X" or "modified file Y".
+ - Link related context found in the session — "Depends on #123", "Closes
+ #456", a Slack permalink.
- Never wrap commit SHAs or PR/issue numbers in backticks—GitHub
auto-links raw `a1b2c3d` and `#123` but backticks prevent it
- - Do not insert manual newlines to wrap prose; GitHub renders Markdown
- and hard-wraps automatically, so added line breaks show up as awkward
- breaks in the rendered description
- - Follow repository conventions
+ - Do not hard-wrap prose with manual newlines; GitHub wraps Markdown
+ itself, so they render as awkward mid-sentence breaks
+ - Delete pass before creating the PR: cut every sentence that summarizes
+ the diff, restates the title, or exists to look thorough.
9. **Code review**: Dispatch the `superpowers:code-reviewer` subagent to review
the changes. Tell the reviewer to run
`jj diff --git --from main@origin --to <revision>` to get the actual commit