From 0f63af178ef62992ed399740df771d02eb458a27 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 06:32:23 -0500 Subject: [PATCH 1/8] Docs(refactor[agents]): Route instead of restate why: AGENTS.md is read on every task, so a policy that matters to one class of change costs context on all the others. Splitting how we work from how we write lets each be loaded when it applies, and keeps both discoverable to humans rather than only to agents. what: - Replace the AGENTS.md body with a router: universal constraints, then one pointer per class of change - Add .github/WRITING.md for prose policy: README, changelog, release notes, commit messages, API docs, source comments, terminology - Move CONTRIBUTING.md under .github/ as workflow policy: build, test, required checks, pull requests, review, releases, compatibility - Title the README "libtmux for " and state the alpha terms the same way across the ports - Section bodies are ; a per-language pass fills them from what this branch replaced --- .github/CONTRIBUTING.md | 35 ++++++ .github/WRITING.md | 41 ++++++ AGENTS.md | 272 ++-------------------------------------- CONTRIBUTING.md | 112 ----------------- README.md | 6 +- 5 files changed, 92 insertions(+), 374 deletions(-) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/WRITING.md delete mode 100644 CONTRIBUTING.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..88711c2 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing + + + +How this project writes prose — README, changelog, release notes, commit +messages, API documentation, and source comments — is set out separately in +[`WRITING.md`](WRITING.md). Read that before changing any of it. + +## Building + + + +## Running the tests + + + +## Checks that must pass + + + +## Pull requests + + + +## Review + + + +## Releases + + + +## Compatibility + + diff --git a/.github/WRITING.md b/.github/WRITING.md new file mode 100644 index 0000000..92acb05 --- /dev/null +++ b/.github/WRITING.md @@ -0,0 +1,41 @@ +# Writing + +How this project writes prose, for humans and agents alike. It governs +`README.md`, `CHANGELOG.md`, release notes, commit messages, CLI and help +text, error messages, API documentation, source comments, and migration +guides. + +For build, test, and pull request workflow, see +[`CONTRIBUTING.md`](CONTRIBUTING.md). + +## README + + + +## Changelog + + + +## Release notes + + + +## Commit messages + + + +## API documentation + + + +## Source comments + + + +## Terminology and capitalization + + + +## Code blocks + + diff --git a/AGENTS.md b/AGENTS.md index 3ca7a68..3d3bd69 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,264 +1,18 @@ -# AGENTS.md +# Agent instructions -Guidance for agents working on libtmux for Java. This repository is the whole -of the port; nothing outside it governs the Java code. +Follow the existing project conventions and keep changes narrowly scoped to +what was asked for. -## Keep off other ports' sockets + -Sibling libtmux ports — Python, Go, Rust, TypeScript, C#, C++ — are worked on in -parallel on this machine, and every one of them starts real tmux servers in -`/tmp`. Their debris outlives their test runs: servers from an exited benchmark -stay up, holding ptys, until something kills them. +## Additional guidance -**Every tmux server this project starts belongs under a path that names this -port.** Use one of: +This file routes; it does not restate. Read the policy that governs the change +being made: -- `/tmp/libtmux-java-test/…` — anything a test starts. -- `/tmp/libtmux-java-dev/…` — anything started by hand while investigating. - -Never `/tmp/libtmux-…` on its own, and never the default socket. `libtmux-` is -the prefix a sibling port is also using, which is the whole problem. - -Why it matters more than tidiness: a suite sharing `/tmp` with another port's -leftovers fails intermittently, under load, in places that have nothing to do -with the change under test — `SERVER_GONE`, misframed rows, `fork: No space -left on device`. Every one of those reads as a regression in whatever you last -touched, and hours go into a bug that was never yours. - -Before blaming a change for an intermittent real-tmux failure, count what is -already running: - -```console -$ pgrep -c tmux -``` - -Then find whose it is, since a socket path exists only in the process's own -command line: - -```console -$ for p in $(pgrep tmux); do tr '\0' ' ' < /proc/$p/cmdline | rg -o '\-S [^ ]+'; done -``` - -Kill only sockets under this port's roots. Another port's servers are not -yours to reap. - -## Socket paths are short by necessity - -A unix socket path cannot exceed about 104 bytes, and tmux reports a longer one -as `error connecting to … (File name too long)`. That rules out sockets under a -build directory or a deep scratch path, and it is why the roots above are short. - -## Commands - -Everything, including formatting and the static analysis gates: - -```console -$ ./gradlew check -``` - -Against every supported tmux release: - -```console -$ ./gradlew testTmuxMatrix -PlibtmuxMatrix=/path/to/tmux/builds -``` - -Regenerate the measured comparison of the carriers, which is never hand-edited: - -```console -$ ./gradlew modeBenchmark -PlibtmuxTmux=/path/to/tmux -``` - -## Carriers change cost, not answers - -`ExecutionMode` chooses how a command travels, and a suite can be run under any -of them: - -```console -$ LIBTMUX_MODE=control ./gradlew check -``` - -A failure that appears only under one carrier is a real finding — the library -claims the answer does not depend on the carrier, and -`ExecutionModeConformanceTest` is where that claim is gated. Confirm it against -a clean `/tmp` first: the failure modes of cross-port debris and of a genuine -carrier defect look alike. - -## Comments earn their maintenance cost - -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. - -### Ceiling - -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. - -### Keep - -- 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. - -### Delete - -- 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. - -### The upkeep gate in practice - -It reaches values that track our own code. It does not reach frozen external -facts. - -Bad (Delete): - -```java -// There are 321 tests to complete for servers. -``` - -Good (Keep): - -```java -// tmux < 3.2 reports the pane ID only after the command completes, -// so this query must stay separate. -``` - -### Documentation exception - -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. - -Javadoc summaries and `@param`, `@return`, and `@throws` tags fall under this -exception. - -## Git Commit Standards - -Format commit messages as: -``` -Scope(type[detail]): concise description - -why: Explanation of necessity or impact. - -what: -- Specific technical changes made -- Focused on a single topic -``` - -Keep the subject ≤50 chars (excluding any trailing `(#NN)` PR ref); wrap -body lines at ≤72 chars. Separate the `why:` and `what:` blocks with a -blank line. - -Common commit 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 -- **java(deps)**: Dependencies -- **java(deps[dev])**: Dev Dependencies -- **ai(rules[AGENTS])**: AI rule updates - -Example: -``` -Pane(feat[sendKeys]): Add support for a literal flag - -why: Send characters without tmux interpreting them. - -what: -- Add a literal field to SendKeysRequest -- Pass -l when it is set -``` - -### Release commits - -Never create tags. Never push tags. The user handles tagging and tag -pushes (tags trigger the CI publish workflow). - -Release commit subjects are plain and short: `Tag v`. Put -the detailed why/what in the commit body. Don't use the -`Scope(type[detail]):` format for releases — don't bury the lede. - -For multi-line commits, use heredoc to preserve formatting: -```bash -git commit -m "$(cat <<'EOF' -Scope(feat[detail]): Concise description - -why: Explanation of the change. - -what: -- First change -- Second change -EOF -)" -``` - -## Code Blocks - -Code blocks are paste-and-run units: pasting one block runs exactly one -intended action. Doctests and other 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 `console` tag 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: - -```console -$ git log \ - --max-count=10 \ - --graph \ - --oneline -``` - -Bad: - -```console -# Show the last ten commits as a graph -$ git log --max-count=10 --graph --oneline -``` +- Documentation and user-facing prose — `README.md`, `CHANGELOG.md`, release + notes, commit messages, CLI and help text, API documentation, and source + comments: [`.github/WRITING.md`](.github/WRITING.md) +- Contribution workflow, building, testing, and pull requests: + [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md) +- Security-sensitive changes: [`SECURITY.md`](SECURITY.md) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index ca91ad6..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,112 +0,0 @@ -# Contributing - -## The gate - -One command has to pass before anything is proposed: - -```console -$ ./gradlew check -``` - -It runs formatting, Error Prone, NullAway, and every test including the ones -that start real tmux servers. A green `check` that reported `UP-TO-DATE` for -every task verified nothing; force it with `--rerun-tasks` when that matters. - -Against every supported tmux release, which is what the compatibility claim in -the README rests on: - -```console -$ ./gradlew testTmuxMatrix -PlibtmuxMatrix=/path/to/tmux/builds -``` - -The matrix is a local tree of built tmuxes, one directory per lane, each with -`bin/tmux`. Build one: - -```console -$ ./scripts/tmux-matrix.sh ~/tmux-builds -``` - -It reads the lane list out of the build, so it cannot drift from what the matrix -actually runs. - -If a real-tmux failure looks like a regression, count what is already running -before believing it — this machine routinely carries several hundred tmux servers -belonging to sibling ports: - -```console -$ ./scripts/reap-stale-servers.sh -``` - -## Tests start real tmux - -Every server this project starts belongs under a path naming this port — -`/tmp/libtmux-java-test/` for tests, `/tmp/libtmux-java-dev/` for anything -started by hand. `AGENTS.md` explains why in full, and it is not tidiness: this -machine runs several libtmux ports at once, and their leftovers turn into this -suite's intermittent failures. - -## What a change is expected to carry - -- **A test that failed before it.** For a bug, that test is the evidence the bug - was real; without it there is nothing to distinguish a fix from a coincidence. -- **A measurement, when the claim is about tmux.** tmux's behaviour differs - across the supported range in ways no amount of reading settles. Notes under - `docs/spikes/` record what was measured and against which release. -- **Nothing generated-looking.** Names that say what a thing is for, comments - that say why rather than what, no abstraction without a second caller. - -## Where things live - -A directory is a published artifact exactly when it declares a Maven publication, -and `platformCoversEveryPublishedModule` fails the build when that set stops -matching `libtmux-bom`. Nothing about this is a convention you have to remember. - -Note the wording: *declares a publication*, not *applies the publishing plugin*. -Those came apart once already — `libtmux-kotlin` applied `libtmux.publication`, -which configures publications rather than creating one, so it looked published to -the build and released no jar. A `publishToMavenLocal` found it; the gate now -asks the question that would have. - -| directory | published | holds | -| -------------------------------- | --------- | ----------------------------------------- | -| `libtmux*/` | yes | one artifact each, named for its directory | -| `integration-tests/` | no | the real-tmux suite, which spans artifacts | -| `benchmarks/` | no | the carrier measurements | -| `examples/` | no | whole runnable programs, run by its own suite | -| `docs-tests/` | no | compiles and runs every snippet in the docs | -| `scripts/` | no | what the build does not do | -| `build-logic/` | no | convention plugins, as an included build | -| `docs/`, `gradle/`, `.github/` | no | everything else | - -The suite lives outside every published module on purpose. A suite inside one -artifact's tests makes that artifact's dependencies and lifecycle answerable for -how the whole library is tested. - -## Commit messages - -Imperative subject, then why the change was needed, then what it did: - -``` -Refuse a request control mode cannot answer - -why: Control mode frames a reply per command, so a request holding -several produced several replies for one awaited request, and the -extras were matched to whatever asked next. - -what: -- Refuse a command group before writing anything -``` - -No emoji, no prefixes, no trailing attribution. - -## Carriers - -`ExecutionMode` chooses how a command travels. A suite can run under any of -them, and a failure that appears under only one is a real finding: - -```console -$ LIBTMUX_MODE=control ./gradlew check -``` - -Confirm it against a clean `/tmp` first. Cross-port debris and a genuine carrier -defect look alike. diff --git a/README.md b/README.md index 3b8536e..95cd784 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Status: alpha](https://img.shields.io/badge/status-alpha-orange.svg)](#status) -> **Alpha.** Every published version carries an `-alpha` qualifier and a `0.0.x` -> number, both of which mean the same thing: the API will change without notice, -> and no release is supported once the next one exists. Pin an exact version. +> **Alpha.** Releases carry an `-alpha` prerelease tag. The API is not +> settled, and any release may change or remove exported identifiers without a +> deprecation period. Pin an exact version. Not recommended for production. Typed, blocking access to [tmux](https://github.com/tmux/tmux) from the JVM. From 072e700eca55a9f63f741db98c4db347490c4463 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:31:18 -0500 Subject: [PATCH 2/8] Docs(feat[writing]): Say how this project writes why: The router promised a prose policy and pointed at a file whose every section body was the literal string . An agent or contributor following the route reached nothing, and the rules it was meant to carry stayed only on master, in a file the branch replaced. what: - Fill README, changelog, release notes, commit messages, API documentation, source comments, terminology, and code blocks - Keep the commit format as the single home for it, resolving master's contradiction between AGENTS.md, which mandated the Scope(type[detail]) prefix, and CONTRIBUTING.md, which forbade prefixes outright; the repository's own history uses the prefix - Recast the documentation rules for Javadoc: contract over mechanism, the first sentence as the generated summary, and JSpecify carrying nullability because NullAway already enforces it - State the 80-column wrap for files and the no-wrap rule for pull request and issue bodies, which GitHub renders differently --- .github/WRITING.md | 390 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 382 insertions(+), 8 deletions(-) diff --git a/.github/WRITING.md b/.github/WRITING.md index 92acb05..872c40b 100644 --- a/.github/WRITING.md +++ b/.github/WRITING.md @@ -8,34 +8,408 @@ guides. For build, test, and pull request workflow, see [`CONTRIBUTING.md`](CONTRIBUTING.md). +Markdown files here wrap at 80 columns. A pull request or issue body does not: +GitHub renders a single newline as a space inside a file and as a line break in +a comment, so a hard-wrapped comment body arrives as ragged stubs. + ## README - +The README is the door; the guides under `docs/guide/` and the Javadoc are the +manual. It answers what this is, whether you can use it, how to install it, what +normal use looks like, and what contract you are buying into — in that order, +and then it stops. + +Four things near the top are compatibility claims, and they move together: + +- **The title is `libtmux for Java`.** Every port titles its README the same + way. +- **The alpha notice states the terms in full.** Releases carry an `-alpha` + prerelease tag, the API is not settled, any release may change or remove + exported identifiers without a deprecation period, pin an exact version, not + recommended for production. `CHANGELOG.md` and the `Status` section say the + same thing in the same words. +- **The requirements are JDK 21 or newer, and tmux 3.2a through 3.7b.** The tmux + range is not a claim — the matrix runs every lane of it — so it may not drift + from `workflows/tmux-matrix.yml`. +- **Coordinates are group `io.github.libtmux`, imported through + `libtmux-bom`**, shown for both Gradle and Maven, because a reader arrives + with one of the two already open. + +Every Java block in the README is compiled and then run against a real tmux by +[`docs-tests`](../docs-tests/). A snippet is a test: it cannot be illustrative +pseudo-code, an elided `try` block, or a call that no longer exists, and one +that stops working fails the build. A block that is meant not to compile says so +and is checked for being rejected. + +Claims have to be falsifiable. "Zero runtime dependencies" is one. A performance +claim carries its number and the command that reproduces it, or it does not +appear. + +Headings are literal: `Requirements`, `Installation`, `Documentation`, `Status`. +A reader scanning for how to install does not look under a heading called +something clever, and neither does an agent. + +Badges are metadata, not an introduction. Each of the five present — CI, tmux +matrix, Maven Central, license, status — answers a question a reader would +otherwise have to go and ask. A sixth needs the same defence. + +No screenshot carries information that exists only in the screenshot. Images go +stale, do not survive grep, and reach a screen reader as nothing. ## Changelog - +`CHANGELOG.md` is a ledger for the people who consume releases, not a rendering +of `git log`. The question a reader arrives with is whether an entry affects +them, so one change gets one entry, and the entry leads with what they will +observe. + +Entries land under `## Unreleased`. The maintainer assigns the version when +cutting a release, so nothing written here predicts one. + +Group under the headings already in use — `Added`, `Changed`, `Fixed`, +`Removed`, `Documented`, `Security` — newest release first, dated `YYYY-MM-DD`. + +An entry opens with a bold clause naming what changed, then gives the prose that +makes it decidable: + +```markdown +- **`tmux_whoami` failed on a socket with no server behind it.** It is the tool + the instructions tell a model to call first, and it asked tmux for its + version, which needs a running server. It now says there is no server and + points at `tmux_list_servers`. +``` + +Name identifiers literally: `Pane.capture`, `LIBTMUX_MODE`, `--rerun-tasks`, +`tmux://panes/{pane}`. Lead with a concrete verb — add, fix, remove, reject, +`now`, `no longer`. + +State a changed default explicitly, and an incompatibility more explicitly +still, with the way forward in the same entry. + +Do not sell a fix: "no longer returns another command's reply", not "improves +reliability". Do not describe effort, and never write "various fixes" — a set of +changes too small to enumerate is reported as no user-visible behaviour having +changed. A refactor nothing can observe is not an entry at all. + +This project is alpha, so an incompatibility is expected rather than +exceptional. It still gets stated plainly, with what to write instead. ## Release notes - +A release note answers a different question from the changelog: not "what +changed" but "should I upgrade, and what do I need to know first". It is +editorial where the changelog is exhaustive, and it may leave things out. + +Order it capability, then consequence, then compatibility: + +> 0.0.1-alpha.7 adds streaming capture, and rejects `windowId` at pane scope +> rather than ignoring it. Pass `scope: window` to read at window scope. JDK 21 +> and tmux 3.2a through 3.7b are unchanged. + +The title is plain — the version, optionally preceded by `libtmux for Java`. +Never "we are excited to announce"; state what shipped. + +Link `CHANGELOG.md` rather than pasting it. How a release is actually cut is in +[`RELEASING.md`](../RELEASING.md). ## Commit messages - +``` +Scope(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. + +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 +- **java(deps)**: Dependencies +- **java(deps[dev])**: Dev dependencies +- **ai(rules[AGENTS])**: AI rule updates + +Example: + +``` +Pane(feat[sendKeys]): Add support for a literal flag + +why: Send characters without tmux interpreting them. + +what: +- Add a literal field to SendKeysRequest +- Pass -l when it is set +``` + +Use a heredoc so the formatting survives the shell: + +```console +$ git commit -m "$(cat <<'EOF' +Scope(feat[detail]): Concise description + +why: Explanation of the change. + +what: +- First change +- Second change +EOF +)" +``` + +The rationale, the alternatives weighed, and what was rejected go in the body. +It is the durable record — timestamped, attached to the exact diff, and free to +maintain — which is why a source comment does not carry them. + +No emoji, no trailing attribution, no tool metadata. + +### Release commits + +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`. The detailed why +and what go in the body. Do not use the `Scope(type[detail]):` format for a +release — it buries the lede. ## API documentation - +Javadoc on public API is a specification, not a programming guide. It states +what a caller may rely on, which is the part the signature cannot state on its +own. + +The first sentence becomes the summary in every index the tool generates, so it +has to stand alone and end at the first period. Lead with a verb describing the +thing: + +```java +/** Captures the visible contents of this pane. */ +``` + +Not "This method is used to capture…". Deleting the introductory clause is the +most useful edit available. + +Document the contract, not the mechanism. What a reader infers the API from: + +- **Nullability.** Packages are `@NullMarked`, so a reference is non-null unless + it carries `@Nullable`. NullAway enforces that in JSpecify mode at build time, + which makes the annotation the documentation — a prose sentence repeating it + is a second copy to keep true. +- **What a returned collection is.** Whether it is immutable, whether it is a + view over live state, and what order it is in. +- **Blocking and threading.** Whether the call waits on tmux, and whether the + type is safe to use from more than one thread. +- **Failure.** Which exceptions, and the condition producing each, the unchecked + ones included. +- **Lifetime.** What holds after the owning `Server` is closed. + +Tags carry their weight: + +- `@param` completes a phrase: `@param history scrollback lines to include, 0 + for the visible screen only`. Not `@param history the history`. +- `@return` documents semantics, not the type: `@return the pane contents, never + null`. Not `@return a String`. +- `@throws` documents the condition: `@throws IllegalArgumentException if + {@code history} is negative`. Not "when an illegal argument is supplied". +- `@since` carries the product version the declaration first appeared in. +- `@deprecated` names the replacement and shows the migration, mirroring the + `@Deprecated(since = …, forRemoval = …)` beside it. Where removal is not + actually decided, do not invent a version. + +Architecture belongs in `package-info.java` — what the package is for, its major +types, the threading model, what holds across all of it — so that type-level +Javadoc stays local and contractual. + +Javadoc is exempt from the loss gate below, because it serves the caller rather +than the maintainer. It is exempt from nothing else. Ceiling: a good man page +entry. ## Source comments - +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. + +### Ceiling + +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. + +### Keep + +- 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. + +### Delete + +- 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. + +### The upkeep gate in practice + +It reaches values that track our own code. It does not reach frozen external +facts. + +Bad (Delete): + +```java +// There are 321 tests to complete for servers. +``` + +Good (Keep): + +```java +// tmux < 3.2 reports the pane ID only after the command completes, +// so this query must stay separate. +``` + +### Documentation exception + +Minimal usage examples, and param, return, and throws 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. + +Javadoc summaries and `@param`, `@return`, and `@throws` tags fall under this +exception. ## Terminology and capitalization - +One voice across every surface. A Javadoc comment says what a caller may rely +on; a changelog entry says what changed; an error message names what failed; +prose says what happens. All of them are present tense, lead with the thing +being described, and stop. + +The most useful editing operation is deleting the introductory sentence. + +| Instead of | Prefer | +| --- | --- | +| "We added…" | "`Pane` now supports…" | +| "New and improved" | "`Pane` now…" | +| "powerful", "seamless" | state the capability | +| "easily", "simply", "just" | omit | +| "robust" | name the failure that is handled | +| "comprehensive" | name what is covered | +| "production-ready" | state the guarantee | +| "optimized" | give the magnitude | +| "various fixes" | name the components | +| "under the hood" | omit unless observable | +| "please note that" | state the fact | +| "leverage", "utilize" | "use" | +| "delve into" | "read", or omit | +| "best practices" | name the practice | +| "in order to" | "to" | + +Spell the project's own terms one way, everywhere. Grep is part of debugging, +users paste error text into a search box, and an agent reasoning across files +has nothing but the string: + +- **tmux** is lowercase, always, including at the start of a sentence. +- **Javadoc** is capitalized; **JDK** and **JVM** are upper case. "Java 21" and + "JDK 21" both appear upstream — this project writes **JDK 21**. +- A **pane**, **window**, **session**, and **server** are what tmux calls them. + Do not introduce a synonym for one. +- Write the identifier, not a description of it: `LIBTMUX_MODE=control`, not + "the mode environment variable"; `--rerun-tasks`, not "the rerun flag"; + `/tmp/libtmux-java-test/`, not "the test socket directory". + +Treat AI slop as review-hostile noise. The goal is information density: + +- **AI signatures.** No "Generated by", no conversational filler, no unexplained + emoji, no tool metadata. +- **Brittle references.** No hard-coded line numbers, fragile file 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, Javadoc, README, or a pull + request description. The diff and the 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. +- **Low-value scaffolding.** No ownerless TODOs, unused future-proofing, debug + artefacts, or defensive wrappers around failure modes nothing can reach. +- **Coded labels.** Write rules and findings as plain imperatives. No `[R1]`, + `Option B`, or any index a reader has to decode. + +Never delete a comment documenting an invariant, a protocol constraint, a +platform quirk, or an upstream workaround. Those are the facts +[Source comments](#source-comments) keeps. ## Code blocks - +Code blocks are paste-and-run units: pasting one block runs exactly one +intended action. Doctests and other 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 `console` tag 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: + +```console +$ git log \ + --max-count=10 \ + --graph \ + --oneline +``` + +Bad: + +```console +# Show the last ten commits as a graph +$ git log --max-count=10 --graph --oneline +``` From cbe421b0d10e4e5460383147aa8fb7b542251dcc Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:31:26 -0500 Subject: [PATCH 3/8] Docs(feat[contributing]): Say how this project works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Same empty route as the writing policy, and this file also has to absorb the socket-hygiene rule. That rule was 40 lines of test hygiene sitting in AGENTS.md, which is read on every task — exactly the always-on cost the router refactor exists to remove. what: - Fill building, running the tests, checks, pull requests, review, releases, and compatibility - Move the socket-hygiene rule here in full: the two roots, why a bare /tmp/libtmux- prefix collides with a sibling port, the failure modes that debris produces, and how to find whose servers are running - Carry over the published-directory table and the libtmux-kotlin case that produced the platformCoversEveryPublishedModule gate - Say that a green check reporting UP-TO-DATE verified nothing, and that the exit status is what to read rather than the last lines of output - Describe review as what actually happens rather than inventing a process the project does not run --- .github/CONTRIBUTING.md | 200 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 192 insertions(+), 8 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 88711c2..03fdc99 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,35 +1,219 @@ # Contributing - +libtmux for Java is twelve Gradle subprojects plus `build-logic`, an included +build holding the convention plugins. The gates below are what a change has to +pass before it is proposed. How this project writes prose — README, changelog, release notes, commit messages, API documentation, and source comments — is set out separately in [`WRITING.md`](WRITING.md). Read that before changing any of it. +A directory is a published artifact exactly when it declares a Maven +publication, and the build fails when that set stops matching `libtmux-bom`. +Nothing about this is a convention you have to remember: + +| directory | published | holds | +| --- | --- | --- | +| `libtmux*/` | yes | one artifact each, named for its directory | +| `integration-tests/` | no | the real-tmux suite, which spans artifacts | +| `benchmarks/` | no | the carrier measurements | +| `examples/` | no | whole runnable programs, run by its own suite | +| `docs-tests/` | no | compiles and runs every snippet in the docs | +| `scripts/` | no | what the build does not do | +| `build-logic/` | no | convention plugins, as an included build | +| `docs/`, `gradle/`, `.github/` | no | everything else | + +The real-tmux suite lives outside every published module on purpose. A suite +inside one artifact's tests makes that artifact's dependencies and lifecycle +answerable for how the whole library is tested. + ## Building - +You need JDK 21 or newer and tmux on `PATH`. Nothing else — Gradle provisions +the toolchain, and the library has no runtime dependencies. + +`./gradlew` is the only supported entry point. A locally installed `gradle` is +not: the wrapper pins the version the build was written against, and +`build-logic` is an included build that a mismatched Gradle will configure +differently. + +```console +$ ./gradlew build +``` ## Running the tests - +**Every tmux server this project starts belongs under a path that names this +port.** Use one of: + +- `/tmp/libtmux-java-test/…` — anything a test starts. +- `/tmp/libtmux-java-dev/…` — anything started by hand while investigating. + +Never `/tmp/libtmux-…` on its own, and never the default socket. `libtmux-` is +the prefix a sibling port is also using, which is the whole problem. + +Sibling libtmux ports — Python, Go, Rust, TypeScript, C#, C++, Swift — are +worked on in parallel on the same machine, and every one of them starts real +tmux servers in `/tmp`. Their debris outlives their test runs: servers from an +exited benchmark stay up, holding ptys, until something kills them. + +Why that matters more than tidiness: a suite sharing `/tmp` with another port's +leftovers fails intermittently, under load, in places that have nothing to do +with the change under test — `SERVER_GONE`, misframed rows, `fork: No space +left on device`. Every one of those reads as a regression in whatever you last +touched, and hours go into a bug that was never yours. + +Before blaming a change for an intermittent real-tmux failure, count what is +already running: + +```console +$ pgrep -c tmux +``` + +Then find whose it is, since a socket path exists only in the process's own +command line: + +```console +$ for p in $(pgrep tmux); do tr '\0' ' ' < /proc/$p/cmdline | rg -o '\-S [^ ]+'; done +``` + +Kill only sockets under this port's roots. Another port's servers are not yours +to reap, however much they cost us: + +```console +$ ./scripts/reap-stale-servers.sh +``` + +A unix socket path cannot exceed about 104 bytes, and tmux reports a longer one +as `error connecting to … (File name too long)`. That rules out sockets under a +build directory or a deep scratch path, and it is why the roots above are short. + +`ExecutionMode` chooses how a command travels, and the suite can be run under +any of them: + +```console +$ LIBTMUX_MODE=control ./gradlew check +``` + +A failure that appears only under one carrier is a real finding — the library +claims the answer does not depend on the carrier, and +`ExecutionModeConformanceTest` is where that claim is gated. Confirm it against +a clean `/tmp` first: the failure modes of cross-port debris and of a genuine +carrier defect look alike. + +Against every supported tmux release: + +```console +$ ./gradlew testTmuxMatrix -PlibtmuxMatrix=/path/to/tmux/builds +``` + +The matrix is a local tree of built tmuxes, one directory per lane, each with +`bin/tmux`. Build one: + +```console +$ ./scripts/tmux-matrix.sh ~/tmux-builds +``` + +It reads the lane list out of the build, so it cannot drift from what the matrix +actually runs. ## Checks that must pass - +One command has to pass before anything is proposed: + +```console +$ ./gradlew check +``` + +It runs formatting, Error Prone, NullAway in JSpecify mode, and every test +including the ones that start real tmux servers. It also compiles and runs every +Java snippet in `README.md`, in each module's README, and in the guides — a +snippet that stopped working fails the build. + +A green `check` that reported `UP-TO-DATE` for every task verified nothing. +Force it when that matters: + +```console +$ ./gradlew check --rerun-tasks +``` + +Check the exit status rather than the last lines of output. Piping to `tail` +reports the pipe's status, which hides `BUILD FAILED`. + +`platformCoversEveryPublishedModule` fails the build when the set of published +directories stops matching `libtmux-bom`. Note the wording used above: *declares +a publication*, not *applies the publishing plugin*. Those came apart once +already — `libtmux-kotlin` applied `libtmux.publication`, which configures +publications rather than creating one, so it looked published to the build and +released no jar. A `publishToMavenLocal` found it; the gate now asks the +question that would have. + +The tmux matrix is not part of `check`, and a green `check` has not predicted +it. Run the matrix before a release. ## Pull requests - +One subject per pull request. Unrelated cleanup found along the way belongs in +its own commit, and usually in its own pull request. + +What a change is expected to carry: + +- **A test that failed before it.** For a bug, that test is the evidence the bug + was real; without it there is nothing to distinguish a fix from a coincidence. + A passing gate is evidence only once it has been shown capable of failing. +- **A measurement, when the claim is about tmux.** tmux's behaviour differs + across the supported range in ways no amount of reading settles. Notes under + `docs/spikes/` record what was measured and against which release. +- **A changelog entry, when a caller can observe the change.** It goes under + `## Unreleased`. Nothing enforces this, which is why it is written down. +- **Nothing generated-looking.** Names that say what a thing is for, comments + that say why rather than what, no abstraction without a second caller. + +Commit format is in [`WRITING.md`](WRITING.md). The constraints every change is +held to, whatever it touches, are in [`AGENTS.md`](../AGENTS.md). ## Review - +Review is the maintainer reading the diff against what this file already asks +for: the gates ran, the change carries what `Pull requests` says it must, and +the scope matches what was asked for. + +Nothing here is automated beyond the checks on the pull request, and there is no +approval quorum — this is a small project and says so rather than describing a +process it does not run. + +Disagreement about a rule is resolved by changing the rule in this file or in +[`WRITING.md`](WRITING.md), not by making an exception in review. ## Releases - +Never create tags. Never push tags. The owner handles tagging and tag pushes, +because a tag triggers the publish workflow, which signs the artifacts and +pushes them to the Central Portal. + +A release commit subject is plain and short: `Tag v`. + +How a release is actually cut — the version property, the signing key, the order +to do things in — is in [`RELEASING.md`](../RELEASING.md). ## Compatibility - +**JDK 21 is the floor.** Three places state it and all three have to agree: the +toolchain and `options.release` in +`build-logic/src/main/kotlin/libtmux.java-library.gradle.kts`, the version +matrix in the CI workflow, which builds on 21 and 25, and the claim `README.md` +makes under `Requirements`. + +**tmux 3.2a through 3.7b is the supported range**, and it is not a claim: the +whole real-tmux suite runs against every one of those releases, and each lane +checks it really ran the tmux it is named after. Moving the range means moving +`workflows/tmux-matrix.yml` and the README together. + +**The API carries no compatibility guarantee.** This project is alpha: releases +carry an `-alpha` prerelease tag, any release may change or remove exported +identifiers without a deprecation period, and only the newest release is +supported. Pin an exact version. + +That is a statement about the Java API, not about tmux correctness. Behaviour +against the supported tmux range is gated on every push, and a change there is a +bug rather than a liberty the alpha terms allow. From 3da95e49393ef49f126c9f628304b9f8008a0a26 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:31:31 -0500 Subject: [PATCH 4/8] Docs(feat[agents]): Give the router its constraints why: The root file is read on every task, so what it holds should be what applies before the class of change is known. It had a placeholder where those constraints belong, and no route to RELEASING.md. what: - State the constraints that bind every change: smallest coherent change, reuse before adding, package-private until a caller needs it, a new file only for a durable boundary, and a gate that has been shown capable of failing - Name the socket roots as a constraint and route to CONTRIBUTING.md for why, rather than restating the reasoning here - Add the release route --- AGENTS.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 3d3bd69..2c8fd7a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,21 @@ Follow the existing project conventions and keep changes narrowly scoped to what was asked for. - +These hold for every change, whatever it touches: + +- Make the smallest coherent change that solves the verified problem; keep + unrelated cleanup out of it. +- Reuse an existing type, helper, or test before adding a new one. +- Keep new API package-private until a caller outside the package needs it. +- Add a file only for a durable boundary — a distinct responsibility, + independent reuse, or splitting an oversized type — never for a single-use + helper or a one-line re-export. +- A passing gate is evidence only once it has been shown capable of failing. + Pair a new test with a deliberate break that proves it bites. +- Every tmux server this project starts belongs under `/tmp/libtmux-java-test/` + or `/tmp/libtmux-java-dev/` — never a bare `/tmp/libtmux-…`, which a sibling + port on this machine is also using. `.github/CONTRIBUTING.md` explains what + goes wrong when they share. ## Additional guidance @@ -16,3 +30,4 @@ being made: - Contribution workflow, building, testing, and pull requests: [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md) - Security-sensitive changes: [`SECURITY.md`](SECURITY.md) +- Cutting a release: [`RELEASING.md`](RELEASING.md) From e8f8e73007fca142858fa0aad2722160a9db2c9e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:32:51 -0500 Subject: [PATCH 5/8] Docs(fix[links]): Point at CONTRIBUTING where it now lives why: Moving CONTRIBUTING.md under .github/ left two links resolving to nothing, and moving the socket-hygiene rule out of AGENTS.md left six files pointing at a file that no longer explains what they cite it for. what: - Repoint README.md and scripts/README.md at .github/CONTRIBUTING.md - Repoint the socket-hygiene referrers in gradle.properties, reap-stale-servers.sh, scripts/README.md, integration-tests/README.md, libtmux-junit5/README.md, and docs/spikes/22-abandoned-servers.md - Leave docs/parity/test-map.md alone: its "per AGENTS.md" quotes the Python repository's file inside a row describing a Python test --- README.md | 5 +++-- docs/spikes/22-abandoned-servers.md | 11 ++++++----- gradle.properties | 4 ++-- integration-tests/README.md | 6 +++--- libtmux-junit5/README.md | 5 +++-- scripts/README.md | 4 ++-- scripts/reap-stale-servers.sh | 3 ++- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 95cd784..1d1a632 100644 --- a/README.md +++ b/README.md @@ -337,8 +337,9 @@ choice. ## Contributing -[`CONTRIBUTING.md`](CONTRIBUTING.md) covers the gate, the tmux matrix, and why -every server this suite starts lives under a path naming this port. +[`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md) covers the gate, the tmux +matrix, and why every server this suite starts lives under a path naming this +port. ## Status diff --git a/docs/spikes/22-abandoned-servers.md b/docs/spikes/22-abandoned-servers.md index 679edc7..d309c18 100644 --- a/docs/spikes/22-abandoned-servers.md +++ b/docs/spikes/22-abandoned-servers.md @@ -64,11 +64,12 @@ design, so tmux would destroy them the moment they were made. **D** works, and works quickly. A `run-shell -b` loop polling the owner's pid reaped the server 0.2 s after `kill -9` of its owner. It was still rejected. -`AGENTS.md` records this machine failing with `fork: No space left on device` -when too many tmux servers are alive; doubling the processes each server holds -spends exactly the resource that is already short, permanently, to shorten a -window that a sweep closes for free. It also needs the lane's own tmux binary -inside a shell string, which the matrix would get wrong. +`.github/CONTRIBUTING.md` records this machine failing with `fork: No space +left on device` when too many tmux servers are alive; doubling the processes +each server holds spends exactly the resource that is already short, +permanently, to shorten a window that a sweep closes for free. It also needs +the lane's own tmux binary inside a shell string, which the matrix would get +wrong. **A** was measured rather than assumed: diff --git a/gradle.properties b/gradle.properties index 0dbf285..569a753 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,8 +16,8 @@ org.gradle.caching=true # Deliberately not org.gradle.parallel. Four of the five modules run real tmux servers, and this # repository shares /tmp with sibling ports whose leftovers already cost it intermittent failures — -# see AGENTS.md. Module-level parallelism would multiply the servers alive at once for a saving that -# has not been measured. Turn it on with a measurement, not on principle. +# see .github/CONTRIBUTING.md. Module-level parallelism would multiply the servers alive at once +# for a saving that has not been measured. Turn it on with a measurement, not on principle. # 512m is not enough for Error Prone and NullAway on this source tree. org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 diff --git a/integration-tests/README.md b/integration-tests/README.md index 6870d7c..55643db 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -43,9 +43,9 @@ first: $ ./scripts/reap-stale-servers.sh ``` -[`AGENTS.md`](../AGENTS.md) explains why their debris becomes this suite's -intermittent failures, and why only servers under this port's own roots are ever -reaped. +[`CONTRIBUTING.md`](../.github/CONTRIBUTING.md) explains why their debris +becomes this suite's intermittent failures, and why only servers under this +port's own roots are ever reaped. ## Next diff --git a/libtmux-junit5/README.md b/libtmux-junit5/README.md index 247655d..036f7f7 100644 --- a/libtmux-junit5/README.md +++ b/libtmux-junit5/README.md @@ -120,8 +120,9 @@ Which is how one suite runs against a whole matrix of releases — see If your project also runs another tmux-using suite, give each one a socket root that names it, and never use the default socket. This repository's own rule and -the failure modes behind it are in [`AGENTS.md`](../AGENTS.md); the short version -is that another suite's leftovers become your intermittent failures. +the failure modes behind it are in +[`CONTRIBUTING.md`](../.github/CONTRIBUTING.md); the short version is that +another suite's leftovers become your intermittent failures. ## Next diff --git a/scripts/README.md b/scripts/README.md index b6fa366..b236b0e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -37,7 +37,7 @@ since removed, which is then addressable only by its own argv. **It only ever touches sockets under this port's roots** — `/tmp/libtmux-java-test` and `/tmp/libtmux-java-dev`. Sibling ports' servers are counted and reported, never -killed. [`AGENTS.md`](../AGENTS.md) explains why. +killed. [`CONTRIBUTING.md`](../.github/CONTRIBUTING.md) explains why. ## Try the MCP server in a real agent @@ -77,4 +77,4 @@ invocation instead — `claude --mcp-config --strict-mcp-config`, or ## Next -- [CONTRIBUTING](../CONTRIBUTING.md) · [Root README](../README.md) +- [CONTRIBUTING](../.github/CONTRIBUTING.md) · [Root README](../README.md) diff --git a/scripts/reap-stale-servers.sh b/scripts/reap-stale-servers.sh index 3fe6035..85fb859 100755 --- a/scripts/reap-stale-servers.sh +++ b/scripts/reap-stale-servers.sh @@ -9,7 +9,8 @@ # argv, and nothing reaps it until the next run. This is that, by hand. # # Only sockets under this port's roots are ever touched. Sibling libtmux ports run on this machine -# and their leftovers are not ours to reap, however much they cost us; AGENTS.md explains why. +# and their leftovers are not ours to reap, however much they cost us; +# .github/CONTRIBUTING.md explains why. set -eu reap=false From 92a04813b64fe0bd58ed6b6eb360121103e8645f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:33:19 -0500 Subject: [PATCH 6/8] Docs(fix[alpha]): State the alpha terms the same way everywhere why: Three places stated the same terms three ways. The README blockquote had been updated to the wording every port now shares, while the README's own Status section and the changelog preamble still carried the older phrasing, so the file disagreed with itself. what: - Restate README Status and the CHANGELOG preamble in the shared terms: an -alpha prerelease tag, an unsettled API, identifiers that may change or be removed with no deprecation period, pin an exact version, not recommended for production - Keep the point that tmux correctness is not alpha, being gated against every supported release on each push - Drop the Maven comparator explanation from Status; gradle.properties already carries it beside the version it explains --- CHANGELOG.md | 10 +++++----- README.md | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e76230c..5538e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ Notable changes, newest first. Versions follow [semantic versioning][semver]. -**This project is alpha.** Every version carries an `-alpha` qualifier, which is -the lowest Maven's comparator recognises, and a `0.0.x` number. Both say the same -thing: anything below may change in any release, including in ways that do not -compile. Only the newest version is supported, and there are no backports. Pin an -exact version rather than a range. +**This project is alpha.** Releases carry an `-alpha` prerelease tag. The API is +not settled, and any release may change or remove exported identifiers without a +deprecation period. Only the newest release is supported, and there are no +backports. Pin an exact version rather than a range. Not recommended for +production. [semver]: https://semver.org/spec/v2.0.0.html diff --git a/README.md b/README.md index 1d1a632..d7178a5 100644 --- a/README.md +++ b/README.md @@ -343,15 +343,15 @@ port. ## Status -**Alpha.** `alpha` is the lowest qualifier Maven's own comparator recognises — -its order runs `alpha < beta < milestone < rc < snapshot < release` — so nothing -published here can sort below what is published today, and every future release -supersedes it cleanly. +**Alpha.** Releases carry an `-alpha` prerelease tag. The API is not settled, +and any release may change or remove exported identifiers without a deprecation +period. Pin an exact version. Not recommended for production. What that means in practice: -- **The API will change without notice**, including in ways that do not compile. -- **Only the newest version is supported.** There are no backports. +- **Any release may change or remove exported identifiers**, without a + deprecation period, including in ways that do not compile. +- **Only the newest release is supported.** There are no backports. - **Pin an exact version.** A range will move under you. - What is *not* alpha is the tmux correctness: the whole real-tmux suite runs against all eight supported releases on every push. From 1515e7ece6250c3e4f8f803f319d3b22b7353a77 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:41:57 -0500 Subject: [PATCH 7/8] Docs(fix[contributing]): Drop the subproject count, log the move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: The opening line said twelve subprojects where settings.gradle.kts declares eleven, and a hardcoded count is what the upkeep gate and the slop rule both forbid — it is false the first module that moves. The branch also owed itself a changelog entry: it moves a file callers link to, and the rule requiring one ships in this same branch. what: - Describe the build without counting it, as the published-artifact rule beneath it already does - Add the Documented entry for the CONTRIBUTING.md move - Present the changelog categories as the allowed set rather than the set already in use; Security has no entries yet --- .github/CONTRIBUTING.md | 2 +- .github/WRITING.md | 4 ++-- CHANGELOG.md | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 03fdc99..8f0888f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -libtmux for Java is twelve Gradle subprojects plus `build-logic`, an included +libtmux for Java is a multi-project Gradle build plus `build-logic`, an included build holding the convention plugins. The gates below are what a change has to pass before it is proposed. diff --git a/.github/WRITING.md b/.github/WRITING.md index 872c40b..4c6a776 100644 --- a/.github/WRITING.md +++ b/.github/WRITING.md @@ -66,8 +66,8 @@ observe. Entries land under `## Unreleased`. The maintainer assigns the version when cutting a release, so nothing written here predicts one. -Group under the headings already in use — `Added`, `Changed`, `Fixed`, -`Removed`, `Documented`, `Security` — newest release first, dated `YYYY-MM-DD`. +Group under `Added`, `Changed`, `Fixed`, `Removed`, `Documented`, or `Security`, +newest release first, dated `YYYY-MM-DD`. An entry opens with a bold clause naming what changed, then gives the prose that makes it decidable: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5538e67..7fe6293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ production. ## Unreleased +### Documented + +- **`CONTRIBUTING.md` has moved to `.github/CONTRIBUTING.md`.** The policy an + agent or contributor reads is now split three ways: `AGENTS.md` routes and + carries only what applies to every change, `.github/WRITING.md` covers how + this project writes, and `.github/CONTRIBUTING.md` covers how it works. A + link to the old path needs updating; GitHub finds the new one on its own. + ## 0.0.1-alpha.6 — 2026-08-16 ### Fixed From 35e404b2b74c1fbc4511a502c875a74354a34ec5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:57:28 -0500 Subject: [PATCH 8/8] Docs(fix[docs-tests]): Say what else it checks, and how MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: The module reads more than it compiles, and only the compiling half was written down. DocumentationFactsTest holds the versions in install blocks, the platform's own README and the parity citations to the build, and nothing said so — the first anyone learns of it is a failure naming a check no document mentions. The gate a contributor is told to run before opening a pull request did not route to any of it either. what: - Add a section for the claims that are not code, one row per check and where it looks, including the two documents nothing takes snippets from - Say a type-declaring block is compiled and never run, whatever its directive: a declaration has nothing to execute - Say throws: matches the exception's simple name, and that the directive comment has to sit directly above the fence - Add yamlString to the fixtures a snippet may assume, and stop counting them in prose; the count is the code's to own - Scope the document list to snippets, since the other checks read wider - Name :docs-tests:test in the checks that must pass and route to this README from there, beside the code that reads the directives --- .github/CONTRIBUTING.md | 19 ++++++++++++--- docs-tests/README.md | 54 ++++++++++++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8f0888f..773942d 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -18,7 +18,7 @@ Nothing about this is a convention you have to remember: | `integration-tests/` | no | the real-tmux suite, which spans artifacts | | `benchmarks/` | no | the carrier measurements | | `examples/` | no | whole runnable programs, run by its own suite | -| `docs-tests/` | no | compiles and runs every snippet in the docs | +| [`docs-tests/`](../docs-tests/) | no | compiles and runs every snippet in the docs | | `scripts/` | no | what the build does not do | | `build-logic/` | no | convention plugins, as an included build | | `docs/`, `gradle/`, `.github/` | no | everything else | @@ -126,9 +126,20 @@ $ ./gradlew check ``` It runs formatting, Error Prone, NullAway in JSpecify mode, and every test -including the ones that start real tmux servers. It also compiles and runs every -Java snippet in `README.md`, in each module's README, and in the guides — a -snippet that stopped working fails the build. +including the ones that start real tmux servers. + +The Java in the documentation is compiled and run as part of that, and the +claims around it — the version in every install block, what the platform says it +manages — are checked with it: + +```console +$ ./gradlew :docs-tests:test +``` + +How a fence says what it is — the directives, the `// →` assertions, the +fixtures a snippet may assume — is in +[`docs-tests/README.md`](../docs-tests/README.md), beside the code that reads +them. A green `check` that reported `UP-TO-DATE` for every task verified nothing. Force it when that matters: diff --git a/docs-tests/README.md b/docs-tests/README.md index 3db4fa7..d159a79 100644 --- a/docs-tests/README.md +++ b/docs-tests/README.md @@ -1,6 +1,7 @@ # docs-tests -**Compiles and runs the code in the documentation. Not published.** +**Compiles and runs the code in the documentation, and checks the claims around +it. Not published.** A snippet is the part of a project people copy and the part nothing compiles, so it goes stale silently — and a stale snippet reads exactly as well as a working @@ -32,10 +33,20 @@ Say otherwise with an HTML comment directly above the fence: | `` | compiles; not run, for the stated reason | | `` | not checked, for the stated reason | -An unrecognised directive fails the build. A snippet nobody is checking, because -of a typo in the thing that says how to check it, is the state this exists to +The comment has to sit directly above the fence, with nothing between them. An +unrecognised directive fails the build: a snippet nobody is checking, because of +a typo in the thing that says how to check it, is the state this exists to prevent. +`throws:` names the exception's simple name, not its package — the comparison is +against `getClass().getSimpleName()`, so `IllegalArgumentException` matches and +`java.lang.IllegalArgumentException` does not. + +A block that declares a type — a `class`, `record`, `interface` or `enum` — is +compiled and never run, whatever its directive says, because a declaration has +nothing to execute. Statements are wrapped in a method body with the fixtures +below in scope; a type is compiled as it stands. + `does-not-compile` earns its keep: it is what keeps `Pane_.index().startsWith("2")` an error. A README claiming the compiler rejects something would otherwise survive the day it stopped being true. @@ -72,11 +83,11 @@ Two consequences worth knowing: ## What a snippet may assume -Documentation shows the interesting line, not the six before it that made a -server. Those six are supplied: `server`, `config`, `session`, `window`, `pane`, -`options`, `socket`, `directory`, `timeout`, and the common imports. A snippet -declaring its own `server` shadows the supplied one, which is what a reader -copying it would get anyway. +Documentation shows the interesting line, not the ones before it that made a +server. Those are supplied: `server`, `config`, `session`, `window`, `pane`, +`options`, `socket`, `directory`, `timeout`, `yamlString`, and the common +imports. A snippet declaring its own `server` shadows the supplied one, which is +what a reader copying it would get anyway. Consequently a fence cannot depend on a variable another fence declared — and neither can a reader who copies just that fence. @@ -96,9 +107,34 @@ documentation, the two cannot drift. $ ./gradlew :libtmux-kotlin:test ``` +## Claims that are not code + +A snippet is executed, so it cannot lie. A version in an install block, or a +list of what the platform manages, is prose — and prose is what is still wrong +six months later, in the one place every reader starts. Those are checked too: + +| what is checked | where it looks | +| --- | --- | +| Every coordinate names the version this build would publish | the root README, `libtmux-bom`'s, every published module's, the Kotlin and Scala guides, and `RELEASING.md` | +| `libtmux-bom`'s README lists exactly what the platform constrains | that README against `libtmux-bom/build.gradle.kts` | +| Every published module's README names it first and states its coordinate | each published module's README | +| A fence in a source language nothing here builds carries a directive saying so | every reader-facing document | +| The contract tests the parity documents cite are unwritten or really declared | `docs/parity/python-api.md`, `docs/parity/test-map.md` | +| Those documents keep saying "planned parity" while those tests are unwritten | the same two | + +The last two are why this module reads documents it takes no snippets from. +`docs/parity/` holds no Java, and `RELEASING.md` is not a place snippets come +from, but a coordinate in either is a claim like any other. + +The snippet suite also asserts a floor on how much it found. A filter or a +rename can reduce a parameterised suite to nothing without failing anything, and +a suite that discovers nothing passes loudly. + ## Which documents -`README.md`, every package's `README.md`, and `docs/guide/*.md`. +Snippets come from `README.md`, every package's `README.md`, and +`docs/guide/*.md`. The checks above that are not about snippets read more than +that, and each row says where it looks. Not `docs/spikes`, `docs/plans` or `docs/studies`: those are dated records of what was measured or decided at the time. Holding them to today's API would either