Skip to content

comment-review

Review the comments in a diff or a subtree and minimize them against the expression ladder: a fact belongs first in the identifier, then in the signature and types, then in the declaration's doc annotation, and only when none of those can carry it does it earn an inline comment. Deletes comments that restate the code, moves facts down the ladder, tightens what survives to one short literal statement, strips volatile project state, and replaces duplicated prose with a reference to one canonical home. Invoke for "review the comments", "too many comments", "comment cleanup", or as the comment pass on a branch before review.

Pluginreview
Invoke /comment-review [diff | <path>… | <PR#>] [--report-only] [--workflow]
Allowed toolsBash Read Edit Glob Grep Task AskUserQuestion Workflow
SourceSKILL.md
Bundled files references/comment-rubric.md , review.workflow.js

Minimize the comments in a body of code by pushing every fact as far down the expression ladder as it will go, and by replacing duplication with a reference.

Usage:

  • /comment-review — review the current branch’s diff against main, plus any uncommitted changes.
  • /comment-review diff — review only what is uncommitted: the working tree and the index against HEAD, plus untracked files. For a pass before you commit.
  • /comment-review src/lib — review every comment in that subtree, committed or not.
  • /comment-review 1742 — review the diff of that PR.
  • /comment-review --report-only — report the verdicts and stop. No edits.
  • /comment-review --workflow — run the judging, duplication, and defence passes as one Workflow script instead of hand-dispatched agents. Without the flag, step 2 judges whether a workflow would help and asks before starting one.

The rules

Every judgement applies the rules below. They live in comment-rubric.md beside this skill, so a code-review pass can apply the same rules to the comments in a diff; the judging agents and the workflow script read that file.

The rules a comment is judged by: where a fact belongs, what earns an inline comment, how a kept comment reads, and when a comment is a duplicate of something with a better home. /comment-review applies them as verdicts; a code-review pass applies them to the comments in a diff.

The expression ladder

A fact about code has exactly one right home. Work down this list and stop at the first rung that can carry it:

  1. The name. A name that states the fact needs no comment stating it. This rung is not just the local identifier — it runs all the way up, and the higher the level, the more readers a good name serves:
    • Variables and constants. // retry three times above const n = 3 is const MAX_RETRIES = 3.
    • Functions and methods. A verb phrase naming the effect and the result. A comment that summarises the body is a rename waiting to happen.
    • Types, fields, and enum members. Status.PendingReview needs no comment saying what the state means; status: string plus a comment listing the values does.
    • Classes. The noun says what the thing is and what it owns. A class that needs a comment to say what belongs in it is usually two classes.
    • Files and modules. The filename is the first thing a reader greps and the last thing they read. A module named for its contents needs no header comment announcing them.
    • Packages and directories. The boundary’s name states what is inside and, by omission, what is not. A small utils holding genuinely miscellaneous helpers is fine — not every function earns a category. What the name has to keep out is the utils that grew a subsystem: once a reader needs a comment to learn what lives there, the directory is a second home for something that deserves its own name.
  2. The signature and the types. Parameter names, a union instead of a string, a non-null return instead of a documented “never null”, an options object instead of three positional booleans. A comment describing what a type already guarantees is a duplicate of the type.
  3. The declaration’s doc annotation. One block on the declaration, for the contract a caller needs and cannot read off the signature: what the function promises, what it throws, what it mutates, what invariant it assumes on entry. Facts a caller needs go here, not scattered inline where a caller never looks. Use the language’s own annotation form rather than a loose block above the declaration — TSDoc /** */ with @param, @returns, @throws; rustdoc /// with # Errors and # Panics; the docstring in Python. The native form is what tooling, hovers, and generated docs read, so a fact written there reaches the caller where a bare // above the line does not. A per-parameter fact belongs in that parameter’s tag, not in an inline comment beside the argument.
  4. An inline comment. Everything left over — and only what is left over.

A name cannot carry an explanation, only a label. When a package or module needs one — how its pieces fit, the protocol its callers follow — that is shared documentation, and the code points at it rather than restating it. See Duplication becomes a reference below for the reference form, and A concept explained in code may belong in documentation for when a block has outgrown the file it sits in.

What earns an inline comment

Only a fact a competent reader cannot deduce from the code in front of them:

  • The why, where the obvious alternative was tried and does not work. Name the alternative and what broke.
  • An empirical fact about the outside world — an API’s undocumented behavior, a browser or runtime bug, a measured cost. Facts nothing in the file can imply.
  • A non-local constraint — an ordering another file depends on, an invariant held elsewhere, a call this code must not make.
  • A deliberate deviation from a convention the reader will otherwise read as a mistake.
  • A pointer to an external cause — an issue, a spec section, a recorded decision.

Everything else is noise. In particular these never earn one: restating the statement below it; naming a block (// loop over the items); describing a parameter the signature already names and types; change history; decorative banners and section rules; a summary of a well-named function’s body; a count or other project measurement that nothing keeps true.

How a kept comment reads

A comment that earns its place still has to be worth reading. A comment that is kept or rewritten obeys these rules:

  • Short. One or two sentences. A comment that needs a paragraph is usually a fact with a home further down the ladder, or one that belongs in documentation.
  • Plain and literal. State the fact directly. No metaphor, no analogy, no jokes — a reader who does not share the reference has to decode the sentence before they can use it, and a metaphor that drifts from the code cannot be detected as wrong.
  • Descriptive, not evaluative. “Runs before the index is built” is a fact. “Careful here!” and “this is tricky” are not; they warn without saying what of.
  • One idea per comment. Two unrelated facts stacked in one block are two comments, each next to the code it is about.
  • Bullets over long prose. Where a block legitimately carries several points — a doc annotation with conditions, a module header — write them as a list. A reader scans a list; they have to parse a paragraph to find the one line they came for. Prose stays where the narrative is the point: reasoning that builds, where each step depends on the one before and the conclusion does not stand without them. Split that into bullets and the argument becomes a set of assertions the reader has to reassemble. A list is for facts that are true independently; a paragraph is for a case that has to be made.

A comment does not pin volatile project state

A count, a version, a date, a list of the files that do something, the name of whoever owns a thing — each is true when written and drifts the moment the project moves, with nothing to notice. The comment is not merely stale afterwards; it is wrong, and it is wrong in a way a reader has no reason to suspect.

Apply one test: does the exact value change what the reader does? Usually it does not, and the sentence is better without it.

# All 67 of these libraries are members of the ROOT bun workspace, so prettier,
# oxlint and tsc come from the root node_modules.

The load is carried by all of them are workspace members. The 67 adds nothing a reader acts on and is wrong on the next library added. Drop the number and keep the sentence. Same for 15 of the 67 declare a build script — some declare a build script; the rest are source-shipped says what the reader needs, and stays true.

When the exact value genuinely is load-bearing — a pinned version another tool must match, a threshold a measurement has to stay under — it is not volatile state, it is a constraint. Keep it, and say what breaks when it changes. When a reader would want the current number, point at the command that computes it rather than writing the answer down.

Duplication becomes a reference

The same explanation written in two places drifts: one site is updated and the other goes stale silently, with nothing to detect it. When one fact is explained in more than one place, pick a single canonical home and make every other site a pointer to it.

  • Choosing the home. For a fact about one function, its doc annotation. For a fact about a mechanism several call sites share, the declaration of that mechanism. For a project convention, the convention doc or standard that owns it — never a source file.
  • The reference form. Code: a relative path and a symbol, as in see writeJsonAtomic in lib/util/fs.ts. Documentation: a relative path to the doc, as in see docs/conventions/logging.md. Both are greppable, and both break loudly when the target moves.
  • Not everything repeated is duplication. Two comments about the same subject that each carry a distinct local fact are two comments. Collapse only when one is genuinely a copy of the other’s content.

Data is the sharpest case. A comment that lists the valid statuses, restates a config default, or spells out a table’s columns is a second copy of that data and will disagree with the first one within a release. Point at the canonical definition — the enum, the schema, the config file — and let the reader read the values there.

A concept explained in code may belong in documentation

When a comment block explains a whole mechanism — the design of a subsystem, a protocol between components, the reasoning behind an architecture — it has outgrown the source file. Before keeping it, search the project’s documentation for the concept — its docs/ tree, its conventions, and wherever it records standards and decisions.

  • The concept is already documented. Replace the block with a relative path to the doc, keeping only the one line the reader of this file needs. Read the target first and confirm it says what you are about to stop saying here.
  • The concept is not documented and deserves to be. Flag the gap, naming the concept, the file, and the doc that should own it. Leave the comment in place until the doc exists — the fact has nowhere else to live yet.
  • The concept is local to this one declaration. It stays, tightened to the rules above. Not every explanation is a document.

Restructuring is in scope. When a comment exists because the code is shaped badly, the fix is to reshape the code, provided the change is behavior-preserving and stays inside the reviewed scope.

1. Resolve the scope and build the inventory

Determine the review scope from the argument:

  • No argument — git diff <default-branch>...HEAD plus git diff HEAD for uncommitted work. Scope is the changed lines and the declarations that contain them. Resolve the default branch rather than assuming main: git symbolic-ref --short refs/remotes/origin/HEAD, falling back to main.
  • diff — uncommitted work only: git diff HEAD for tracked changes, staged and unstaged, plus git ls-files --others --exclude-standard for new files, which are in scope whole. Nothing already committed is reviewed.
  • A path — every source file under it. Scope is the whole file.
  • A number — gh pr diff <n>. Scope is the diff. Run against a checkout of the PR’s head (gh pr checkout <n>) when applying edits, since steps 5 and 6 need the files on disk; --report-only can read the diff alone.

Then build the comment inventory over that scope with Grep, recording for each comment its path, line, verbatim text, and the declaration that encloses it. Exclude generated files, vendored trees, and fixtures. Report the count as N comments across M files and continue; if the count is zero, print COMMENT-REVIEW-CLEAN and stop.

2. Judge each comment — fan out one agent per file

Choose the dispatch mode

Steps 2, 3, and 4 all fan out agents. They run either as hand-dispatched Agent calls or as one Workflow script chaining the three passes. Decide once, here, before dispatching anything:

  • --workflow was given. Use the Workflow tool. The flag is the user’s opt-in; do not ask again.
  • No flag. Judge whether a workflow would help. It helps when hand dispatch would exceed the parallel-agent cap or push every per-file verdict through this session’s context: as a rule, more than 8 files, more than 40 comments, or any subtree scope. When it would help, ask once with AskUserQuestion, stating the file and comment counts and the number of agents each mode runs, with “run as a Workflow” and “dispatch agents directly” as the options. When it would not help, dispatch with Agent and do not ask.

The Workflow tool gates on explicit consent; the flag or the answer is that consent. Under Workflow, the checked-in script carries all three passes — judge per file, then the duplication pass over every verdict, then a defender per deletion — and returns the merged verdicts. The script is review.workflow.js. Pass it by path, not inline:

Workflow({
scriptPath: "<absolute path of review.workflow.js, beside this skill file>",
args: {
rulesPath: "<absolute path of references/comment-rubric.md, beside this skill file>",
files: [{ path, comments: [{ line, text, declaration }] }, …],
docsRoots: ["docs/"],
scope: "<one line naming what is under review>"
}
})

It returns { files: [{ path, verdicts }], docGaps, homes, counts }, each verdict already merged through steps 3 and 4. Take it to step 5. Batch the inventory by comment count so one invocation stays inside the session’s workflow agent guideline, and run the batches in sequence.

The verdicts

Give each judging agent the file and the rules. Each returns one structured verdict per comment:

VerdictMeans
promote-to-nameThe fact belongs in an identifier. Names the rename.
promote-to-signatureThe fact belongs in a type or parameter. Names the change.
promote-to-annotationCaller-facing; move it to the declaration’s doc block.
dedupe-to-referenceRestates a fact with a canonical home. Names the home.
promote-to-docExplains a whole mechanism. Names the doc that should own it, and whether it already exists.
tightenEarns its place, but only part of it does. Gives the shorter text.
deleteRestates the code, or is stale, or is wrong.
keepCarries a non-deducible fact, stated once, at the right length.

Every non-keep verdict carries the concrete replacement — the new name, the new type, the target declaration, the reference target, or the rewritten text. A verdict with no replacement is not actionable; the agent returns keep instead.

tighten is the verdict for a comment that carries a real fact but breaks one of the reading rules or pins a volatile value; it carries the shorter text, with the value removed. delete only when the value was the whole comment. A comment that explains a concept with no doc to point at stays in place and is reported as a documentation gap.

Agents also flag any comment that is factually wrong about the code beneath it as delete or tighten with the correction, regardless of whether the diff touched it. A stale comment is worse than no comment, so fix it on sight.

3. Cross-file duplication pass

One agent reads the full inventory at once and reports groups of comments that explain the same fact in different places. For each group it names the canonical home and the reference each other site should carry. This pass needs every comment together, so it runs after step 2 completes rather than per-file.

Merge its groups into the step-2 verdicts: a comment in a group gets dedupe-to-reference unless step 2 already marked it delete.

The same agent resolves every promote-to-doc verdict, because only it can see whether two files explain the same mechanism. For each, it searches the project’s documentation roots for the concept and returns either the existing target — which turns the verdict into dedupe-to-reference against that target — or a documentation gap, which becomes a reported finding and leaves the comment in place.

4. Verify the deletions — the expensive error is losing a fact

Deleting a comment that carried a hard-won fact costs more than leaving a redundant one, and the loss is invisible afterwards. So every delete and every dedupe-to-reference gets one independent agent whose job is to defend the comment: it reads the code without the comment and argues that the fact is not deducible, or that the named canonical home does not actually contain it.

When the defender succeeds, downgrade the verdict to tighten or keep. When uncertain, keep. promote-* and tighten verdicts skip this pass — they move a fact rather than dropping it.

5. Apply

Work file by file, applying surviving verdicts with Edit. Three constraints:

  • A rename must be complete. Apply promote-to-name only when every reference to the identifier is inside the reviewed scope; otherwise downgrade to tighten and note it. A half-applied rename is a broken build.
  • Behavior does not change. promote-to-signature is a type-level or argument-shape change only. If the fix needs a semantic change, drop it and report it as a finding instead.
  • Skip rather than argue. A verdict you judge to be a false positive is noted as skipped, with one line of why.

Under --report-only, stop here and report the verdicts without editing.

6. Verify the result

Run, and do not proceed past a failure:

git diff --stat

Confirm the diff touches only comments, doc blocks, identifiers, and types — no control flow, no literals other than renamed constants. Then run the affected packages’ own type checks and tests, whatever the project’s task runner calls them.

A green suite after a comment pass is the whole gate: a rename that missed a call site or a signature change that broke a caller shows up here and nowhere else.

7. Report

Print a terminal marker and then the summary:

COMMENT-REVIEW-DONE removed=<n> promoted=<n> referenced=<n> tightened=<n> kept=<n> doc-gaps=<n>

Follow it with the count per verdict, the renames applied, the canonical homes the duplication pass chose, and every verdict skipped with its reason. Then the net line count the comments lost. End with the documentation gaps: each concept that outgrew its source file with no doc to point at, its location, and the doc that should own it.

Use COMMENT-REVIEW-CLEAN instead when no comment needed changing, and ERROR reason="..." when the scope could not be resolved or the checks stayed red.

Failure modes

  • The ladder used as license to delete. The goal is fewer comments carrying more, not fewer comments. A pass that deletes half the comments and promotes nothing did not do the work.
  • Renaming past the scope boundary. The most tempting rename is usually the one with call sites across the repo. Step 5’s first constraint exists for it.
  • Rewriting a comment you did not understand. A comment whose fact you cannot place is a keep, not a delete. Not understanding it is evidence it carries something.
  • Reference to a home that does not say it. Before writing see X, read X and confirm the fact is actually there. If it is not, put it there first.
  • A workflow started on the assessment alone. Step 2’s assessment decides whether to ask, never whether to run. Without --workflow or a yes, the passes run as Agent calls.

Notes

  • Read-and-edit only on comments, names, types, and doc blocks. This skill never changes behavior; a simplification pass restructures code and a bug-finding pass fixes defects.
  • Terminal markers are namespaced COMMENT-REVIEW- so a caller can grep for them.
  • The skill does not commit. Landing the change is the caller’s step, so a comment pass can ride on the branch that motivated it.