tend-prose

Review the prose you added in recent work — code comments, docstrings, and Markdown — against four equal defects: **existence** (it should not have been written at all), **durability** / **narration**

SKILL.md

GitHub

You are reviewing prose you recently added — code comments, docstrings, and Markdown (skill bodies, docs/, READMEs) — for four defects that carry equal weight:

  • Tightness — text that spends more words than it informs: it restates what the function name, signature, and types already convey, or stretches a real point past the length it needs. CLAUDE.md's docstring rule: document only the non-obvious contract (side effects, runtime constraints, cross-boundary coupling, don't-change-this traps).
  • Existence — text that should not have been written: a description of how the code works rather than a constraint on it, something a reader recovers from the code itself, or a real constraint filed where nothing will load it.
  • Negation — text whose subject is a thing the change removed, surviving as its own denial instead of being removed altogether. It is written in clean present tense, so it reads as a constraint and no change verb fires. The idiom is don't think about the polar bear.
  • Durability — text that describes the change that produced the code rather than the code's durable behaviour, whether it narrates against the state before the branch or against an earlier step inside it. CLAUDE.md: "Comments describe the code's lasting contract, not the change that produced it."

They are listed in the order that spells TEND, which is a memory aid only. Step 2 has no order — it applies all four to each line in one pass — and Step 3 fixes in the opposite direction from this list: deletions before rewrites before trims, so you never shorten a sentence you are about to cut.

The output is edits, not a report. Fix the clear cases in place; only ask about genuinely ambiguous ones.

This skill is the long version of CLAUDE.md § "Writing things down", and the only home for these rules.

Single-lens mode

The argument may name one lens, optionally followed by focus guidance: /tend-prose durability, /tend-prose tightness src/db, /tend-prose polar bear.

  1. existence
  2. durability, alias narration
  3. tightness, alias bloat
  4. negation, alias polar bear

Then run only that lens: skip the other three in Step 2 and apply only its fixes in Step 3. This is the mode for "I just saw narration in there", and for a bare polar bear here on a PR comment — a targeted pass, not the full sweep.

With no lens named, all four run.

When to use

  • After an autonomous run (several commits since the operator last engaged), before handing back — all four defects accumulate step-to-step.
  • When the scoped work removed a module, file, field or concept that prose described. That is the condition lens 4 exists for, and the only one that puts a defect in prose nobody rewrote.
  • Any time you notice a comment reads like a diff note, or a docstring restates its own signature. If that is the whole complaint, name the lens.
  • Skip if recent work added no prose (pure logic/test edits with no new comments).

Step 1: Scope to what you added since the operator last engaged

In order:

  1. Uncommitted changes: git diff HEAD + untracked files (git status --short).
  2. Commits since upstream / since the operator's last turn: git log --oneline @{u}..HEAD, or the SHA range the session summary names. git diff <range> is the scope.
  3. Nothing matches → tell the user there's nothing to review and stop.

Only the added/changed prose lines are in scope for lenses 1–3 — don't rewrite pre-existing comments you didn't touch. Lens 4 is the exception, and reads the other side of the same diff:

Collect the removed nouns once the range is fixed. From the removed lines (git diff <range>) and the deleted files (git diff --diff-filter=D --name-only <range>), list what the change took away: identifiers, file paths, field and type names, and named concepts. Grep each against the post-change tree. A name that survives only inside prose is a lens 4 candidate — the code no longer has it, so any sentence still about it is about nothing.

Skip this sub-step when the diff removes nothing — most additive work — so the ordinary pass pays nothing for the lens.

Step 2: Read each added prose line and ask all four questions

A line can fail any of the four, or several. Work through the diff once, applying all four lenses to each comment — not one traversal per lens. (In single-lens mode, only the named one.) Lens 4 also ranges over prose the diff did not touch, since a sentence that survived a removal is exactly the one nobody rewrote.

Lens 1 — should it exist at all?

The default is not to write it. Prose costs context on every session that loads it, and it goes stale invisibly — a constraint survives a refactor, a description of how the constraint works does not.

Keep it only when all three hold:

  1. It is a constraint or an accepted cost, not a description of how the code works.
  2. It is not recoverable from the code and its docstrings by someone reading them — or recovering it means taking in more modules and flows at once than anyone holds in their head, where a paragraph gets there faster. Even then, the candidate is a docstring at the topmost point of the code, not a separate file.
  3. Getting it wrong breaks something — you can name the specific way a future editor goes astray without it.

Then check it is in the right home, and move it if it isn't — a rule that only one call site can violate is a comment; a page nobody's glob reaches is nothing. The homes, in order of preference — take the first that fits:

  1. A comment at the call site — a trap that lives at one line.
  2. A docstring at the topmost point of the code it describes — how a flow works, when following it means holding more modules in your head than fit at once.
  3. The skill or hook whose run it governs — a step, or a rule that only binds while that process runs.
  4. The directory's own CLAUDE.md — an obligation every edit in that directory's files must respect, loaded on the first read there.
  5. .claude/rules/<area>.md (frontmatter paths:) — the same, for files no single directory bounds — '**/*.test.ts', several scattered paths.
  6. A colocated README.md — the big picture, for when not having it is itself what blocks the edit.
  7. A runbook page (docs/runbook/*.md, or the project's equivalent) — a procedure a human executes.

A line in the root CLAUDE.md answers one question before any of these: CLAUDE.md § "About this file"'s test, since that file loads on every turn. A line that fails it moves to the first home above that loads when its process runs.

The alternatives already live in the PR or issue thread, and any of these homes can cite it. Add a #1234 beside the line where a reader would otherwise stop and wonder "hmm, why this and not the obvious thing?"; leave it off where nobody would ask, because a citation on an unsurprising line is one more thing to chase.

Rule or README? A rule — homes 4 and 5 alike — is what every edit in the files it covers must respect whether or not the editor went looking — an obligation you can violate without noticing. It loads automatically, so it must stay short and always-relevant. A README carries the part the code cannot: why the arrangement is this one, where reading the modules end to end still leaves the reason obscure. It is opened on purpose, by someone who has already noticed they are lost, and it runs as long as that explanation takes. The default is a rule; reach for a README when the obscure why is the thing worth writing, and spending every matched edit's context on it would be the wrong trade.

Never create a new top-level doc without asking the operator — and when you ask, argue the "don't" side. A top-level doc is the one home nothing scopes, so every later session pays for it; an operator waving it through is easy to get and is not the same as it being right. If nothing in the list fits, that is usually the signal it does not need writing at all.

Touching documented code is not a reason to edit the doc. Edit a rule when the constraint changed — not when code near it moved, was renamed, or grew a feature. A rule that names a function, module path or column is already suspect: name it only when the rule is about that name.

Lens 2 — is it narration?

A line is narration when it only makes sense to a reader who knows an earlier state of the code — the state before the branch, or a step inside it. Tells:

  • Change verbs anchored to the past: "no longer", "now also", "used to", "previously", "migrated from", "this used to…", "as of this change", "renamed from".
  • "for now" / "temporarily" / "once X lands" — situational hedges that go stale.
  • References to a prior step, ticket, or version as if the reader is watching the diff.

The test: would this sentence still be true and useful to someone reading the final code a year from now, with no memory of the edit? If it only informs "what changed", it's narration.

The special case is narration against an intermediate step inside the branch. In shipped prose it is the same defect as the rest of the lens. It earns a name because it is the half that survives in the documents Step 4 excludes: a commit body is meant to narrate the branch against the state before it, and still has no business reporting what a draft became on the way there (@.claude/skills/squash-message/SKILL.md owns that pass).

Lens 3 — is it bloat?

A line is bloat when it costs a reader more than it tells them — either they already had the fact from the name, signature, and types, or the fact is real but buried in twice the prose it needs. Tells:

  • Restates the signature — names the params or return type, or paraphrases the function name (/** Fetches the user by id. */ over fetchUserById(id: string)).
  • Narrates the next lines of code — "Loop over the items and set the flag", "If the list is empty we skip".
  • Restates a type's or enum's members in prose sitting next to the declaration.
  • Ceremony — "This function is responsible for…", "Helper that…", "Note that…" wrapped around one actual fact.
  • A docstring block on something that crosses no boundary — the contract is already obvious from name + types, so the block is decoration.
  • Length: > 4 lines is almost certainly too much; find the one or two facts that aren't self-evident and keep only those. Even exactly 4 is too much unless it's a proper docstring block on a function/class/etc. A good inline clarification fits ~2 lines. The point is often real and the prose still too long:
    • "We await this before returning, because otherwise the transaction may still be open by the time the caller commits, which can deadlock under concurrent writes." → "Await before returning — an open transaction here deadlocks concurrent writes."
    • "It's worth noting that this cache can only be used on the server, since the key includes the workspace id and the browser bundle has no access to it." → "Server-only: the cache key needs the workspace id."
  • Markdown: an added paragraph that re-explains what the adjacent bullet or code block already shows; a "Note:" restating the rule directly above it.

Lens 4 — is it about a thing that isn't there?

When a change removes something the prose described, the reflex is to negate the sentence in place rather than delete it. The mention survives as its own denial, and every later reader pays for a thing that is not there. Tells:

  • A subject from Step 1's removed-noun list. The name survives only inside prose; grep says the tree no longer has it.
  • A negated predicate in clean present tense — "X is not a Y", "nothing generates X", "X is not a segment". No change verb, which is exactly why lens 2 waves it through. Step 3's negator sweep is its search.

The test: with the sentence gone, would its subject have crossed the reader's mind at all? If the sentence is the only reason they are thinking about the thing, the sentence is what put the bear there — which is Dostoevsky's point in Winter Notes on Summer Impressions: set yourself the task of not thinking about a polar bear, and the cursed thing comes to mind every minute. Denying a removed thing sets the reader that task.

The discriminator — constraint or residue. This is where the lens over-deletes if you run it on the tells alone, because a codebase's prose is properly full of negative rules. Apply the test and the two separate cleanly: a constraint's subject is something the reader reaches for anyway, so it crosses their mind with or without the sentence.

  • "Dev artifacts go under gitignored tmp/, not as new .gitignore entries." — a constraint. The next editor reaches for a .gitignore line whether or not anything was ever removed. Keep it.
  • "Copy is not a segment — every string sits in the module that renders it." — residue, if the copy catalogue was just deleted. Nobody reading the current tree would propose a catalogue; the sentence exists to answer the draft that had one. Cut it.

Same grammar, opposite verdicts: residue guards against something a previous draft did, which no longer threatens anyone.

A change whose point is removing something is where bears breed, and whoever has the removed thing in mind — its author, or a reviewer reading the - lines — cannot tell whether a fresh reader would think of it. On such a change, judge each candidate against the resulting file with the diff closed, and read every line touching the removed behaviour's situation, not only the negator hits: once a skip for branches that hold all the work is gone, "it runs after every turn, a branch that holds all the work included" denies the skip without a negator. A call still close after that goes to the operator, who reads the result rather than the history.

Step 3: Fix

Existence:

  • Delete when the line describes how the code works, or restates something a reader recovers from the code — that is the most common outcome and it is a clean one.
  • Move when the constraint is real but mis-homed: a rule only one call site can violate becomes a comment there; a paragraph of orientation in a rule becomes a colocated README; a procedure becomes a skill. State it once in its new home rather than in both.
  • Keep a named trap beside the thing it traps, and a decision's alternatives in the PR thread with a #1234 from the docstring that needs them.

Narration:

  • Rephrase to a present-tense property when the underlying fact is durable: "The emoji is no longer parsed" → "Nothing parses the emoji." "Now also sets x" → "Sets x."
  • Delete when the note carried no lasting information beyond the change itself ("Migrated from the old helper", "Renamed for clarity").
  • Keep genuine forward-references to still-pending work (e.g. "§F2 is deferred") and durable rationale phrased in the present ("kept in sync by hand because…"). These are contracts, not changelog.

Bloat:

  • Cut to the non-obvious facts — the side effect, the runtime constraint, the coupling, the trap. Drop every clause the name and types already carry.
  • Downgrade a docstring block to a one-line inline comment inside the function when its only real purpose is a don't-break-this warning — CLAUDE.md says the dev editing the code will see it there, and that's enough.
  • Delete entirely. This is a normal outcome and often the right one; a comment the code already makes true earns nothing by being shortened.
  • If you can't say it in ~2 lines, question whether the code should be clearer rather than the comment longer.

Negation:

  • Delete. A sentence whose whole content is that something is absent has nothing to restate positively, so this lens cuts where the others rewrite.
  • Keep the fact, drop the denial where the mention carries a live fact alongside it. Rephrase as what is there: "The mark is not generated — public/aeapp-mark.svg is committed" → "One committed public/aeapp-mark.svg serves every consumer", the page, the favicon and the Open Graph card alike.
  • Keep a negative sentence that passes the constraint-vs-residue discriminator. A rule against a temptation the reader has anyway is a constraint, and cutting it is the false positive this lens must not produce.

Resolve in this order — existence, negation, durability, tightness (1, 4, 2, 3): the two deletion verdicts first, so no rewrite or trim is ever spent on a line that is about to go.

Then sweep the result for negators. The order above runs lens 4 before the lens that manufactures bears: durability's own fix turns "the emoji is no longer parsed" into "nothing parses the emoji". So once every fix is applied, grep the resulting text of each prose line in scope — every added line, plus whatever lens 4 read outside the diff:

git diff -U0 <range-start> -- <files> | grep '^+' \
  | grep -niE "\b(not|no|nothing|none|neither|nor|never|nobody|without|cannot)\b|n't\b"

<range-start> against the working tree, so the uncommitted fixes are what gets read. Only comments and Markdown prose count: a flag (--no-sandbox), a string literal or a code span is code, and code is never a bear.

The grep proposes; the discriminator decides. Most negative sentences in a codebase are constraints, so every hit goes through the constraint-vs-residue test above, and only residue is fixed, by the negation fixes. A no longer hit is a durability miss: rephrase it there, then put the rephrasing through the same test.

The sweep belongs to lens 4 and runs wherever it does. It also runs after a durability single-lens pass, over the lines that pass rewrote.

Step 4: Do NOT touch

  • Commit messages and PR bodies — narrating the branch is their job.
  • docs/plans/*.md — transient by nature, swept at finalize.
  • docs/remove-before-merging/<branch>/squash-message.md — a commit body, and transient by nature.
  • CHANGELOG / release-notes files — a dated record of changes is the point.
  • Deliberately thorough reference prose — length carrying rationale the reader can't get elsewhere is not bloat; length carrying nothing is. But that describes a colocated README.md, opened on purpose, not a directory's CLAUDE.md or a .claude/rules/*.md file: a rule's length is a budget, because it loads in full every time a file it covers is read. Paragraphs of rationale in a rule are a lens 1 finding — the orientation belongs in a README the rule links to.

Step 5: Commit

Commit the edits, with the fixes and their reasons in the body.