From df10299e8c2c149d643d4bd45db9822e505f693c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:10:35 -0500 Subject: [PATCH 1/7] Docs(feat[writing]): Say how this project writes why: The prose conventions existed as habit and as scattered rules inside AGENTS.md, so every new document argued them out again and a contributor had nowhere to look. Splitting them out gives them one home, and lets a change that touches no prose skip loading them. what: - Add .github/WRITING.md: voice with a diction table, README, changelog, release notes, commit messages, XML documentation, source comments, terminology, Markdown, code blocks, error messages, slop prevention - Record the commit format this repository already uses, and say plainly that Conventional Commits are not used and should not be introduced - Ground the XML documentation rules in the tree: one-line summaries, verb-first, remarks for the contract, and as the rare case it actually is rather than a default - Ban GitHub alert blocks, because nuget.org renders this README and does not understand them - Say how documented examples are verified: every C# block is compiled and a "csharp run" block is executed, with anchoring as the separate guarantee against drift --- .github/WRITING.md | 491 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 .github/WRITING.md diff --git a/.github/WRITING.md b/.github/WRITING.md new file mode 100644 index 0000000..72c4f40 --- /dev/null +++ b/.github/WRITING.md @@ -0,0 +1,491 @@ +# Writing + +How this project writes: `README.md`, `CHANGELOG.md`, release notes, commit +messages, XML documentation, source comments, and error messages. It governs +every surface a reader reaches, and applies to a one-line `` as much +as to a release announcement. + +[CONTRIBUTING.md](CONTRIBUTING.md) covers how we work — the toolchain, the +gates, what a change has to carry. This covers how we write. + +## Voice + +Calm, literal, precise, useful. Write as though the reader is competent, busy, +and may eventually have to debug this library at 2 a.m. + +- **Understated, not enthusiastic.** "Reads one option." Not "Effortlessly + access the full power of tmux options!" +- **Declarative, not chatty.** "Pass `-l` when it is set." Not "All you have to + do is set the flag." +- **Specific, not clever.** "tmux 3.4 escapes a dollar sign twice." Not + "Handles version quirks intelligently." +- **Outcome before implementation.** Say what the caller observes, then how it + works if that knowledge is part of the contract. +- **Sentence-case headings.** "Running the tests", not "Running The Tests". +- **Adjectives only when falsifiable.** `allocation-free`, `thread-safe`, + `trim-safe`, `O(n)` carry information. `powerful`, `seamless`, `robust`, + `blazing-fast` do not. + +The most useful editing operation is deleting the introductory sentence. + +| Instead of | Prefer | +| --- | --- | +| "We added…" | "`Pane.CaptureAsync` now…" | +| "New and improved" | "`Foo` now…" | +| "powerful", "seamless" | state the capability | +| "easily", "simply" | 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" | + +## README + +The README is an onboarding path, not the project's biography. It is also +package metadata: nuget.org renders it, so it is the first thing most readers +of this library ever see. + +The first screen carries the title, one sentence saying exactly what the +library does, the badges, and a minimal example that compiles. Everything else +comes after. + +Show the golden path first, then configuration, then the advanced case. +Architecture and design notes go late, or in `docs/` — a reader wants to know +whether they need the library before they are told how it is built. + +State compatibility explicitly rather than making somebody open a `.csproj`. +The supported tmux versions, the target frameworks, the supported operating +systems, and trim and ahead-of-time safety each get a row. + +Examples are product surface. Every one is compilable, copyable, idiomatic, +and explicit about setup — no elided `using` directives, no `// setup omitted`, +no placeholder methods that do not exist. An example that assumes invisible +context is hostile to a newcomer and useless to an agent. If something is +deliberately left out, say so in prose. + +Put exact symbols in prose. "Set `SendKeysRequest.Literal` to `true`" beats +"turn on the literal flag" — it lets a reader jump straight to the code. + +Prefer boring, predictable headings. "Compatibility" and "Documentation" +retrieve well; "When things get weird" does not. + +## Changelog + +`CHANGELOG.md` is a consumer-facing compatibility ledger, not `git log`. Every +bullet answers one question: what changed for somebody consuming the package? +A refactor nobody can observe is not an entry. + +One change per bullet. Lead with the identifier and a concrete verb — add, +fix, remove, deprecate, `now`, `no longer`. Name identifiers literally: +`Pane.CaptureAsync`, `TMUX_TMPDIR`, `tmux://panes/{pane}`. + +Group under `### Added`, `### Changed`, and `### Fixed`. Bold the opening +sentence of anything a reader must act on; leave the rest plain. + +Entries land under `## [Unreleased]`. The maintainer assigns the version when +cutting a release, so nothing here predicts one. A released heading is +`## [0.0.0-alpha.N] — YYYY-MM-DD`, and every bracketed heading gets a matching +reference-link definition at the bottom of the file — a bracket with no +definition renders as literal text. + +Do not sell a fix. "No longer truncates panes wider than 500 columns", not +"improves capture reliability". Do not describe effort. Give the old behaviour +only where it explains a break. + +State a changed default explicitly, and an incompatibility more explicitly +still, with the way forward in the same bullet. For a break worth spelling +out, use the four-part frame: + +```markdown +**`Server.KillAsync` now throws instead of returning `false`.** + +- Previous behaviour: returned `false` when no server was running. +- New behaviour: throws `TmuxNotRunningException`. +- Reason: the silent `false` masked a misconfigured socket path. +- Recommended action: check `Server.IsRunning`, or catch the exception. +``` + +## Release notes + +The changelog is archival; a release page answers why anyone should care about +*this* release. It is the changelog plus prioritization. + +Lead with one paragraph: what is shipping, who should care, and whether +upgrading is safe. Then highlights, then breaking changes, then the full list. +Do not pretend every patch is a product launch — for a patch, one paragraph +and a `Fixed` list is the whole thing. + +Every claim carries evidence. "Serialization allocates one fewer intermediate +buffer per command" is a claim; "significantly faster" is not. A performance +number names the benchmark, the tmux version, the host, and the date, and is +stated as a ratio or a marginal cost, because absolute milliseconds move by a +factor of five between machines. + +This project has published no release pages yet, so this section is a bar to +meet rather than a description of what exists. + +## 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 72 characters or fewer, excluding any trailing `(#NN)` +pull request reference; 50 or fewer is better and most of the history manages +it. Wrap body lines at 72. Separate the `why:` and `what:` blocks with a blank +line. No emoji, anywhere. + +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 +- **dotnet(deps)**: Dependencies +- **dotnet(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 property to SendKeysRequest +- Pass -l when it is set +``` + +The body explains why this implementation exists. The diff already says which +statements changed, so a body that restates the diff carries nothing. + +Conventional Commits (`feat:`, `fix!:`, `BREAKING CHANGE:` footers) are +deliberately not used here. The format above predates them in this repository, +every commit follows it, and no tooling consumes the Conventional form. Do not +introduce it. + +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 +)" +``` + +### Release commits + +Never create tags. Never push tags. The owner handles tagging and tag pushes, +because a tag matching `v*` 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 + +Every public member carries XML documentation. `CS1591` is unsuppressed in all +four shipped projects and `TreatWarningsAsErrors` is on, so a missing comment +is a build error rather than a warning. + +`` is one sentence on one line. Start with a verb: `Gets` for a +property, an active present-tense verb for a method — `Reads`, `Runs`, +`Returns`, `Creates`, `Sends` — and `Represents`, `Provides`, or `Describes` +for a type. + +```csharp +/// Reads one option. +``` + +Do not restate the identifier. "Gets the timeout" says nothing the signature +did not; "Gets the maximum time allowed for each attempt before it is +canceled" says what the type cannot. + +`` carries everything longer, in `` blocks. It is a separate +tag, never folded into ``. This is where the facts that make a +library trustworthy live — thread safety, ownership and disposal, cancellation +semantics, ordering, lifetime, and the tmux quirk behind a design: + +```csharp +/// +/// tmux answers commands in the order it received them, so this is safe to +/// call concurrently: each caller gets its own answer rather than someone +/// else's. Cancelling stops the wait, not the command; tmux has already +/// been told. +/// +``` + +`` names the condition, not the type. "More than one of direction, +explicit size, and mode is set" is useful; "Thrown when the request is +invalid" is not. Document every exception a caller can reasonably hit. + +Use the semantic tags rather than formatting by hand: ``, +``, `null`, ``. They flow through +IntelliSense and the generated reference; hand-formatting does not. + +`` is bare, with no `cref`, and only where inheritance genuinely +means identical semantics — `Equals`, `GetHashCode`, `Dispose`, an interface +implementation. Do not use it to avoid documenting a subtly different override. + +`` and `` appear exactly once in this codebase, on +`LibTmuxException`, where the catch pattern is non-obvious enough to earn a +runnable snippet. That is the bar. Do not add them by default — a summary and +remarks that state the contract are what this library relies on, and README +and `docs/` carry the worked examples. + +## 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 + +Two or three lines is the working norm here, and four is the ceiling. 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): + +```csharp +// There are 321 tests to complete for servers. +``` + +Good (Keep): + +```csharp +// tmux < 3.2 reports the pane ID only after the command completes, +// so this query must stay separate. +``` + +### Documentation exception + +Minimal usage examples, and ``, ``, and `` 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. + +## Terminology and capitalization + +`tmux` is always lowercase, including at the start of a sentence. Recast +nothing to avoid it. + +`libtmux` names the product, the GitHub organisation, the command-line tool, +and the Python library this one ports. `LibTmux` names the C# package, +namespace, and type. The two are never swapped. + +One noun per concept, everywhere: server, session, window, pane, client, +option, hook, buffer. If it is a pane, it is not also a view, a region, or a +split. Synonym rotation costs a reader precision and costs grep, search, and +an agent a match. + +## Markdown + +Wrap prose at 80 columns. Badges, table rows, fenced code, and a line +dominated by a single URL are exempt — breaking those hurts more than the +column costs. + +No GitHub alert blocks. `> [!NOTE]`, `> [!WARNING]` and the rest render as +literal text everywhere except GitHub, and this project's README is rendered +by nuget.org, which is where most readers meet it. A plain blockquote, or just +a sentence, renders everywhere. + +Tables, badges, and links are fine. + +Never wrap a pull request or issue body. GitHub renders a single newline as a +space inside a file and as a line break inside a comment, so a hand-wrapped +comment arrives as ragged stubs. + +## Code blocks + +Code blocks are paste-and-run units: pasting one block runs exactly one +intended action. Executed examples are exempt — the test suite runs them, +nobody pastes them. + +- **One command per block.** Multiple steps may share a block only when + explicitly chained with `&&`, `;`, or `\` continuations — the chain is then + one logical command. +- **Explanations go in prose above the block**, never as `#` comments inside + it. +- **Command menus are per-command blocks with prose lead-ins**, not tables. +- **Shell commands use the `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 +``` + +### C# blocks are tests + +Every C# block in a shipped document is compiled against the real assemblies, +and a block tagged ```` ```csharp run ```` is additionally executed against a +tmux server of its own. `ReadmeExampleTests` does both, across the root README, +each package README, and `docs/modes/`. An example is either true or a failing +test, which is what stops one that cannot work from being rendered on a package +page. + +So tag a block `csharp run` when it is meant to execute, and leave it plain +`csharp` when it only illustrates. A plain block still has to compile. + +Write examples accordingly. The harness supplies a preamble and hoists type +declarations, but everything else has to be real: no `// setup omitted`, no +method that does not exist, no magic constant the reader cannot resolve. + +Decision records under `docs/decisions/` are deliberately outside this. They +quote what was run at the time, and an example edited later to keep compiling +records nothing. + +### Anchored snippets + +Anchoring is a second guarantee on top of compilation, and it catches a +different failure: prose drifting from the code it was copied from. + +A block wrapped in `` and `` is +materialized from a `#region` of the same name in +`examples/LibTmux.Examples/Snippets/`, so the document holds a verbatim copy of +code that runs. The copy is materialized rather than transcluded because these +are package READMEs and nuget.org renders the Markdown it is given without +resolving anything. + +The loop runs one way: edit the example, then bring the copy across. Never edit +the block in the document. + +```console +$ uv run python eng/docs/sync_snippets.py +``` + +Prefer anchoring for an example a reader is likely to copy verbatim. A block +that only demonstrates a call shape does not need it. + +[`examples/README.md`](../examples/README.md) has the mechanism in full — the +region markers, the `usings:` option, what each check fails on, and how to add +an example. + +## Error messages + +An exception message is a complete sentence: capitalized, period-terminated, +and naming the offending value. Where the caller could plausibly have gotten +it right, say what would have worked. + +```csharp +throw new McpException($"No session '{trimmed}' exists. These do: {known}."); +``` + +Make failure modes searchable. "`ConnectAsync` throws `TmuxNotRunningException` +when no server is listening on the socket" lets a reader find the explanation +from either the method or the exception; "connection can fail" does not. + +## Slop prevention + +Treat AI slop as review-hostile noise, not as proof that text or code is +wrong. The goal is to maximise information density. + +- **AI signatures.** No "Generated by", no conversational filler, 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, XML documentation, + README, or a pull request description. +- **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. +- **Prose inflation.** The diction table under [Voice](#voice) governs. +- **Coded labels.** Write rules and findings as plain imperatives. No `[R1]`, + `Option B`, or any index a reader has to decode. + +Preserve the why. Never delete a comment documenting an invariant, a protocol +constraint, a platform quirk, or an upstream workaround — those are the facts +[Source comments](#source-comments) keeps, and every other comment is judged +by it. From d08fe54f44795b77003d6fe49ac04f1d44756604 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:12:22 -0500 Subject: [PATCH 2/7] Docs(feat[contributing]): Absorb the gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: AGENTS.md and CONTRIBUTING.md both described the gates, and each held material the other lacked — the socket root, NU1004 and the macOS lane only in one, what a change must carry only in the other. Two partial copies is worse than one, because a contributor cannot tell which is short. The file moves under .github/ so one location answers for it. GitHub reads a contributing guide from there, so the new-issue and new-pull-request pages are unaffected. what: - Take the socket root rule, the mise-pinned toolchain, the gate job, the pack-before-integration order, NU1004, the macOS wrapped-line finding, the MCP swap, and the evidence recording boundary - Correct the toolchain claim: there is no mise.toml, and CI installs the SDK with setup-dotnet rather than going through mise - List all five document validators; AGENTS.md named only two - Point at examples/README.md for the example mechanism, and name the sync step that brings a changed example across - Add Review, Releases and Compatibility, which nothing stated before - Point at WRITING.md for prose and commit format instead of restating --- .github/CONTRIBUTING.md | 419 ++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 91 --------- 2 files changed, 419 insertions(+), 91 deletions(-) create mode 100644 .github/CONTRIBUTING.md delete mode 100644 CONTRIBUTING.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..94b21f7 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,419 @@ +# Contributing + +Thanks for looking. This repository is `libtmux` for .NET: a typed, +asynchronous client for tmux, plus a query layer, a workspace builder, and an +MCP server. The gates below are what a change has to pass. + +This file is how we work. For how we write — README prose, `CHANGELOG.md`, +release notes, commit messages, XML documentation, source comments, and error +messages — follow [WRITING.md](WRITING.md). Read it before changing any of +them. + +## Getting set up + +`dotnet` is pinned to `10.0.302` by [`global.json`](../global.json) with +`rollForward: disable`, and again by `.tool-versions`. It resolves through +[mise](https://mise.jdx.dev) locally, so it is not on `PATH` — every command +below is prefixed accordingly: + +```console +$ mise exec -- dotnet build LibTmux.slnx --configuration Release --warnaserror +``` + +CI does not use mise. It installs the same SDK with `actions/setup-dotnet` +from `global-json-file` and calls `dotnet` directly, so a workflow file never +carries the prefix. + +The validators are Python and run through [uv](https://docs.astral.sh/uv/). +There is no Python project file — each script carries its own PEP 723 header. + +You also need a real `tmux`, version 3.2a or newer. The suite drives one +rather than mocking it, because this library's job is being right about tmux +and only tmux can say whether it is. + +## Own your tmux socket root + +Give this repository a socket root of its own before running anything: + +- Tests: `TMUX_TMPDIR=/tmp/libtmux-dotnet-test` +- Servers you start by hand: `TMUX_TMPDIR=/tmp/libtmux-dotnet-dev` + +This matters more than it looks. Several libtmux ports live on this machine +and run real tmux at the same time. A socket in the default root is reachable +by all of them, so one port's cleanup sweep kills another port's servers +mid-run — and the failure surfaces in whichever suite noticed first, which is +rarely the one that caused it. That misattribution is what turns socket +sharing into a debugging loop. + +tmux reads `TMUX_TMPDIR` when it execs and puts a `-L name` socket in +`$TMUX_TMPDIR/tmux-$UID/name`, so exporting it before the run is enough. A +`-S path` socket ignores it and needs a path under the root instead. + +Two things are never safe here, because the processes and directories belong +to other workspaces: + +- `pkill tmux`, or any kill by a pattern matching more than your own root +- deleting `/tmp/tmux-$UID/` or another port's root + +To find what this repository left behind, list its root rather than matching +process names: + +```console +$ ls /tmp/libtmux-dotnet-test/tmux-$(id -u) +``` + +A socket file outlives the server that made it, so read that listing as +candidates and confirm each with `has-session` before deciding it is alive. + +## Building + +Restore against the lock files, check formatting, then build with warnings as +errors: + +```console +$ mise exec -- dotnet restore LibTmux.slnx --locked-mode +``` + +```console +$ mise exec -- dotnet format LibTmux.slnx --verify-no-changes --no-restore +``` + +```console +$ mise exec -- dotnet build \ + LibTmux.slnx \ + --configuration Release \ + --no-restore \ + --warnaserror +``` + +The build is the style guide. `TreatWarningsAsErrors`, `Nullable`, +`EnforceCodeStyleInBuild`, and analyzers at `10-recommended` are set +repository-wide, and `CS1591` is unsuppressed in every shipped project. If it +compiles clean, the formatting is right and every public member is documented. + +## Running the tests + +Unit tests run on both target frameworks: + +```console +$ mise exec -- dotnet test \ + --project tests/LibTmux.UnitTests/LibTmux.UnitTests.csproj \ + --configuration Release \ + --framework net8.0 \ + --no-build \ + --minimum-expected-tests 1 +``` + +The integration suite drives a real tmux and needs the socket root above: + +```console +$ mise exec -- dotnet test \ + --project tests/LibTmux.IntegrationTests/LibTmux.IntegrationTests.csproj \ + --configuration Release \ + --framework net10.0 \ + --no-build \ + --minimum-expected-tests 1 +``` + +## Checks that must pass + +[`.github/workflows/dotnet.yml`](workflows/dotnet.yml) is the source of truth, +and its `gate` job is the single name branch protection requires. `gate` needs +`build` and nothing else, so adding a required job means adding it to `gate`'s +`needs` rather than to a protection rule. + +### Order matters before the integration suite + +The packaging tests inside the integration suite read what a pack produced, so +they fail on a tree nobody packed. Run the workflow's order — pack, then the +package consumer, then the ahead-of-time publish — or expect +`PackageClosureTests` to fail for a reason that is not a bug: + +```console +$ mise exec -- dotnet pack \ + LibTmux.slnx \ + --configuration Release \ + --no-build \ + --output artifacts/packages +``` + +```console +$ uv run python eng/parity/inspect_packages.py +``` + +```console +$ mise exec -- dotnet run \ + --project tests/LibTmux.PackageConsumer/LibTmux.PackageConsumer.csproj \ + --configuration Release \ + --framework net8.0 \ + --no-restore +``` + +```console +$ mise exec -- dotnet publish \ + tests/LibTmux.AotSmoke/LibTmux.AotSmoke.csproj \ + --configuration Release \ + --framework net10.0 \ + --runtime linux-x64 +``` + +`LibTmux.PackageConsumer` is deliberately absent from `LibTmux.slnx`. It exists +to prove the packaged artifact rather than a project reference, so it restores +and runs standalone. + +### Validators that read documents, not the build + +Five checks run against documents rather than code, which is what makes them +easy to forget locally: + +```console +$ uv run python eng/parity/verify_public_api.py +``` + +```console +$ uv run python eng/parity/verify_capabilities.py +``` + +```console +$ uv run python eng/parity/verify_workflows.py +``` + +```console +$ uv run python eng/docs/sync_snippets.py --check +``` + +```console +$ uv run eng/mcp/dump_tools.py --check +``` + +`sync_snippets.py --check` is the one that catches a hand-edited example. It +compares each published block against the region it was quoted from and fails +on any difference, so bring a change across rather than typing it into the +document: + +```console +$ uv run python eng/docs/sync_snippets.py +``` + +The examples themselves are checked by the test suites rather than by a script. +`ReadmeExampleTests`, inside the integration suite, compiles every C# block in +the shipped documents and runs the ones tagged `csharp run`; +`SnippetContractTests`, inside the example suite, holds every published region +to an example that runs. [`examples/README.md`](../examples/README.md) has the +whole mechanism. + +The engineering scripts have tests of their own: + +```console +$ uv run --with pytest --with tomlkit python -m pytest eng --quiet +``` + +### NU1004, which is not a dependency problem + +Publishing ahead of time names a runtime identifier, and restore then writes +one into the lock file of every project in that graph — including the +library's, where the section is empty because no package resolves differently. +That is why `src/LibTmux` and `src/LibTmux.Generators` declare +`RuntimeIdentifiers`: without it the lock files disagree with the projects and +the *next* `restore --locked-mode` fails with NU1004, which reads like a +dependency problem and is not one. + +Adding a platform to the matrix means adding its identifier there and +regenerating: + +```console +$ mise exec -- dotnet restore LibTmux.slnx --force-evaluate +``` + +### The other workflows + +[`dotnet-tmux.yml`](workflows/dotnet-tmux.yml) builds each supported tmux from +source and runs the integration suite against it, behind a `compatibility` job +that plays the same role as `gate`. That is what proves the compatibility +range; the build workflow only ever sees whatever tmux Ubuntu ships. + +`dotnet.yml` also carries an advisory `macos arm64` lane, because the +compatibility claim names macOS and a claim nobody runs is a claim. It runs +`continue-on-error` and is deliberately outside `gate`'s `needs`, so a platform +difference cannot block every commit. It restores without `--locked-mode`, +because the lock files are generated for the Linux runtime identifiers this +repository publishes. + +Every failure that lane has produced was a difference in what the platform put +on the screen rather than in what tmux did. The last two were a runner hostname +61 characters long: bash's prompt then fills 78 of the pane's 80 columns, and +tmux stores the wrap as a real line break, so a capture that does not ask for +`-J` returns typed text split across two lines. **Assertions about text a user +typed capture with `joinWrappedLines`.** + +`codeql.yml` and `scorecard.yml` run on a schedule rather than on the gate, +because what they check can change without a commit. Every action reference is +pinned to a commit SHA with the version in a trailing comment, which is what +stops a moved tag from changing what CI runs. Dependabot maintains those pins. + +## Testing the MCP server means running a real agent + +`src/LibTmux.Mcp` is a stdio server, so the only honest test of its tool +descriptions is whether a model picks the right tool without being told which. +`eng/mcp/mcp_swap.py` points every installed agent CLI at a local build, and +`revert` puts their configs back from the timestamped backup it took: + +```console +$ uv run eng/mcp/mcp_swap.py use \ + --source release \ + --env TMUX_TMPDIR=/tmp/libtmux-dotnet-dev +``` + +Pass `--env TMUX_TMPDIR=...` whenever the sockets under test are not in the +default root. An agent spawns the server with its own environment, so a socket +this shell can see is one the server cannot. + +That same gap is why the swap writes `DOTNET_ROOT` into each config. A +framework-dependent apphost finds its runtime through `DOTNET_ROOT` or `PATH`, +mise puts the SDK in neither, and the failure is silent from the agent's side: +the binary exits before the handshake and the agent reports only that the +server has no tools. + +The tool reference is generated rather than written, so it cannot describe a +surface that is not there: + +```console +$ uv run eng/mcp/dump_tools.py +``` + +A wait takes a control-mode client, which is a real attached client: it shows +up in the user's `list-clients` for as long as the wait runs. It attaches with +`ignore-size` so it never drags the window down to its own size, and the watch +is reference counted per session so it exists only while a wait does. Changing +either of those changes what a user sees on their own screen. + +## What a change is expected to carry + +**A behaviour change needs a test against a real tmux.** This library's job is +being right about tmux, and only tmux can say whether it is. + +**A version-dependent behaviour needs a row in the ledger.** Anything that +differs between 3.2a and 3.7b goes through the capability model, and each +difference names the test that proves it in +[`docs/parity/version-deltas.json`](../docs/parity/version-deltas.json). + +**A public API addition needs five edits, and each will tell you.** The Roslyn +analyzer baseline (`PublicAPI.Unshipped.txt`), the type and its members in +`docs/public-api.json`, its values if it is an enum, and its owning component +in `eng/parity/verify_production_plan.py`. They fail independently and by name; +follow the errors. + +**A documented example is compiled, and a `csharp run` block is executed +against a live tmux.** `ReadmeExampleTests` compiles every C# block in the +shipped READMEs and `docs/modes/` against the real assemblies, and runs the +ones tagged to run against a tmux server of their own. If it does not compile, +it is not documentation. Anchoring a block to a snippet region adds drift +protection on top of that; [`examples/README.md`](../examples/README.md) says +how. Add examples to the READMEs or `docs/modes/`, not to the decision records +— those quote what was run at the time and are not edited to keep compiling. + +**A performance claim needs a recorded run.** See +[`docs/benchmarks`](../docs/benchmarks/README.md). Absolute milliseconds move +by a factor of five on one host, so a claim is stated as a marginal cost or a +ratio, with the tmux, host and date that produced it. + +## The Python original is a separate checkout + +This repository was imported out of a monorepo that also held Python libtmux, +so anything grounded in that source needs to be told where it went: + +```console +$ LIBTMUX_PYTHON_REPOSITORY=~/work/python/libtmux \ + uv run python eng/parity/verify_ledger.py +``` + +## Flaky, or broken? + +Some real-tmux tests are load-sensitive. A single failure is worth re-running +in isolation before blaming a change, and worth investigating rather than +shrugging at. Three signs it is the machine and not the code: the failing test +moves between runs, the failure reads as a missing server or an expired wait +rather than a wrong value, and the file it is in is not one the change touched. + +## Pull requests + +Keep the change narrowly scoped. Unrelated cleanup belongs in its own commit, +or its own pull request. + +A passing gate is evidence only once it has been shown capable of failing, so +pair a new test with a deliberate break that proves it bites. + +Commit format is in [WRITING.md](WRITING.md). + +## Review + +A reviewer is checking two things beyond correctness: that the change carries +what the section above requires, and that anything a reader will see follows +[WRITING.md](WRITING.md). A comment that restates its code, a changelog entry +that describes effort rather than impact, or a public member without a +`` are all review findings, not nits. + +## Releases + +Releases are cut by the owner, in two commits. + +First, `chore(release[version]): Bump to 0.0.0-alpha.N` edits +[`Directory.Build.props`](../Directory.Build.props) — `VersionSuffix` and +`PackageReleaseNotes` — and renames `## [Unreleased]` in `CHANGELOG.md` to the +dated version heading. Versioning is manual `VersionPrefix`/`VersionSuffix`, +and one string covers all four shipped packages. + +Second, a `Tag v0.0.0-alpha.N` commit, then the tag itself. + +**Never create tags. Never push tags.** A tag matching `v*` triggers +[`release.yml`](workflows/release.yml), which verifies the tag matches the +built `Version` property, packs, proves the package installs and runs on both +target frameworks, generates an SBOM and a provenance attestation, and pushes +to NuGet through trusted publishing. Renaming that workflow file breaks the +trusted-publishing policy registered on nuget.org, so change the policy first. + +### Recorded evidence is a release artifact + +A capability row is `pending` until a matrix run records evidence for it, and +`verified` after. What `verified` claims is exact: these tmux versions, on +these frameworks, at *this tree* — the fingerprint covers every tracked file +outside the evidence directory. Any commit changes it, so a verified row is +true at one commit and stale at the next. + +That is why recording belongs at a release boundary rather than in the gate, +and why `reconcile_versions.py` and `verify_ledger.py` are not in +`dotnet.yml`. Between releases every row is `pending`, which is the honest +state: nobody has run the matrix against this tree. + +To record, on the commit being released: + +```console +$ eng/tmux/run-matrix.sh \ + --evidence-dir docs/parity/evidence/0001 \ + --capability-cohort 0001 \ + tests/LibTmux.IntegrationTests/LibTmux.IntegrationTests.csproj +``` + +```console +$ uv run python eng/parity/reconcile_versions.py \ + --evidence docs/parity/evidence/0001/results.ndjson \ + --write +``` + +Commit the bundle and the rewritten `version-deltas.json` together, because the +fingerprint is of the tree that commit produces. A tmux build takes about forty +seconds here and the matrix runs the suite fourteen times, so budget half an +hour. + +## Compatibility + +tmux **3.2a through 3.7b**, on **net8.0** and **net10.0**, on Linux and macOS. +Windows is unsupported. The packages are trim- and ahead-of-time-safe. + +During alpha the public API can change in any release with no deprecation +period, so a consumer pins an exact version. Widening the supported range means +a row in the ledger, an entry in the tmux matrix, and a README that says so. + +## Reporting a vulnerability + +Not here — see [SECURITY.md](../SECURITY.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 0535647..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,91 +0,0 @@ -# Contributing - -## What you need - -`dotnet` is pinned by `global.json` and resolves through [mise](https://mise.jdx.dev), -so it is not on `PATH`: - -```console -$ mise exec -- dotnet build LibTmux.slnx --configuration Release --warnaserror -``` - -The validators are Python and run through [uv](https://docs.astral.sh/uv/). You -also need a real `tmux` — the suite drives one rather than mocking it. - -## Give your tmux a socket root of its own - -Tests set `TMUX_TMPDIR=/tmp/libtmux-dotnet-test` for themselves. If you start a -server by hand, put it under `/tmp/libtmux-dotnet-dev`. - -This matters more than it looks. Several libtmux ports run real tmux on one -machine, and a socket in the default root is reachable by all of them, so a -cleanup sweep can kill another project's servers mid-run — and the failure -surfaces in whichever suite noticed, not the one that caused it. - -Never `pkill tmux`, and never delete `/tmp/tmux-$UID/`. To find what this -repository left behind, list its own root: - -```console -$ ls /tmp/libtmux-dotnet-test/tmux-$(id -u) -``` - -A socket file outlives the server that made it, so confirm each with -`has-session` before deciding it is alive. - -## Running what CI runs - -`.github/workflows/dotnet.yml` is the source of truth. Two things are easy to -miss locally, because they check documents rather than the build: - -```console -$ uv run python eng/parity/verify_public_api.py -``` - -```console -$ uv run python eng/parity/verify_capabilities.py -``` - -The packaging tests read what a pack produced, so run the workflow's order — -pack, then the package consumer, then the ahead-of-time publish — before the -integration suite, or `PackageClosureTests` fails for a reason that is not a bug. - -## What a change is expected to carry - -**A behaviour change needs a test against a real tmux.** This library's job is -being right about tmux, and only tmux can say whether it is. - -**A version-dependent behaviour needs a row in the ledger.** Anything that -differs between 3.2a and 3.7b goes through the capability model, and each -difference names the test that proves it in -[`docs/parity/version-deltas.json`](docs/parity/version-deltas.json). - -**A public API addition needs five edits, and each will tell you.** The Roslyn -analyzer baseline (`PublicAPI.Unshipped.txt`), the type and its members in -`docs/public-api.json`, its values if it is an enum, and its owning component in -`eng/parity/verify_production_plan.py`. They fail independently and by name; -follow the errors. - -**A documented example is compiled, and a `csharp run` block is executed against -a live tmux.** If it does not compile, it is not documentation. Add examples to -the READMEs or `docs/modes/`, not to the decision records — those quote what was -run at the time and are not edited to keep compiling. - -**A performance claim needs a recorded run.** See -[`docs/benchmarks`](docs/benchmarks/README.md). Absolute milliseconds move by a -factor of five on one host, so a claim is stated as a marginal cost or a ratio, -with the tmux, host and date that produced it. - -## Style - -The build is the style guide: `TreatWarningsAsErrors`, `Nullable` enabled, -analyzers at `10-recommended`, and `EnforceCodeStyleInBuild`. If it compiles -clean, the formatting is right. - -Comments explain *why*, not *what*. The surrounding code is the reference for -density and idiom. - -Commit messages say what changed and why it was worth changing. No emojis. - -## Reporting a vulnerability - -Not here — see [SECURITY.md](SECURITY.md). From 0fe903d2f6b5b6872a257aa5a0c8d832f749cc70 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:12:39 -0500 Subject: [PATCH 3/7] Docs(refactor[agents]): Route instead of restate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: CLAUDE.md symlinks here, so all 364 lines loaded on every task in the repository — the MCP swap gotchas and the comment policy included, whichever file was being touched. A root instruction file should answer which policy applies to the thing being changed, not hold every policy itself. what: - Cut to change discipline and a Which policy applies list - Phrase each route as a condition on what is being changed rather than an unconditional instruction to go read a file - State that each listed file is the single home for its subject, replacing the old arrangement where two files answered the same question at different lengths - Leave nothing here that WRITING.md or CONTRIBUTING.md now carries --- AGENTS.md | 392 +++++------------------------------------------------- 1 file changed, 36 insertions(+), 356 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cdbb693..4bf1226 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,356 +1,36 @@ -# AGENTS.md - -Guidance for AI agents working on LibTmux, a .NET client for tmux. - -## Own your tmux sockets - -Several libtmux ports live on this machine and run real tmux at the same time. -A socket in the default root is reachable by all of them, so one port's cleanup -sweep kills another port's servers mid-run — and the failure surfaces in -whichever suite noticed first, which is rarely the one that caused it. That -misattribution is what turns socket sharing into a debugging loop. - -Give this repository a socket root of its own, named for the port and what it -is for: - -- Tests: `TMUX_TMPDIR=/tmp/libtmux-dotnet-test` -- Servers you start by hand: `TMUX_TMPDIR=/tmp/libtmux-dotnet-dev` - -tmux reads `TMUX_TMPDIR` when it execs and puts a `-L name` socket in -`$TMUX_TMPDIR/tmux-$UID/name`, so exporting it before the run is enough. A -`-S path` socket ignores it, and needs a path under the root instead. - -Two things are never safe here, because the processes and directories belong to -other workspaces: - -- `pkill tmux`, or any kill by a pattern matching more than your own root -- deleting `/tmp/tmux-$UID/` or another port's root - -To find what this repository left behind, list its root rather than matching -process names. A socket file outlives the server that made it, so read the -listing as candidates and confirm each with `has-session`: - -```console -$ ls /tmp/libtmux-dotnet-test/tmux-$(id -u) -``` - -## The toolchain is not on `PATH` - -`dotnet` is pinned by `global.json` and resolves through mise: - -```console -$ mise exec -- dotnet build LibTmux.slnx --configuration Release --warnaserror -``` - -## What gates this repository - -`.github/workflows/dotnet.yml` is the source of truth, and its `gate` job is -the single name branch protection requires — adding a job means adding it to -`gate`'s `needs`, not to a protection rule. Beyond building and `dotnet test`, -two validators run on documents rather than the build, and are easy to forget -locally: - -```console -$ uv run python eng/parity/verify_public_api.py -``` - -```console -$ uv run python eng/parity/verify_capabilities.py -``` - -The packaging tests inside the integration suite read what a pack produced, so -they fail on a tree nobody packed. Run the build workflow's order — pack, then -the package consumer, then the ahead-of-time publish — before the integration -suite, or expect `PackageClosureTests` to fail for a reason that is not a bug. - -Publishing ahead of time names a runtime identifier, and restore then writes -one into the lock file of every project in that graph — including the library's, -where the section is empty because no package resolves differently. That is why -`src/LibTmux` and `src/LibTmux.Generators` declare `RuntimeIdentifiers`: without -it, the lock files disagree with the projects and the *next* -`restore --locked-mode` fails with NU1004, which reads like a dependency problem -and is not one. Adding a platform to the matrix means adding its identifier -there and regenerating: - -```console -$ mise exec -- dotnet restore LibTmux.slnx --force-evaluate -``` - -`.github/workflows/dotnet-tmux.yml` builds each supported tmux from source and -runs the integration suite against it, behind a `compatibility` job that plays -the same role as `gate`. That is what proves the compatibility range; the build -workflow only ever sees whatever tmux Ubuntu ships. - -`dotnet.yml` also carries an advisory `macos arm64` lane, because the -compatibility claim names macOS and a claim nobody runs is a claim. Its first -run failed 15 of 854 integration tests; the last of those is fixed and the lane -is green, so what keeps it outside `gate` is now a choice rather than an -outstanding diagnosis. It restores without `--locked-mode`: the lock files are -generated for the Linux runtime identifiers this repository publishes, so -locking a macOS restore would fail for a reason that is not a dependency -problem. - -Every one of those failures was a difference in what the platform put on the -screen rather than in what tmux did. The last two were a runner hostname 61 -characters long: bash's prompt then fills 78 of the pane's 80 columns, and tmux -stores the wrap as a real line break, so a capture that does not ask for -`-J` returns typed text split across two lines. Assertions about text a user -typed capture with `joinWrappedLines`. - -Two more workflows run on a schedule rather than on the gate, because what they -check can change without a commit: `codeql.yml` analyses the build, and -`scorecard.yml` scores the repository's supply chain. Every action reference in -this repository is pinned to a commit SHA with the version in a trailing -comment, which is what stops a moved tag from changing what CI runs. Dependabot -maintains those pins; a pin nobody updates is just a stale action. - -## Testing the MCP server means running a real agent - -`src/LibTmux.Mcp` is a stdio server, so the only honest test of its tool -descriptions is whether a model picks the right tool without being told which. -`eng/mcp/mcp_swap.py` points every installed agent CLI at a local build and -`revert` puts their configs back from the timestamped backup it took: - -```console -$ uv run eng/mcp/mcp_swap.py use --source release --env TMUX_TMPDIR=/tmp/libtmux-dotnet-dev -``` - -Pass `--env TMUX_TMPDIR=...` whenever the sockets under test are not in the -default root. An agent spawns the server with its own environment, so a socket -this shell can see is one the server cannot. - -That same gap is why the swap writes `DOTNET_ROOT` into each config. A -framework-dependent apphost finds its runtime through `DOTNET_ROOT` or `PATH`, -mise puts the SDK in neither, and the failure is silent from the agent's side: -the binary exits before the handshake and the agent reports only that the -server has no tools. - -The tool reference is generated rather than written, so it cannot describe a -surface that is not there. Regenerate it whenever the tool surface changes: - -```console -$ uv run eng/mcp/dump_tools.py -``` - -A wait takes a control-mode client, which is a real attached client: it shows -up in the user's `list-clients` for as long as the wait runs. It attaches with -`ignore-size` so it never drags the window down to its own size, and the watch -is reference counted per session so it exists only while a wait does. Changing -either of those changes what a user sees on their own screen. - -## The Python original is a separate checkout - -This repository was imported out of a monorepo that also held Python libtmux, -so anything grounded in that source needs to be told where it went now: - -```console -$ LIBTMUX_PYTHON_REPOSITORY=~/work/python/libtmux uv run python eng/parity/verify_ledger.py -``` - -## Recorded evidence is a release artifact - -A capability row is `pending` until a matrix run records evidence for it, and -`verified` after. What `verified` claims is exact: these tmux versions, on these -frameworks, at *this tree* — the fingerprint covers every tracked file outside -the evidence directory. Any commit changes it, so a verified row is true at one -commit and stale at the next. - -That is why recording belongs at a release boundary rather than in the gate, -and why `reconcile_versions.py` and `verify_ledger.py` are not in -`.github/workflows/dotnet.yml`. Between releases every row is `pending`, which -is the honest state: nobody has run the matrix against this tree. - -To record, on the commit being released: - -```console -$ eng/tmux/run-matrix.sh --evidence-dir docs/parity/evidence/0001 --capability-cohort 0001 tests/LibTmux.IntegrationTests/LibTmux.IntegrationTests.csproj -``` - -```console -$ uv run python eng/parity/reconcile_versions.py --evidence docs/parity/evidence/0001/results.ndjson --write -``` - -Then commit the bundle and the rewritten `version-deltas.json` together, because -the fingerprint is of the tree that commit produces. A tmux build takes about -forty seconds here and the matrix runs the suite fourteen times, so budget half -an hour. - -## 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): - -```csharp -// There are 321 tests to complete for servers. -``` - -Good (Keep): - -```csharp -// 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. - -XML documentation — ``, ``, `` — falls under this -exception; `CS1591` is unsuppressed in the published projects. - -## 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 -- **dotnet(deps)**: Dependencies -- **dotnet(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 property to SendKeysOptions -- 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. + +## Change discipline + +These apply to every change, whatever it touches: + +- Make the smallest coherent change that solves the verified problem. Keep + unrelated cleanup out of it. +- Reuse an existing file, helper, API, or test before adding a new one. +- Keep a new type or member internal until a caller outside the assembly needs + it. A public surface is a promise, and this one is checked by five separate + gates. +- Add a file only for a durable boundary — a distinct responsibility or + independent reuse — not 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. + +## Which policy applies + +This file routes; it does not restate. Read the one that governs the change +being made: + +- For changes to documentation or user-facing prose — `README.md`, + `CHANGELOG.md`, release notes, commit messages, CLI and help text, error + messages, XML documentation, or source comments — follow + [`.github/WRITING.md`](.github/WRITING.md). +- For building, testing, the gates, pull requests, and releases, follow + [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md). +- For a security-sensitive change, or to report a vulnerability, follow + [`SECURITY.md`](SECURITY.md). + +Each is the single home for its subject. Where a rule appears to be stated +twice, the file listed above governs. From c21c0e1c61308dd4543c260bba3b7f36eb7bf556 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:12:59 -0500 Subject: [PATCH 4/7] Readme(docs): Title the port, state alpha terms why: The title named the package rather than the port, so a reader arriving from another language could not tell which of the family this is. The alpha warning also claimed more than the others do, and claimed it differently, which makes the family's terms look inconsistent when they are not. what: - Title the README "libtmux for .NET", matching every sibling port - State the alpha terms the same way across the ports: an -alpha prerelease tag, an unsettled API, no deprecation period, pin an exact version, not recommended for production - Drop the claim that behaviour is proven on every commit; the tmux matrix runs on its own workflow, not on each commit --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f7b8518..bd5448f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# LibTmux +# libtmux for .NET [![LibTmux](https://img.shields.io/nuget/vpre/LibTmux?logo=nuget&label=LibTmux)](https://www.nuget.org/packages/LibTmux) [![downloads](https://img.shields.io/nuget/dt/LibTmux?logo=nuget&label=downloads)](https://www.nuget.org/packages/LibTmux) @@ -10,10 +10,9 @@ Drive [tmux](https://github.com/tmux/tmux) from .NET. Servers, sessions, windows, panes, clients, options, hooks and buffers, typed and asynchronous, against every tmux from **3.2a to 3.7b** on **net8.0** and **net10.0**. -> **Alpha.** The public API is not settled and can change between prereleases -> without notice, so pin an exact version. The behaviour is proven against all -> seven supported tmux versions on every commit — what is unsettled is the shape -> of the API, not whether it works. +> **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. ```csharp From 2490101e58e2b01074e702c315a03a32f56c137e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:13:30 -0500 Subject: [PATCH 5/7] Readme(fix[install]): Use the noun-first verb why: .NET 10 added the noun-first CLI forms and Microsoft recommends them; the pinned SDK takes "dotnet package add" and its --prerelease flag. The old form still works, so this is about matching the toolchain the repository pins rather than fixing a break. what: - Rewrite the install command in the root README and in each package README to "dotnet package add" - Track the same change in the quality bar, which quotes the README - Leave "dotnet tool install" alone; it is already noun-first --- README.md | 2 +- docs/quality-bar.md | 2 +- src/LibTmux.Query.Json/README.md | 2 +- src/LibTmux.Workspace/README.md | 2 +- src/LibTmux/README.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bd5448f..f6573d1 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ documented examples that are executed against live tmux in CI. | **[LibTmux.Mcp](src/LibTmux.Mcp/README.md)** | [![v](https://img.shields.io/nuget/vpre/LibTmux.Mcp?logo=nuget&label=%20)](https://www.nuget.org/packages/LibTmux.Mcp) | You want an assistant driving tmux. Installs as a tool, not a reference. | ```console -$ dotnet add package LibTmux --prerelease +$ dotnet package add LibTmux --prerelease ``` The core takes exactly one dependency. Anything that would add another ships as diff --git a/docs/quality-bar.md b/docs/quality-bar.md index be60fe8..4409d51 100644 --- a/docs/quality-bar.md +++ b/docs/quality-bar.md @@ -16,7 +16,7 @@ and are not defects. Below 9 means something is wrong rather than absent. | Criterion | Evidence | |---|---| -| Install to working code in one screen | `dotnet add package LibTmux --prerelease`, then a seven-line example, both in the first 30 lines of the README | +| Install to working code in one screen | `dotnet package add LibTmux --prerelease`, then a seven-line example, both in the first 30 lines of the README | | One way to do a thing | Async only, no synchronous twins; every tmux-reaching call takes a `CancellationToken` | | The mode you are in is visible at the call | `session.CreateWindowAsync`, `server.EnterControlModeAsync`, `server.Chain()` — never a flag in options | | Failure says what to do next | `TmuxDispatchState` on every exception; the retry decision is an exception filter, [proved in tests](../tests/LibTmux.UnitTests/Exceptions/DispatchStateTests.cs) | diff --git a/src/LibTmux.Query.Json/README.md b/src/LibTmux.Query.Json/README.md index 293e413..f6ab916 100644 --- a/src/LibTmux.Query.Json/README.md +++ b/src/LibTmux.Query.Json/README.md @@ -8,7 +8,7 @@ want it does not get it. > without notice, so pin an exact version. ```console -$ dotnet add package LibTmux.Query.Json --prerelease +$ dotnet package add LibTmux.Query.Json --prerelease ``` ## When you want this diff --git a/src/LibTmux.Workspace/README.md b/src/LibTmux.Workspace/README.md index e4123fb..b34bf42 100644 --- a/src/LibTmux.Workspace/README.md +++ b/src/LibTmux.Workspace/README.md @@ -7,7 +7,7 @@ workspace files, on top of [LibTmux](https://www.nuget.org/packages/LibTmux). > without notice, so pin an exact version. ```console -$ dotnet add package LibTmux.Workspace --prerelease +$ dotnet package add LibTmux.Workspace --prerelease ``` Adds one dependency, [YamlDotNet](https://github.com/aaubry/YamlDotNet), which diff --git a/src/LibTmux/README.md b/src/LibTmux/README.md index a732a7e..0acfe48 100644 --- a/src/LibTmux/README.md +++ b/src/LibTmux/README.md @@ -9,7 +9,7 @@ every tmux from **3.2a to 3.7b**, on **net8.0** and **net10.0**. > seven supported tmux versions on every commit. ```console -$ dotnet add package LibTmux --prerelease +$ dotnet package add LibTmux --prerelease ``` One dependency: `Microsoft.Extensions.Logging.Abstractions`, which is From 27eec1bf5df0793779b20400da2c073780b1bf76 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:13:49 -0500 Subject: [PATCH 6/7] Changelog(fix[links]): Link the newer versions why: Every version heading is bracketed like a link, but only alpha.1 through alpha.4 had a reference definition. The three newest headings rendered as literal text with brackets around them, which reads as a broken link rather than a deliberate plain heading. what: - Define alpha.5, alpha.6 and alpha.7 against their release tags, all three of which exist --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc24e8..ef00066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -221,6 +221,9 @@ it is: a published version can never be deleted from nuget.org, only unlisted. - `LibTmux.Workspace` — sessions from tmuxp workspace files. - `LibTmux.Mcp` — a Model Context Protocol server, installed as a .NET tool. +[0.0.0-alpha.7]: https://github.com/libtmux/libtmux-dotnet/releases/tag/v0.0.0-alpha.7 +[0.0.0-alpha.6]: https://github.com/libtmux/libtmux-dotnet/releases/tag/v0.0.0-alpha.6 +[0.0.0-alpha.5]: https://github.com/libtmux/libtmux-dotnet/releases/tag/v0.0.0-alpha.5 [0.0.0-alpha.4]: https://github.com/libtmux/libtmux-dotnet/releases/tag/v0.0.0-alpha.4 [0.0.0-alpha.3]: https://github.com/libtmux/libtmux-dotnet/releases/tag/v0.0.0-alpha.3 [0.0.0-alpha.2]: https://github.com/libtmux/libtmux-dotnet/releases/tag/v0.0.0-alpha.2 From 34569efebf9cb09b09d471710517d3a9398e7952 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 07:47:47 -0500 Subject: [PATCH 7/7] Docs(fix[examples]): Say how examples are checked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: The README's C# is compiled and run because it is quoted from an example, and the only account of how that works was a docstring in sync_snippets.py and an XML comment on the test. Somebody adding an example had nowhere to learn the region markers, and somebody editing a published block learned about --check from CI failing on a rule no document stated. what: - Add examples/README.md, where somebody editing an example already is: what an [Example] is, the topic and region naming, the dedent, the usings option, and the nine documents that publish - Say the loop runs one way — edit the example, then sync; never edit the block — and that --check is what CI runs - Name both failure modes SnippetContractTests holds: a region no example runs, and an example the snippet reader cannot see - Separate the two guarantees: every C# block is compiled and a csharp run block is executed, while anchoring is what stops a published block drifting from its source - Link it from the README's documentation list --- README.md | 1 + examples/README.md | 112 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 examples/README.md diff --git a/README.md b/README.md index f6573d1..78aff57 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ and a tool above it never reaches the model's list. - [Public API](docs/public-api.md) — the reviewed, approved surface - [Version deltas](docs/parity/version-deltas.json) — every tmux difference, with its proof - [Decisions](docs/decisions/) — why the transport, object model and query catalog are shaped this way +- [Examples](examples/README.md) — every example here is a test, and the C# in this README is quoted from one - [AGENTS.md](AGENTS.md) — how to work in this repository ## Compatibility diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..102bfff --- /dev/null +++ b/examples/README.md @@ -0,0 +1,112 @@ +# Examples + +Every example here runs against a real tmux server of its own, as a test, on +every build. Nothing in this directory can quietly stop compiling or stop being +true — and because the READMEs quote from it, neither can they. + +Two mechanisms sit on top of that, and they catch different failures: + +| Check | Where | Fails when | +| --- | --- | --- | +| `ReadmeExampleTests` | `tests/LibTmux.IntegrationTests/Documentation/` | A C# block in a shipped document does not compile, or a `csharp run` block does not run | +| `sync_snippets.py` | `eng/docs/` | A published block has drifted from the region it was quoted from | +| `SnippetContractTests` | `tests/LibTmux.ExampleTests/` | A published region is not an example that runs, or an example is somewhere the snippet reader cannot see | + +## Writing an example + +An example is a method carrying `[Example]`, taking a `Server` and a +`CancellationToken`, and returning `Task`. It lives in a class under +[`LibTmux.Examples/Snippets/`](LibTmux.Examples/Snippets/) — one class per +topic, and the class name *is* the topic, so `Chaining.cs` holds the chaining +examples. + +```csharp +/// Runs three commands through a single tmux invocation. +[Example("Three commands, one process")] +public static async Task ManyCommandsOneProcess(Server server, CancellationToken ct) +{ + #region ManyCommandsOneProcess + await server.Chain() + .Then("new-window", "-d", "-n", "build") + .Then("new-window", "-d", "-n", "test") + .ExecuteAsync(ct); + #endregion +} +``` + +Run them all: + +```console +$ mise exec -- dotnet run \ + --project examples/LibTmux.Examples/LibTmux.Examples.csproj \ + --configuration Release +``` + +## Quoting an example in a document + +The `#region` name is what a document publishes, and it matches the method +name. Put an anchor pair where the block belongs and the region is copied +between them: + +```markdown + + +``` + +Then materialize it: + +```console +$ uv run python eng/docs/sync_snippets.py +``` + +The region is dedented before it is written, so code nested inside a method +lands at column zero in the document. A document that needs a `using` the +snippet file already has at file scope can ask for it, and the directive is +written above the block: + +```markdown + +``` + +Nine documents publish snippets: the root README, each of the four package +READMEs, and the four files under `docs/modes/`. Decision records are +deliberately excluded — they quote what was run at the time, and an example +edited later to keep compiling records nothing. + +### The loop runs one way + +Edit the example, run it, then bring the copy across. **Never edit the block in +the document** — `--check` compares the two and fails the build on any +difference, so a hand-edited block is reported as drift rather than kept: + +```console +$ uv run python eng/docs/sync_snippets.py --check +``` + +That is what CI runs. + +### Both failure modes + +`SnippetContractTests` fails a region that no `[Example]` method runs, because +a published block nobody executes is exactly the sample that rots. It also +fails an example that lives outside +[`LibTmux.Examples/Snippets/`](LibTmux.Examples/Snippets/), since that is the +one directory `sync_snippets.py` globs — an example anywhere else is invisible +to it and would never be published at all. + +## Blocks that are not snippets + +A C# block does not have to be a snippet. `ReadmeExampleTests` compiles every +`csharp` block in those same nine documents against the real assemblies, and +executes the ones tagged `csharp run` against a tmux server of their own: + +````markdown +```csharp run +Window built = await session.CreateWindowAsync(new NewWindowRequest(name: "build"), ct); +``` +```` + +Tag a block `csharp run` when it is meant to execute, and leave it plain +`csharp` when it only illustrates. A plain block still has to compile. The +harness supplies a preamble and hoists type declarations, so an example does +not repeat the setup — but everything else has to be real.