Document code
Development tools backing a-novel and a-novel-kit. Home of a-novel CLI and AI skills.
npx -y skills add a-novel-kit/stack --skill document-codeAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Write and improve code documentation. Use whenever asked to document code, add comments, write a doc comment, annotate a function, document a package, explain a file's purpose, or clean up unclear or outdated comments — however phrased ("doc this"). Applies to Go, Bash/shell, YAML, Svelte, SQL, TypeScript, and other source or config files. Invoke before writing any documentation, even for one function.
SKILL.md
28.4 KB, as published. Nobody here has run it
Code Documentation Skill
This skill governs how to write, improve, and maintain code documentation. Help the reader understand what something does and how to use it, not how it is implemented.
Document at the element level. Every exposed element — type, function, target, value, field — documents itself directly; never describe elements in an aggregate block above them. A section header listing its targets, a type doc enumerating its variants, a struct doc itemizing its fields all put the doc in the wrong place: the reader looks at the element and finds no comment, while the description sits paragraphs above with all the others. This applies everywhere — pnpm scripts, enum values, interface methods, struct fields.
What to Document
Document these file types:
- Language source files:
.go,.ts,.svelte,.sql,.sh,.bash,.proto, etc. - General configuration files:
.yaml,.toml,.json(when they encode project logic, not tool config)
Do not document these:
- Tool-specific configuration files:
.golangci.yaml,.eslintrc,.prettierrc,renovate.json, etc. Their documentation belongs to the tool, not the codebase. - Auto-generated files (protobuf output in
protogen/, mocks, migrations generated by tooling).
For .proto files, the comment conventions on service, rpc, message, field, and enum values
belong to the write-proto skill: apply this skill's general principles (accuracy, no redundancy,
intent over mechanics) and follow write-proto for proto-specific comment style and placement.
For openapi.yaml, field descriptions, examples, and spec-level rules belong to the write-openapi
skill: apply this skill to prose quality, write-openapi to every spec-structural decision (status
codes, schema shape, breaking changes).
What deserves a comment at all
Most code deserves none. A comment must clear one of two bars:
- It describes the repo's own logic — type and method docs, package roles, domain invariants (the "Documentation Priorities" below).
- It explains an unusual behavior specific to this repo — a constraint, workaround, or ordering the reader cannot extract from the code in front of them.
Orientation is part of the first bar. In a long function or file, one line naming each logical movement lets a contributor grasp the shape before reading it. Pace those deliberately: one orienting line per movement, nothing inside it unless a line is genuinely surprising. Clustered or line-by-line comments bury the signal as badly as none at all.
Everything else is noise. In particular:
- Platform-wide conventions don't get repeated per repo. A behavior that is unusual but holds across the organization (e.g., compose files never declare sibling services because the tooling resolves them) belongs ONCE in the tool that implements it or in the org-level documentation; copies in each repo drift. If a reader could be surprised, fix the central doc, not the local file.
- The global picture belongs to the project docs. The architecture, the layer split, the reason
the system is shaped this way — README and CONTRIBUTING own that (see
write-project-docs); a code comment re-explaining it duplicates a doc that then drifts. Code comments carry the local detail those files can't: the invariant at the line that enforces it, why this call precedes that one. - Nothing the code already says. If reading the surrounding lines answers the question, the
comment restates; delete it.
depends_onalready shows ordering — only the why of a surprising dependency is comment-worthy. - No external dependencies. A comment must stay meaningful from the git history alone. Never
reference a PR, review thread, or temporary plan document. The one exception: linking an issue
for a patch or temporary workaround, where the issue is the tracking artifact. Write it as the
full URL —
https://github.com/<org>/<repo>/issues/<n>— and keep the marker short, ending it withTracked in <url>. In source a bare#Nis not a hyperlink and names no repo, while a full URL pastes straight from the editor into a browser. (On GitHub itself#Nauto-links, so this applies to code.)
The test, with "document the code as it is" below: a new contributor with only this repo checked out today — does the comment teach them something true, local, and not already on their screen?
Prose economy
Maximize meaning per word. Every sentence carries an idea the reader needs — rationale first, mechanics only when surprising. Exhaustive and terse are not opposites: cover everything that matters, in as few words as it takes, and stop.
- Lead with why; the code already shows what.
- Cut hedges, preambles, and ceremony ("note that", "in order to", "it is important to").
- Stay sharp. Cut the sugar. Softening costs words and buys nothing: a comment is read by someone
who needs the fact, not reassurance. Drop the throat-clearing ("on purpose", "deliberately", "it is
worth noting", "the case that matters"), the emphasis that adds no information (
MUST,NEVER, and the rest of the shouted words), and the build-up that delays the point ("two is nobody's intended answer"). Assert the fact and move on. - One precise sentence beats three approximate ones. If a comment reads well aloud, it ships.
- Default to plain words. Prefer the common word and the short one. Reach for an advanced or technical word only when it earns its place — when it replaces a whole clause or removes a real ambiguity.
- Write plain sentences, not labels. State the point as a subject-verb-object sentence and keep that order; a fronted object or a cleft makes the reader unpack it first. A rhetorical label ("The reason:", "Note:", "Why this matters:") is ceremony — delete it and let the sentence carry the idea: "This runs first because the cache is cold", not "Why this runs first: the cache."
- Name a thing once, and keep the name. Use one term for one concept. Do not alternate synonyms ("the cache," then "the store") for the same thing, or the reader stops to check whether they differ. An abstract label earns its place only when tied to something the reader can hold: "the intention" is vague until it is "the specification behind your idea" — name it that, then carry "specification" through. Where the domain already has a precise term, use it and resist paraphrasing it for warmth, because a paraphrase can carry a connotation the precise term rules out: a "convoy" may lose stragglers and still be one, where an "atomic landing" is all-or-none by name. When a name doubles as a common word, name its category on first use — "a dedicated Tracking status" reads as a place on the board where "Tracking" alone reads as an activity — and use it bare afterwards. When one concept spans several concrete forms, name the umbrella too, and never reuse that word in its ordinary sense a line later.
- Make every reference land on something the reader already holds. Keep a pronoun beside its one possible antecedent: "comprehension and judgment are the whole job, so spend your attention there" beats "so spend them there", where "them" reaches back past "the whole job" for its noun. Name earlier content by a plain description rather than an abstract handle the reader has to decode; a handle earns its place only once you have attached it to the thing.
- Concision is careful word choice, not dropped grammar. Keep sentences fully formed — subject and verb — rather than clipping them to fragments. A doc that is short but cryptic has failed.
- Write the choice, not the rejected alternative. State what holds and stop. The contrast form ("X is this, not that", "we use A rather than B") spends a clause on something the reader was never going to do, and leaves the rejected idea sitting in their head next to the real one. Write "Values are stored as JSONB", not "Values are stored as JSONB, never EAV". Keep a counter-example only where the wrong path is the one a reader would otherwise take; then give it once, plainly.
- A counterfactual is that same defect in other grammar. A sentence describing what would happen under an implementation nobody wrote defends the code instead of describing it: "a fixed counter would repeat them", "a nested transaction would inherit the caller's isolation", "a redacted placeholder would still leak the length". State the property that holds — "Both must differ", "A redaction still carries its length" — and drop the rest. Three uses of "would" survive this rule. A previewed action the code shows without taking: "renderPruneImpact names the rulesets a plan's prune op would delete" documents what a dry run displays. A condition that genuinely occurs: "git refuses when incoming commits would clobber a locally-modified file" is git's real behavior and the code branches on it. And the wrong-path counter-example above: "depend on the typed structs declared here rather than reading os.Getenv directly" names the call a reader would otherwise reach for. When the conditional describes what the reader is shown or a condition that occurs, keep it.
- Prefer affirmative sentences to clause chains. Give a rule its own sentence rather than trailing it off another as "…, so X must Y", and split a chain of subordinate clauses into plain statements. "…the MAC and ENC keys of an AES-CBC-HMAC key, so they must differ" becomes "…the MAC and ENC keys of an AES-CBC-HMAC key. Both must differ." This is not license to shred prose into fragments — the aim is statements a reader takes in one at a time, and the fully-formed-sentence rule above still holds.
This applies to every prose surface we write — doc comments, package docs, README sections, PR
descriptions (see open-pull-request), and planning issues (see plan-feature). Load this section
before writing prose on any of them, including surfaces this skill does not otherwise own.
Documentation Priorities
1. Exported / public symbols (highest priority)
Write with the end-user in mind: the reader is using this API, not maintaining it.
- Describe what the symbol does and how to use it. Keep it relevant and direct.
- Do not explain internal implementation details.
- Do not restate the name or the obvious (e.g.,
// Foo returns foo.is useless). - Every exported type needs a type-level doc, including parameter and option structs
(
FooRequest,FooOptions, etc.) — even when every field already has its own comment. Field docs explain the fields; the type doc explains the purpose of the whole.
2. Unexported / internal symbols
Write with the maintainer in mind: the reader must grasp intent and usage well enough to modify it safely.
- Same principles: what it does, how it's used — not how it works internally.
- Less critical than exported symbols; be more concise.
3. Inline technical comments
Only add inline comments when the logic is genuinely non-obvious. Ask: would a competent developer in this language understand the line without one? If yes, skip it. Standard language constructs, external library behavior, and restatements of the code are all noise — see "What to Avoid".
4. Interface vs. implementation
When a type has both an interface and a concrete implementation, document the interface thoroughly — that is what users read. The implementation doc serves maintainers: keep it terse, or omit it when the interface doc already covers everything. Never duplicate the same explanation across both.
5. Package-level documentation
When a package's purpose isn't self-evident, add a package-level doc comment explaining its architectural role: where it sits in the codebase, what layer it belongs to, and how it relates to adjacent packages. Avoid listing features — those change; the role usually doesn't.
Understand the package's place in the overall architecture before writing its doc. If you're uncertain (is this a utility layer, a business-logic layer, something else?), ask the developer rather than guessing. A wrong package doc is worse than none.
Each language has a canonical place for the package doc — always use it:
- Go: a dedicated
doc.gofile at the package root — see the Go conventions below. - Shell / other imperative files: the top-level comment block in the entry file (after the shebang).
Imperative Files (Scripts, Main Entrypoints)
For files that describe a sequence of actions (shell scripts, main.go, job runners, etc.):
-
Top-level package comment: explain what the file does as a whole.
- In Go: a package-level doc comment placed before
package main(orpackage foo), never a comment onfunc main()— Go tooling and doc generators read that as a function doc, not the package description. - In shell: place it after the shebang (
#!/...) line. - Keep it brief — one to three sentences. For
cmd/packages with multiple entry points (e.g., grpc vs. rest, dev vs. prod), always explain which role this particular command serves and cross-reference the others.
- In Go: a package-level doc comment placed before
-
Step comments: when the file has distinct logical phases, add a short comment before each to orient the reader. If a phase hides a non-obvious sub-step (a multi-step dependency wiring chain, say), add one line on that sub-group's purpose, not its mechanics.
-
Section separators: use comment blocks (e.g.,
# ---- Section Name ----) to visually separate major logical steps when the file is long enough to benefit from it.
Language-Specific Conventions
Go
- Exported symbols: doc comment directly above the declaration, with the symbol name near the start —
as the plain subject (
// Foo holds...) or with a natural article (// A Foo holds...,// The Foo interface...). Readers scanninggo docoutput orient on it, so never omit the name:// Holds...and// This holds...are wrong. - Package doc: library packages, always a dedicated
doc.goat the package root — never inline in a source file, not the first file alphabetically, not a random one. Command packages (package main), directly beforepackage maininmain.go— the idiomatic location; adoc.gois unusual there. - Errors: always document exported error variables (
ErrFoo). Callers check them by identity, so say what condition causes the error to be returned rather than restating the name. Say whether the error is returned or only logged — the distinction matters to callers. - Enums (typed string/int constants): the type doc says what the enum conceptually represents and how it's used; never describe the values there. Document each constant individually, explaining what that specific value means or selects.
- Interfaces: document the interface type and its methods. Implementations can reference the interface instead of duplicating doc.
- Cross-references: use
[Symbol],[Type.Field], or[package.Symbol]godoc link syntax. Never link to an unexported symbol from outside its package, or to any symbol in aninternalpackage the current package cannot import — neither resolves, and both signal inaccurate coupling. Describe the relationship in prose instead. - Language: Go returns errors — never write "throws", "raises", or "panics" when the code just returns an error.
Shell (bash/sh)
- Shebang first, then a top-level comment block explaining the script's purpose and usage.
- Use
# ---- Section ----style separators for distinct phases. - Comment variables when their purpose or expected format isn't obvious.
YAML (config files)
- Add inline comments to explain non-obvious fields, valid values, or important constraints.
- Do not comment fields that are self-explanatory from the key name.
TypeScript / Svelte
- JSDoc (
/** ... */) for exported functions, types, and components. - For Svelte components: briefly describe the component's role at the top if it's not obvious.
Domain Concepts and Specialized Knowledge
Public APIs often work with domain concepts, serialization formats, or industry standards a competent developer may not know. Explain the concept rather than assume familiarity, so a reader can use the API without looking up external specs first.
Where and how to introduce concepts
Introduce a concept once, at its first appearance in the public API — typically the package doc or the first exported type that depends on it. Later uses take the term for granted.
An introduction answers: what is it, and what does the caller do with it? One or two sentences are usually enough. Name the standard (RFC, spec) that governs the format; the reader can look up the rest.
Serialized and encoded values
When a string, []byte, or other primitive field holds a structured or encoded value, document:
- What it contains semantically (e.g., "a signed JWT token")
- What format it uses (e.g., "compact JWT — base64url-encoded header.payload.signature")
- If non-obvious, how to produce or consume that value (e.g., "returned by the SignToken RPC")
Token string documented as // The JWT token. tells the caller nothing actionable;
// Token is the signed compact JWT (base64url header.payload.signature) returned by Sign. does.
Generic type parameters
When a type parameter must satisfy a domain contract, state that contract in terms the caller can act on, not just "must match X". Common patterns:
- Serialization contract: "C must be JSON-serializable and match the struct embedded at signing time"
- Protocol contract: "T must implement proto.Message"
- Schema contract: "Row must have fields matching the SQL columns selected by this query"
Trust and security boundaries
When an API has security implications — what data leaves the process, which side holds secrets, what operations happen locally vs. remotely — document them in the package doc or at the primary entry point. Callers need this to reason about correctness and trust:
- "Signing is performed server-side; private keys never leave the service."
- "Verification is done locally using cached public keys — no network call per token."
- "The private key is generated and stored in memory only; it is never persisted."
Document the code as it is, not the change that produced it
A comment describes the code in its current form — never the edit, migration, or plan that created it. The reader has no access to what the code used to be, and references to that past rot the moment the change merges.
The test: if this were the first commit introducing the file — not a modification of something already there — would you still write this comment? If not, delete it or rewrite it in the present tense. This rules out:
- Change narrative: "now that X is removed", "the new model", "predates the redesign", "was previously", "had to go", "dropped in the rewrite". State what the code does, not what it stopped doing.
- Comparisons to deleted code: "matches the old bash script", "the Go port of
foo.sh", "same default the script used". The thing compared against is gone; the comparison points at nothing. - Pointers that send the reader outside the code: "per spec §6.4", "see PLAN.md", "see #50", "the Stage-2 gate", "Gate-2 override". Transient ones rot when the branch merges; durable ones (an issue or epic link) persist but still force the reader out of the file to reconstruct the rationale, and a planning label like "Stage 2" goes meaningless once the work ships. Fold the rationale into the comment itself and write it as if the plan that motivated the code never existed.
- Removal notes: "placeholder removed — see above", or any comment whose only content is that something used to be here. Absence needs no monument.
Keep the rationale, drop the history. "Uses --no-deps to avoid podman-compose's broken
depends_on wait" is durable; "mirrors the lessons from the old CLI's --no-deps handling" carries
the same fact but only parses for someone who remembers the old CLI. The prior bar still applies:
comment only when the code doesn't supply the context — a durable comment that restates the code is
still noise.
A placeholder pending planned work is the one case where the future is worth naming — as a
self-contained TODO/FIXME on the provisional line, describing what is missing in its own terms
("TODO: always returns success; compute the real result"), not a prose comment that only parses with
the roadmap open. The surrounding doc still describes what the code does today, cleanly, as if the
placeholder were the intended design.
Sweeping comments across a codebase
A cleanup pass over existing comments — one repo or many — behaves differently from documenting new code, and fails in its own ways.
Scope the check to the tree, not to your diff. Searching your own diff answers "did I change this
correctly?" It cannot answer "did I find everything?" The moment the bar tightens mid-sweep — a rule
added after reviewing the first pass — every comment the earlier pass deliberately left alone is
reclassified, and only a full re-scan surfaces them. Re-scan the whole tree whenever the rules change,
and measure the tree you are sweeping: a grep -r over a sibling checkout reports whatever
branch that checkout happens to be parked on, not your baseline.
A pattern search locates candidates; reading classifies them. Every mechanical scan is a proxy —
would, rather than, , not — and the worst findings match none of them. Change narrative
("the defect lived on the read path", "fixed in v1.4.5"), aggregate blocks describing the elements
below them, and docs that contradict the code are all found by reading the block the grep dragged into
view, never by the grep itself.
Expect to find comments that are wrong. Trimming forces someone to read the code underneath, which is why a verbosity sweep routinely turns up inverted parameter descriptions, references to helpers that no longer exist, and doc blocks attached to the wrong function. Those are the valuable output. Fix the comment, leave the code, and call them out separately from the prose changes — a reviewer skims rewording and scrutinizes a corrected claim.
Comments in a generating source must be regenerated. Editing a .proto comment changes the
generated Go, and the generated-go job fails on the drift. Run the repo's generate script and commit
the output as its own chore(gen) commit — git-conventions forbids mixing types, and a docs
commit carrying regenerated files hides the reason they changed.
A comment-only diff is not always a comment-only diff. Removing a trailing comment lets gofmt
re-align a struct or const block, and removing the comment above a single-element var (…) group
makes gofumpt collapse it. Both are the formatter asserting itself rather than an edit to the code, and
both belong in the PR description — an unexplained non-comment hunk costs a reviewer more than it saves.
Consistency and Quality
Wrong documentation is a liability. Every time you write or touch it:
- Verify accuracy: re-read the code and confirm the doc matches what it actually does. Watch initialization claims ("pre-fetches", "caches on creation") — check the constructor to see whether the behavior is eager or lazy.
- Check consistency: if a concept is described elsewhere (another file, the interface), use the same terminology.
- Rephrase freely: fix awkward, redundant, or misleading doc rather than preserving it out of caution. Rewrite entire sections if that's what it takes to make them clear.
- Respect scope: asked to document one function, don't refactor the whole file. Document what was asked, fix obvious inconsistencies nearby, and stop there.
What to Avoid
- Redundant preambles:
// This function...— the symbol name already acts as the subject. Same for field docs:// Tell the service to Xwhen the field already implies a configuration value — state the behavior directly. - Doc comments that omit the symbol name entirely — see the Go conventions above.
- Sentence fragments in doc comments — every top-level doc (type, func, var) should be a complete sentence. Field docs may be shorter, but avoid opening with a bare adjective like "Optional ..." or "Required ..."; a noun phrase ("Validation overrides to apply") is clearer and still concise.
- Explaining how the language works (readers know Go, shell, etc.).
- Paraphrasing external APIs: the reader can look those up.
- Documenting obvious setters/getters unless there's a real invariant to explain.
- Noise comments that just make the file longer without adding information.
- Copying the same description from an interface down to the struct method — reference or omit instead.
- Tying a reusable component's docs to one caller's wiring: an action input, exported function, or library parameter documents the contract — the capability it needs — not how one consumer supplies it. Bad: "the AGENT_BOT_CLIENT_ID secret". Good: "a GitHub App with write access to Checks". Naming a specific secret or env var couples the general component to one deployment and misleads the next caller.
- Enumerating anything in a list — fields, parameters, behaviors, checks, steps, etc. Lists go stale as soon as an item changes, and they push implementation details into docs that should describe intent. Bad: "It validates signature, expiry, issuer, audience, and subject against the config." Good: "It validates all token claims against the configuration registered for the given usage." When an example helps, give one representative item, not a complete inventory.
- Section-level enumerations: describing elements in a block header above them — document each element directly, as stated at the top of this skill.
- British English spelling: use American English throughout ("initialize" not "initialise", "behavior" not "behaviour").
- Incorrect acronym casing: write acronyms as the language ecosystem conventionally styles them
(
JSONnotJson,DAOnotdao,gRPCnotgrpc,URLnoturl). - Referencing inaccessible symbols in cross-package links — see the Go conventions above.
- All-caps emphasis words: avoid
MUST,SHOULD,OPTIONAL,NOT, etc. in prose documentation. These RFC-style terms feel out of place in code docs; plain prose ("must", "only", "nil for...") reads better. - Contrastive framing where the positive alone would do:
// Stored, not derived,// We use X rather than Y,// This is not a cache. State the choice instead:// Stored at publish time.— see "Write the choice, not the rejected alternative" above. - Counterfactuals defending the implementation:
// A fixed counter would repeat them,// a nested transaction would inherit the caller's isolation. State the property that holds — see "A counterfactual is that same defect in other grammar" above, including the three cases where a conditional stays. - A rule trailing off another sentence:
// …, so they must differ. Give it its own sentence — see "Prefer affirmative sentences to clause chains" above. - Rhetorical labels standing in for a sentence:
// Why this matters: ...,// Note: ...,// The reason: ...— state the point plainly, as under "Write plain sentences, not labels" above. - Change narrative and references to deleted code or transient docs — see "Document the code as it is, not the change that produced it" above.
Process
- Read the file(s) fully before writing anything.
- Understand the context: for package-level doc, also look at adjacent packages (what layer is this? how does it relate to its neighbors?). If the architectural role is unclear, ask the developer before writing anything.
- Identify gaps: missing doc on exported symbols, stale/incorrect comments, package doc absent.
- Check existing doc: is it accurate? Consistent? Could it be said more clearly?
- Write or improve following the priorities above.
- Re-read the result: does every comment add value? Is it consistent with the rest of the file?