How this project writes prose, for humans and agents alike. It governs
README.md, CHANGES, release notes, commit messages, docstrings,
source comments, and the prose under docs/ — every surface a reader
reaches.
For environment setup, the gates, and the pull request workflow, see CONTRIBUTING.md.
Three surfaces, one voice. A docstring says what a caller may rely on; a
CHANGES entry says what changed; prose says what happens. All three are
present tense, lead with the thing being described, and stop. Why it was
built that way belongs in the commit message, which is timestamped and
attached to the diff.
The most useful editing operation is deleting the introductory sentence.
Lead with verbs and name concrete things. Put identifiers in backticks. Prefer short declarative sentences, one operational fact each. Do not explain Python to Python developers; do explain this project's semantics.
Type annotations describe shape. Documentation describes meaning. A sentence that restates a signature has said nothing.
Use MUST, SHOULD, and MAY only where the normative sense is meant. Say what actually happens rather than that something is "supported".
| Instead of | Prefer |
|---|---|
| "We added…" | "Pane.send_keys now accepts…" |
| "New and improved" | "Server.sessions now…" |
| "powerful", "seamless" | state the capability |
| "easily", "simply", "just" | omit |
| "simple", "obvious", "intuitive" | omit |
| "robust" | name the failure that is handled |
| "comprehensive" | name what is covered |
| "production-ready" | state the guarantee |
| "optimized", "blazingly fast" | give the magnitude |
| "various fixes" | name the components |
| "under the hood" | omit unless observable |
| "please note that", "note that" | state the fact |
| "leverage", "utilize" | "use" |
| "delve into" | "read", or omit |
| "best practices" | name the practice |
| "in order to" | "to" |
The default reader writes Python and drives tmux through libtmux's object
API — Server, Session, Window, Pane. They are fluent in tmux
itself — servers, sessions, windows, panes, targets, formats — and
comfortable in Python, but you cannot assume they know libtmux's
internals: the format-string query layer, neo, the options and hooks
machinery, or when an object goes stale and needs refresh().
A second, smaller reader works on libtmux or against its lower layers: format tokens, the neo query interface, custom traversal, or contributing. Serve them too, but mark their material opt-in ("for the rarer cases", "advanced") so the default reader knows they can stop. Never make the common case pay a comprehension tax for the advanced one.
Rules that follow:
- Second person, present tense, active. "You split the window", not "A pane is created". Address the reader who is doing the thing.
- Concept before API surface. Open by saying what the object or method is and what it does for the reader. The signature — the parameters, the flags — is the last detail they need, not the first. A page that opens with a method signature has buried the idea under its mechanics.
- Say when they can stop. Lead with the default and the reassurance: most readers never reach for this, the defaults work, the advanced parts are optional. Let a skimmer leave after one paragraph.
- Grant permission, don't demand attention. "Reach for this when…", "for the rarer cases" — tell readers they're in the right place without implying they must read on.
- Progressive disclosure. Order by how many readers need it: the common call → the one argument a few will tune → the lower-level primitive → querying tmux directly. Each step is for a smaller audience than the last.
- Lean on the hierarchy. The reader thinks Server → Session → Window → Pane; reinforce that chain when you explain containment or traversal. It is the mental model the whole library hangs on.
- Name the trade-off. If a call costs something — an extra tmux
round-trip, a stale object needing
refresh(), a polling wait — say so, and say what it buys ("a busy wait, not an event, but reliable"). State it; don't sell it. - Frame by concept, not by mechanism. Don't headline a feature by
its tmux flag or format token in prose; that names the implementation
surface, which is the reader's last concern. Name the concept. The
mechanics vocabulary — a parameter table, a
#{format}token, the-ttarget — belongs in a reference table or the API docs, and only there.
A README is the shortest path from "what is this?" to competent use, not the project's autobiography.
The first sentence is a contract. It says what abstraction the reader has been handed, concretely enough to tell this package apart from the neighbouring one.
Get to a runnable command or snippet before anything the reader can skip. A logo, a mission statement, a comparison matrix and three paragraphs of history in front of the install line all cost the same thing.
State the minimum Python version and meaningful platform constraints in
prose, not only in badges. requires-python in pyproject.toml is the
authority; the README must agree with it.
Name the distribution, the import, and the executable separately wherever they differ. That distinction prevents a Python-specific class of confusion.
Examples are executable, not illustrative fiction. Never
your-command <some-options>. See
Documented examples that run for which
blocks are executed and how to write one that qualifies.
Document the semantic model, not the parameter list. A signature already enumerates parameters; what it cannot say is what the call mutates, what blocks, and what it raises.
State defaults explicitly — defaults are API. State negative guarantees
where they exist: "performs no network I/O", "never mutates a Session
you didn't ask it to touch", "never writes outside the tmux socket it
was given". They establish boundaries faster than any amount of
description.
Headings stay conventional and stable, because people deep-link them. Badges are few and load-bearing.
Examples in this repository are tests. This section is the contract for
writing one the test suite can actually see, and it is this repo's real
mechanism — read from pyproject.toml's [tool.pytest.ini_options] and
the root conftest.py, not assumed.
A fence tag is cosmetic. Only a >>> prompt executes. A block
written as
```python
server = Server()
```
is prose that looks like a test. Nothing collects it, nothing runs it, and it can be wrong for years. The same block written with prompts is a test:
```python
>>> server = Server()
```
This is the single most expensive mistake available when editing documentation, because removing the prompts leaves a green test suite and a silently deleted test. When editing a file that contains examples, count the prompts before and after.
The fence tag is python for a doctest session and console for a
shell command at a $ prompt.
Where examples run. testpaths lists src/libtmux, tests,
docs, and README.md. addopts sets --doctest-docutils-modules and
-p no:doctest: the stdlib doctest pytest plugin is disabled, and
gp-libs' docutils-based collector (pytest_doctest_docutils, loaded
under the sphinx entry-point name) runs in its place. That collector
reads doctests out of .py docstrings and out of any Markdown or
reStructuredText page docutils parses — which is why a >>> block in
README.md or under docs/ is executed exactly like one in a
docstring. ELLIPSIS and NORMALIZE_WHITESPACE are enabled globally
(doctest_optionflags), so ... elides variable output and whitespace
differences do not fail a comparison. Reach for an inline
# doctest: +FLAG only for the block that needs something more.
# doctest: +SKIP is not permitted. It is a workaround that tests
nothing. Use the fixtures.
Do not downgrade a doctest to a non-executed block to make it pass.
A .. code-block:: or an unprompted fence does not run. If an example
cannot pass, fix the example or fix the code.
The doctest_namespace fixtures. The root conftest.py's
add_doctest_fixtures fixture seeds doctest_namespace before any
doctest in the suite runs, so a block never needs an import or a setup
preamble to reach these names: server, session, window, pane,
Server, Session, Window, Pane, Client, ControlMode,
control_mode, monkeypatch, request. It only does this inside an
actual doctest item and only when tmux is on PATH — a doctest that
uses one of these names with no tmux binary available fails with a
NameError, not a skip.
The Server trap. conftest.py assigns doctest_namespace["Server"]
twice: first to the real libtmux.server.Server class, then — a few
lines later — to the TestServer fixture from
src/libtmux/pytest_plugin.py, a functools.partial that builds
uniquely-socketed servers and registers their cleanup. The second
assignment wins. A bare Server(...) in a doctest is therefore the test
factory, not the real class. Import the class explicitly when an example
needs class-level API:
>>> from libtmux.server import ServerThe socket_path trap. Both the server fixture and the doctest
Server() factory construct servers with a socket name, so their
socket_path attribute is None. When an example needs the real path,
ask tmux for it: server.cmd("display-message", "-p", "#{socket_path}").stdout[0].
# doctest: +HIDE. A gp-libs extension flag: it is a no-op for
execution and checking — the line still runs and its output, if any, is
still verified — but it tells the Sphinx-rendered docs to drop that line
from the page. Use it to set up state (an env var, a queried socket
path) a docstring's Examples section does not need to show:
>>> from libtmux.server import Server as TmuxServer # doctest: +HIDE
>>> socket_path = server.cmd( # doctest: +HIDE
... "display-message", "-p", "-t", pane.pane_id, "#{socket_path}"
... ).stdout[0]Every code block on a docs/ page is an independent doctest. Blocks
do not share a session: each one gets a fresh copy of the namespace
and a fresh tmux server. A later block cannot use a pane an earlier
block created — the name is simply not defined there. Write every block
self-contained. Order is load-bearing for the reader's narrative, not
for state — reorder, add, or drop a block as the prose demands, and keep
the story coherent.
Docstring examples use the NumPy Examples section:
Examples
--------
>>> from example import add
>>> add(2, 2)
4
Room to grow. The docutils collector reads .md and .rst
wherever it is loaded, which is everywhere in this repository. A
prompted block added to a documentation page is executed from that
moment with no configuration change. The MyST {doctest} directive and
the reStructuredText .. doctest:: directive are also registered and
available for a case that ever needs an explicitly marked block, rather
than a plain prompted fence. This section is where any additional
executed-block format is documented when one is adopted.
Warm the framing, never the facts. Resolution-order lists, value tables, exact error strings, format tokens, and class or method cross-references carry meaning in their exact form — leave them alone. The friendly voice belongs in the sentences around a precise block, introducing it, not inside it paraphrasing it into vagueness.
Link the first prose mention of any symbol that has a useful
destination on that page — a Python object, a libtmux API, a tmux
concept with a glossary entry, a topic or configuration page, an
external tool or project. Use the most specific target available:
{class}, {meth}, {func}, {mod}, {exc}, or {attr} for API
objects; {ref}, {doc}, or {term} for documentation pages, section
anchors, and glossary entries; a Markdown link for an external project.
After the first linked mention on a page, later mentions can stay plain
unless the distance or context makes another link useful. Do not rely
on a later reference section to satisfy the first-mention rule — if the
first occurrence would be a heading, grid-card teaser, or introductory
sentence, link that occurrence or retitle the heading so the first
prose mention can carry the link. Leave command examples, code blocks,
and literal configuration values as code; link the surrounding prose
instead.
A {ref} must match its target's anchor exactly. Anchors mix
underscore and hyphen forms across pages (context_managers,
pane-interaction) — copy the target's own (label)= line rather than
guessing the form. just build-docs catches a broken cross-reference;
the doctests do not, so build the docs before you commit.
docs/topics/pane_interaction.md is the worked example: a
concept-first intro that says what a Pane is and which two methods
(send_keys, capture_pane) cover most uses before any signature, an
explicit "you can stop after the first two sections" reassurance,
sections ordered by shrinking audience, honest trade-offs (polling is a
busy wait; a resize is a request, not a guarantee), methods named by
what they do with {meth} cross-references, and precise capture-flag
and format tables left exact. Read it before reshaping another page.
CHANGES is the changelog. It is rendered as the Sphinx changelog page
(docs/history.md includes it) — not CHANGELOG.md. Modeled on
Django's release-notes shape: deliverables get titles and prose, not
bullets.
A ledger, not a narrative. It is scanned, and the question a reader is asking is whether an entry affects them.
Release entry boilerplate. Every release header is
## libtmux X.Y.Z (YYYY-MM-DD). The file opens with a
## libtmux X.Y.Z (Yet to be released) placeholder block fenced by
<!-- KEEP THIS PLACEHOLDER ... --> and <!-- END PLACEHOLDER ... -->
HTML comments — new release entries land immediately below the END
marker, never above it.
Open with a multi-sentence lead paragraph. Plain prose, no italic.
Open with the version as sentence subject ("libtmux X.Y.Z ships …") so
the lead is self-contained when excerpted. Two to four sentences telling
the reader what shipped and who cares — user-visible takeaways, not
internal mechanism. Cross-reference detail docs with {ref} to keep the
lead compact.
Lead paragraphs are release-time material — off-limits to branches
and PRs. The unreleased entry carries no lead paragraph and no version
summary: sections only (### Breaking changes, ### What's new
deliverables, ### Fixes, …). Speaking for the release — what the
version "is", "ships", or "focuses on" — is presumptuous before its
scope is final; only the person cutting the release writes that, and
only when the user explicitly asks to release. Never write or edit a
lead from a feature branch, and never ask or imply that a release should
happen.
Each deliverable is a section, not a bullet. Inside ### What's new, every distinct deliverable gets a #### Deliverable title (#NN)
heading naming it in user vocabulary, followed by 1-3 prose paragraphs
explaining what shipped. Don't wrap a paragraph in - — bullets are
for enumerable lists, not paragraph containers. Cross-link detail docs
("See {ref}foo`` for details.") so prose stays focused.
The deliverable test. Before writing an entry, ask: "What's the deliverable, in user vocabulary?" If you can't answer in one sentence, the entry isn't ready. Mechanism (helper internals, byte counters, schema-validation locations) belongs in PR descriptions and code comments, not the changelog.
Fixed subheadings, in this order when present: ### Breaking changes, ### Dependencies, ### What's new, ### Fixes,
### Documentation, ### Development. Dev tooling (helper scripts,
internal automation) lives under ### Development. For breaking
changes, show the migration path with concrete inline code (a # Before
/ # After fenced code block). Dependency floor bumps use the form
Minimum `pkg>=X.Y.Z` (was `>=X.Y.W`).
PR refs (#NN) sit in each deliverable's #### heading.
When bullets are appropriate. Catch-all sections (### Fixes,
occasionally ### Documentation) with 3+ genuinely small items use
bullets — one line each, never paragraphs. If a bullet swells past two
lines, promote it to a #### Title (#NN) heading with prose body.
Anti-patterns.
- Fragile metrics: token ceilings, third-party version pins, percent benchmarks, exact byte counts. Describe the capability, not the math.
- Internal jargon: private symbols (leading-underscore identifiers), algorithm names exposed for the first time, backend scaffolding.
- Walls of text dressed up as bullets.
- Buried breaking changes — they get their own subheading at the top of the entry.
Always link autodoc'd APIs. Any class, method, function, exception,
or attribute that has its own rendered page must be cited via the
appropriate role ({class}, {meth}, {func}, {exc}, {attr}) —
never with plain backticks. Doc pages without explicit ref labels use
{doc}. Plain backticks are correct for code syntax, env vars,
parameter names, and file paths that aren't doc pages — anything without
an autodoc destination.
MyST roles. Class references use {class} (e.g.
{class}`~libtmux.Pane`), methods use {meth}, functions use
{func}, exceptions use {exc}, attributes use {attr}, internal
anchors use {ref}, doc-path links use {doc}, glossary terms use
{term}.
Summarization style. When a user asks "what changed in the latest
version?" or similar, lead with the entry's lead paragraph (paraphrased
if needed), followed by each #### deliverable heading under
### What's new with a one-sentence summary. Cite (#NN) only if the
user asks for source links. Don't invent versions, dates, or numbers not
present in CHANGES. Don't quote line numbers or file offsets — those
shift as the file evolves.
CHANGES is the permanent ledger; a release page is editorial. Lead
with one paragraph naming the headline change, then three to five
highlights, then link the full changelog.
Numbers over adjectives. "Cold start 41 ms to 6 ms" is a sentence; "much faster startup" is a smell.
A list of merged commit subjects is a merge log wearing a release-note hat. Put the hand-written highlights above it.
Versions are PEP 440 identifiers. Semantic-versioning meaning is applied to the documented public API — which includes configuration keys and serialized formats, not only imported Python symbols.
The prime directive: never restate the type. The annotation is the source of truth; the docstring carries what the annotation cannot.
This is documentation debt wearing a docstring:
def get_id(pane: Pane) -> str:
"""Get the pane's identifier.
Parameters
----------
pane : Pane
The pane.
Returns
-------
str
The identifier.
"""
Document instead the dimensions the type system cannot encode:
- Mutation. What it changes in place.
- Ownership. What the caller must close, release, or keep alive.
- Ordering. Whether results come back in a guaranteed order.
- Timing. What has finished by the time the call returns, or the awaitable resolves.
- Failure. Which exceptions are raised and what triggers each.
- Idempotence. Whether calling twice does anything the second time.
- Concurrency. Whether calls are coalesced, queued, or independent, and whether the object is thread-safe, process-safe, or fork-safe.
- Units and ranges. What a number means and what values are accepted.
- Boundary behaviour. What zero, empty, and the maximum do.
- Platform. Behaviour that differs by operating system or dependency version.
- Security boundary. What is executed, and what is only read.
The ambiguity worth resolving by example: whether "retry three times" means three attempts or four. State it.
The first sentence stands alone; tooling truncates there. PEP 257 applies: triple double quotes, an imperative one-line summary ending in a period, a blank line before any extended description. Do not repeat an introspectable signature.
All public functions and methods use the
NumPy docstring convention —
enforced by ruff's pydocstyle rules (convention = "numpy" in
pyproject.toml), not relitigated in review:
"""Short description of the function or class.
Detailed description using reStructuredText format.
Parameters
----------
param1 : type
Description of param1
param2 : type
Description of param2
Returns
-------
type
Description of return value
"""Classes with fields — NamedTuple, dataclasses — document every
field in an Attributes section:
@dataclasses.dataclass()
class Pane(Obj, OptionsMixin, HooksMixin):
""":term:`tmux(1)` :term:`Pane`.
Attributes
----------
server : Server
Server the pane belongs to.
"""Autodoc renders every field whether or not you describe it, so an
undocumented NamedTuple field ships to the API docs as "Alias for
field number 0" and a dataclass field ships bare. Document all of
them — a class with three fields and two documented still ships a stub
for the third.
A comment ships only if it passes all three gates. Fail any: delete or rewrite. Borderline: delete — borderline means the information is reconstructible, which is what makes deletion cheap.
Loss. Three years from now, would losing this cost a maintainer real time rediscovering intent, an invariant, a constraint, or a failure mode the code and tests do not already make obvious?
Elite. Would SQLite, Redis, the Go standard library, or CPython write this comment, at this length? Those projects state the constraint and stop. They do not argue with an imagined objector.
Upkeep. Will it stay true without maintenance? A comment that hand-syncs a value the code owns — a count, an offset, a line reference, a duplicated constant — is false the first time that value moves.
One or two lines. A comment reaching four is either carrying several facts, in which case split it, or arguing, in which case cut it to the fact.
Rationale, alternatives weighed, and the story of how the code got here belong in the commit message: timestamped, attached to the exact diff, and free to maintain.
A comment often holds both a constraint and the deliberation that found it. Keep the constraint, cut the deliberation. "Runs at most once per second" survives; "this is the right trade for now" does not.
- Why over how: upstream quirks, protocol and compatibility constraints, performance tradeoffs still part of the contract.
- Invariants, preconditions, ordering, lifetime, and concurrency requirements that types and tests cannot express.
- Code that looks wrong but is not, so a later cleanup does not reintroduce the bug.
- A high-level sketch of an algorithm whose local operations do not reveal the whole.
- Narration of the next lines; code translated into English.
- Restated names, types, defaults, or control flow.
- Values duplicated from the code and hand-synced.
- Justification, hedging, or apology for a choice.
- Speculation about future requirements.
- History version control already holds, including commented-out code.
- Ticket and issue numbers. They say nothing to a reader without tracker access, and they rot when the tracker moves. Unfinished work goes in the tracker, not the source.
- Transient observations — "currently", "for now", "the latest release" — that go stale with no nearby edit.
It reaches values that track our own code. It does not reach frozen external facts.
Bad (Delete):
# There are 321 tests to complete for servers.Good (Keep):
# tmux < 3.2 reports the pane ID only after the command completes,
# so this query must stay separate.Doctests, minimal usage examples, and param, return, and raises lines on public API are exempt from the loss gate — they serve the caller, not the maintainer. They are exempt from nothing else. Ceiling: a good man page entry.
NumPy-style Parameters, Returns, and Attributes sections and
executable doctests fall under this exception — autodoc ships every
field whether or not you describe it, and a doctest that runs is also a
test.
Pick the domain noun and keep it. If the code calls something a
session, do not call it a workspace in one paragraph and an environment
in the next. If the method is capture_pane, write "capture"
everywhere rather than alternating with "read", "grab", and "snapshot".
Stable vocabulary is what makes search, deep links, and an agent's retrieval work at all.
Python and PyPI keep their own capitalisation. Distribution names are written as they are published.
Do not write counts into prose — how many symbols exist, how many tests there are. They go stale silently and no reader needs them. Counts that pin a fixture or guard an invariant are different, and belong in code.
Prose wraps at 80 columns. Table rows, badge lines, and long links are exempt, because breaking them harms rendering. A pull request or issue body does not wrap at all: GitHub renders a single newline as a space in a file and as a line break in a comment, so a wrapped comment body arrives as ragged stubs.
GitHub alert blocks — > [!NOTE], > [!WARNING] — render as literal
text outside GitHub, so reserve them for at most one load-bearing
warning per document. Write the sentence so it carries the fact on its
own, and a renderer that drops the marker loses nothing.
Do not use a local absolute path or an email address in anything published.
Code blocks are paste-and-run units: pasting one block runs exactly one intended action. Executed examples are exempt — the test suite runs them, nobody pastes them.
- One command per block. Multiple steps may share a block only when
explicitly chained with
&&,;, or\continuations — the chain is then one logical command. - Explanations go in prose above the block, never as
#comments inside it. - Command menus are per-command blocks with prose lead-ins, not tables.
- Shell commands use the
consoletag with a$prefix. This separates interactive commands from scripts and enables prompt-aware copy. - Split long commands with
\— one flag or flag+value pair per indented continuation line, positional arguments last.
Good — show the last ten commits as a graph:
$ git log \
--max-count=10 \
--graph \
--onelineBad:
# Show the last ten commits as a graph
$ git log --max-count=10 --graph --onelineScope(type[detail]): concise description
why: Explanation of necessity or impact.
what:
- Specific technical changes made
- Focused on a single topic
Keep the subject to 50 characters or fewer, excluding any trailing
(#NN) pull request reference, and wrap body lines at 72. Separate the
why: and what: blocks with a blank line.
The subject has two forms. Scope(type[detail]): Description, with a
colon, for anything that changes behaviour. Scope(type[detail]) Description — no colon — for routine maintenance: a dependency bump
(py(deps…)) or an AI rule update (ai(rules…)) that does not change
what the code does.
Common types:
- feat: New features or enhancements
- fix: Bug fixes
- refactor: Code restructuring without functional change
- docs: Documentation updates
- chore: Maintenance (dependencies, tooling, config)
- test: Test-related updates
- style: Code style and formatting
- ci: Workflow and pipeline changes
- py(deps): Dependencies
- py(deps[dev]): Dev dependencies
- ai(rules[AGENTS]): AI rule updates
- ai(claude[rules]): Claude Code rules (
CLAUDE.md) - ai(claude[command]): Claude Code command changes
Example:
Pane(feat[send_keys]): Add support for a literal flag
why: Send characters without tmux interpreting them.
what:
- Add a literal parameter to send_keys
- Pass -l when it is set
For a multi-line message, use a heredoc so the formatting survives:
$ git commit -m "$(cat <<'EOF'
Scope(feat[detail]): Concise description
why: Explanation of the change.
what:
- First change
- Second change
EOF
)"Never create tags. Never push tags. The owner handles tagging and tag pushes, because a tag triggers the publish workflow.
A release commit subject is plain and short: Tag v<version>. The
detailed why and what go in the body. Do not use the
Scope(type[detail]): format for a release — it buries the lede.
Treat AI slop as review-hostile noise, not as proof that text or code is wrong. The goal is to maximise information density.
- AI signatures. No "Generated by", no conversational filler ("Certainly!", "Here is..."), no unexplained emoji, no tool metadata.
- Brittle references. No hard-coded line numbers, fragile file or test counts, dated "as of" claims, bare SHAs, or local absolute paths — unless they are strict evidentiary artefacts such as a benchmark log.
- Diff narration. Do not restate what moved, was renamed, or was removed in anything the reader holds alongside the diff: code, docstrings, README, CHANGES, or a pull request description. The diff and commit message already carry it.
- Branch-internal narrative. Do not mention intermediate states, abandoned approaches, or "no longer" behaviour unless users of a published release actually experienced the old state — see Shipped vs. branch-internal narrative.
- Low-value scaffolding. No ownerless TODOs, unused future-proofing, debug artefacts, or defensive wrappers around failure modes nothing can reach.
- Prose inflation. The diction table under Voice governs; replace an inflated word with a concrete description of behaviour, constraints, or trade-offs.
- Coded labels. Write rules and findings as plain imperatives. No
[R1],Option B, or any index a reader has to decode. Internal agent bookkeeping may use ids; shipped text may not.
Preserve the "why". Never delete a comment documenting an invariant, a protocol constraint, a platform quirk, or an upstream workaround — those are the facts Source comments keeps, and every other comment is judged by it. Preserve exact counts, dates, and SHAs that serve as evidence in benchmark results, release notes, stack traces, or lockfiles — that evidence is immune to the brittle-references rule above.
Link to a pinned revision, never to trunk. A pinned permalink is not a
brittle reference; an unlinked SHA dropped into prose is. blob/master/…
links rot silently — the file moves, lines shift, and the anchor lands
on unrelated code while still resolving.
- Prefer a release tag (
blob/v1.4.0/…). Most durable, and it tells the reader which released version the claim held for. - Otherwise use a 7-char commit ref (
blob/9a29b1a/…) reachable from trunk. Use when there is no tag or the claim is about unreleased code. Never a PR-head SHA — it can be rebased or garbage-collected. - Reserve
blob/master/…for living documents meant to always show the latest state, such as this contributing guide. - Line anchors (
#L120-L145) are only safe on a pinned ref.
Long-running branches accumulate tactical decisions — renames, refactors, attempts-then-reverts, intermediate states. Commit messages and the diff hold what changed and why. Do not restate either in artefacts the downstream reader holds: code, docstrings, README, CHANGES, pull request descriptions, release notes, migration guides.
When deciding what counts as branch-internal, use trunk or the parent branch as the baseline — not intermediate states inside the current branch.
The published-release test. Before adding rename history,
"previously" / "formerly" / "no longer X" phrasing, "removed" / "moved"
/ "refactored" / "fixed" diff paraphrases, or a ### Fixes entry to a
user-facing surface, ask: did users of the most recently published
release ever experience this old name, old behaviour, or bug? If no, it
is branch-internal narrative — move it to the commit message and
describe only the current state in the artefact.
Keep in shipped artefacts. Deprecations and migration guides for
symbols that actually shipped. ### Fixes entries for bugs that
affected users of a published release. Comments explaining why the
current code looks this way — invariants, platform quirks, upstream bug
workarounds — that make sense to a reader who never saw the previous
version. Default: when in doubt, keep the artefact clean and put the
story in the commit.
Cleanup in hindsight. Applying this rule retroactively from inside
a feature branch: first diff against the parent branch or trunk to see
which commits this branch actually introduced. For commits this branch
introduced, offer fixup! commits with git rebase --autosquash, or
one cleanup commit at branch tip — the author chooses. Leave commits
already in trunk or a parent branch alone by default; act on them only
on explicit instruction, and then fold the cleanup into one commit at
branch tip without rewriting trunk or parent-branch history. If
cleaning up in-branch bleed would touch a colleague's in-flight work or
expand the branch beyond its stated goal, stay in lane: protect the
branch's current goal and leave prior bleed alone.