diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..773942d --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,230 @@ +# Contributing + +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. + +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/`](../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. + +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: + +```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. diff --git a/.github/WRITING.md b/.github/WRITING.md new file mode 100644 index 0000000..4c6a776 --- /dev/null +++ b/.github/WRITING.md @@ -0,0 +1,415 @@ +# 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). + +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 `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: + +```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 +``` diff --git a/AGENTS.md b/AGENTS.md index 3ca7a68..2c8fd7a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,264 +1,33 @@ -# AGENTS.md - -Guidance for agents working on libtmux for Java. This repository is the whole -of the port; nothing outside it governs the Java code. - -## 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. - -**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. - -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 -``` +# Agent instructions + +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 + +This file routes; it does not restate. Read the policy that governs the change +being made: + +- 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) +- Cutting a release: [`RELEASING.md`](RELEASING.md) diff --git a/CHANGELOG.md b/CHANGELOG.md index e76230c..7fe6293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,24 @@ 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 ## 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 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..d7178a5 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. @@ -337,20 +337,21 @@ 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 -**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. 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 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