libtmux 0.1.0-alpha.1

Async typed tmux client and object model
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
# libtmux Rust crate design

## Outcome

`rs/` is a Cargo workspace whose members all live under `rs/crates/`:

| Crate            | Published | What it is                                    |
| ---------------- | --------- | --------------------------------------------- |
| `libtmux`        | yes       | The async tmux client and object model         |
| `libtmux-macros` | yes       | `#[derive(Filterable)]`, for downstream structs |
| `tmux-mcp`       | no        | A Model Context Protocol server over `libtmux` |
| `tmux-workspace` | no        | Builds tmux workspaces from tmuxp-style YAML   |

The two unpublished crates exist to exercise the public API from outside, the
way a real consumer would. The dependency runs one way: `libtmux` knows
nothing about either.

`libtmux` presents `Server`, `Session`, `Window`, `Pane`, and `Client`
handles, hierarchy listings, refresh, options, hooks, buffers, key bindings,
and the local typed filtering kernel, keeping Python libtmux's object
vocabulary and the same tmux floor of 3.2a. The format catalog, intrinsic
snapshots, and winlink projections back those handles and stay crate-private.
Optional features add control mode, a blocking runtime, portable expression
serialization, tracing, and the real-tmux test guard.

Cargo is authoritative for compilation, dependency resolution, testing,
documentation, and publishing. A `justfile` groups those commands so `just`
alone lists them; it adds no build step and does not participate in the
published crate.

The first transport uses Tokio subprocesses. Command, target, result,
capability, snapshot, and query values do not depend on that transport, so a
future control-mode or engine-ops crate can reuse them without changing normal
object APIs.

## Goals

- Reach behavioral feature parity with the pinned Python public API baseline.
- Keep familiar names and hierarchy while using Rust ownership, errors,
  builders, iterators, and async methods naturally.
- Preserve tmux 3.2a compatibility and gate newer format fields and flags by
  detected capabilities.
- Make IDs, targets, fields, option scopes, directions, flags, and query
  cardinality statically meaningful.
- Preserve the bytes emitted by tmux and expose decoding explicitly without
  assuming every tmux release emits untransformed callback bytes.
- Provide isolated real-tmux tests with explicit socket paths and fail-closed,
  platform-scoped cleanup.
- Leave a coherent extension seam for immutable plans, query lowering,
  control mode, serialization, and alternate executors.
- Keep the default crate small enough to audit and pleasant to embed.

## Non-goals

- A JavaScript, N-API, Python, WASM, or C binding.
- A second synchronous API.
- Control mode in the default transport. Normal commands stay one process per
  command whatever the optional `control-mode` feature does.
- A literal port of Python inheritance, arbitrary keyword arguments, mutable
  dictionaries, or exceptions.
- Compatibility methods whose only current behavior is raising
  `DeprecatedError`.
- Reproducing Python's `QueryList` type, its equality and lookup-suffix
  defects, stale fields, or cross-server identity defects.
- A process-global engine or operation registry.

## Compatibility contract

The target compatibility surface is tmux 3.2a and newer on Linux, macOS, and
WSL. Native Windows is unsupported because tmux is unavailable there. The
initial Rust MSRV is 1.85, the first stable compiler supporting Edition 2024.
The Foundation gate verifies Linux with both Rust 1.85 and the pinned
development toolchain; the release slice adds the remaining platform matrix.

The compatibility baseline for the first Rust release is the Python package at
commit
[`c4a980b`](https://github.com/tmux-python/libtmux/tree/c4a980b). The parity
ledger treats every module in its API reference as public, including
`Client`. Internal Python implementation types are ported only when their
behavior is visible through public APIs.

That baseline does not move during implementation. Later Python changes enter
the Rust scope only through an explicit baseline update and parity-ledger diff.

Behavioral parity means equivalent tmux effects, return information,
cardinality, ordering, version gating, and documented failure behavior. Rust
syntax intentionally differs where Python constructs have no sound or
idiomatic equivalent.

## Evaluated architectures

### Literal Python-shaped port

This approach would expose string IDs, dynamic `field__operator` lookups,
mutable property bags, broad target strings, and methods with many optional
arguments. It offers superficial familiarity but loses Rust's type checker,
cannot make live properties async, and preserves accidental behavior. It is
rejected.

### Runtime-generic core and adapter workspace

This approach would begin with separate core and Tokio crates and make every
handle generic over a transport. It keeps dispatch allocation-free, but
`Server<T>` propagates through `Session<T>`, `Window<T>`, `Pane<T>`, queries,
errors, and downstream signatures. The crate/version/feature coordination has
no initial consumer. It is rejected as premature.

### Tokio-first crate with an internal executor boundary

This is the selected design. Public handles are concrete and cheap to clone.
They share an `Arc<Core>`, while `Core` owns a private object-safe executor.
The subprocess executor is Tokio-native. Public values above execution remain
transport-independent.

The design grafts three proven ideas without importing their competing plan
models:

- immutable, inspectable query and command values as the semantic source;
- the familiar `Server -> Session -> Window -> Pane` authoring facade;
- typed fields, portable predicates, traversal, and explicit cardinality.

A later engine layer can embed the same query and command values in one
immutable statement graph. It must not introduce a second semantic model.

## Spike findings

The disposable spike compiled and exercised the selected seams against real
tmux. Its source is not copied into the crate.

- Native async methods in a public transport trait are not object-safe. An
  internal boxed adapter kept the public transport implementation ergonomic
  and every object handle non-generic.
- Direct transport generics compiled but spread through all child handle
  types. A closed transport enum compiled but prevented downstream engines.
- A single predicate trait accepting both closures and typed expressions
  defeated inline closure parameter inference on Rust 1.85. Native
  `.filter(|item| ...)` therefore remains the closure path, while
  `.matching(expr)` accepts portable expressions and named matchers. No
  collection wrapper or duplicate closure method is needed.
- Single-evaluation `q`-quoted tmux formats round-tripped delimiter bytes,
  embedded newlines, and invalid UTF-8 on pinned tmux 3.2a. Delimiter
  splitting and separately sampled length prefixes did not work.
- tmux 3.4 and 3.5 wrap that output in `VIS_OCTAL | VIS_CSTYLE | VIS_NOSLASH`,
  so the transport carries two escaping layers rather than one. The layers do
  not collide: `#{q:}` runs first and doubles every backslash, `VIS_NOSLASH`
  stops `vis` adding more, and no member of tmux's `format_quote_shell` set is
  an octal digit or a `VIS_CSTYLE` letter. Each dialect is therefore a separate
  injective grammar, and a decoder that accepts only one of them rejects the
  other's output instead of returning altered bytes.
- Owned snapshots refreshed predictably. Refreshing one clone did not mutate
  another clone, avoiding locks and hidden shared state.
- A real-tmux guard created unique short socket paths, exposed the exact path,
  and removed the daemon and socket on drop.
- Lenient list access and explicit `try_*` access can coexist without making
  raw command execution swallow failures.
- A failed command at the start of a tmux semicolon chain prevents later
  commands from executing. A control-mode implementation that predicts one
  result block per separator can wait forever for blocks tmux will never send.

## Architecture

```text
Server / Session / Window / Pane / Client
                 |
                 +--> operation builders --> Command
                 |
                 +--> Vec snapshots --> native iterators
                                      |
                                      +--> Matcher / FilterExpr
                 |
                 +--> immutable Info snapshots
                                   |
                              Arc<Core>
                                   |
                         private Executor trait
                                   |
                        Tokio subprocess executor
                                   |
                                  tmux
```

### Core and executor

`Core` owns the configured tmux executable, captured launch context, server
identity, default timeout, capability snapshot, and executor. Handles contain
`Arc<Core>` plus owned identity and snapshot data. No public handle is generic
over a runtime or executor.

The private executor is stored as
`Arc<dyn Executor + Send + Sync + 'static>`. It consumes an owned
`CommandRequest` and returns a `Send` future producing a raw result. It never
receives domain objects. Adding a public alternate executor later is additive
because the boundary already exists, but the first release does not freeze an
unused public async trait.

Each `Core` owns its dispatch-request counter. Cloned `Server` values share
that Core and counter; separately constructed servers have separate scopes,
even when they identify the same tmux endpoint. The Core allocates an ID before
executor validation, so a rejected request can carry an ID without spawning a
process. A caller retry is another Core dispatch request and receives another
ID. The ID is not globally unique or a process ID.

The Tokio executor uses argv directly and never invokes a shell. Each spawned
child is transferred to an independently owned supervisor task. That task
captures stdout and stderr concurrently, applies a deadline, and remains
responsible for terminating the child's isolated process group and awaiting the
direct child after deadline expiry, caller cancellation, or explicit shutdown.
Pipe draining precedes `wait`, so the unreaped direct child anchors the numeric
process-group identity until inherited pipes close or the overall deadline
expires. Dropping the caller future signals cancellation; `kill_on_drop` is
only a final safety net and is never the reaping strategy.

Deterministic async reaping requires the Tokio runtime to remain alive until
the operation or explicit executor shutdown completes. Runtime teardown cannot
make that async guarantee; it synchronously signals the process group and uses
`kill_on_drop` for the direct-child fallback, without claiming a completed
wait. Callers that own a runtime shut the `Server` down before the runtime.
`TestServer` separately owns its foreground tmux daemon as a synchronous child
so its `Drop` cleanup does not depend on a live Tokio runtime.

Concurrent subprocess requests have no ordering guarantee beyond the atomic
execution of each process. Callers that require ordering await requests in
sequence. A future persistent executor serializes protocol writes internally
while preserving correlation between concurrent callers. Every public handle
is `Send + Sync`; compile-time assertions make that contract executable.

### Commands and results

`Command` stores typed tokens rather than one shell string. Each token carries
both its executable value and a public-or-sensitive diagnostic classification.
Custom `Debug` renderers, executor errors, and tracing use only the redacted
diagnostic view. Every Foundation command token is literal, so a token ending
in `;` gains one escape byte during argv lowering. Existing backslashes do not
suppress that lowering. Shell-free execution alone does not make semicolons
literal because tmux reparses every argv token after process launch.

Foundation Core dispatch accepts one logical command per request.
`CommandResult` stores the Core-scoped dispatch-request identity, a sanitized
command summary, exit status, and raw stdout and stderr bytes. It never copies
executable argv.
Borrowed strict UTF-8 views return
`std::str::Utf8Error`; named lossy views are explicit. Owned decoding errors
are not retained because their diagnostics could expose raw output. A non-zero
status and non-empty stderr remain data at the raw `cmd()` boundary; domain
wrappers decide which tmux responses are failures.

Batching and structural semicolon chains are deferred. When they are added,
independent batches return one result per request. A direct subprocess chain is
one Core dispatch request and one result because subprocess output cannot prove
per-command attribution. A planner that requires individual terminal states
must dispatch independently unless the executor advertises per-command
correlation. Control-mode block identities remain separate from the Core
dispatch-request identity and are retained rather than merged; any command
without evidence is `unknown`, never guessed to be failed or skipped.

Later builders for pane input, environment values, prompts, and buffers must
mark those arguments sensitive at construction. Each owning slice passes
sentinel secrets through its command families and proves that `Debug`, errors,
and tracing omit them.

### IDs, targets, and relationships

`SessionId`, `WindowId`, and `PaneId` validate `$`, `@`, and `%` prefixes.
Targets are scope-specific: pane operations cannot accept window targets.
`ServerIdentity` structurally normalizes the effective socket endpoint,
including the resolved socket root for named and default sockets and the
absolute path captured for explicit sockets. Symlink-sensitive parent
components are preserved rather than collapsed, and dispatch uses the same
captured path as identity. It never uses `Arc` pointer identity. Independently
constructed servers for the same structural endpoint therefore compare
equally, while equal-looking object IDs on different endpoints do not.

Handle equality and hashing use `(ServerIdentity, object ID)` for sessions,
windows, and panes, and `(ServerIdentity, client_name)` for clients. Window and
pane handle identity follows the underlying tmux object, not the discovery
edge. `WindowLinkIdentity` separately represents
`(ServerIdentity, SessionId, window index, WindowId)`.

A strict ownership tree is insufficient because tmux windows are linked.
`WindowLink` is a first-class edge containing session ID, window index, and
window ID. Server-wide window and pane enumeration retains one row per
winlink. Underlying object identity and edge identity remain distinct.

`Pane` retains the winlink context from which it was discovered. Live parent
resolution re-queries tmux so move and link operations do not leave traversal
permanently attached to stale parents.

### Private snapshots and future refresh

The current `SessionInfo`, `WindowInfo`, `PaneInfo`, `ClientInfo`,
`Availability<T>`, projections, format plans, and built-in fields are
crate-private. No public hierarchy handle or listing can return them yet. The
discovery slice will promote only the values needed by a public consumer and
define handle refresh around complete owned snapshots.

Inside that private kernel, known IDs, indices, flags, sizes, timestamps, and
enums use typed fields. `TmuxText` retains stored bytes exactly and exposes
byte, strict UTF-8, and explicitly lossy views without an implicit string
conversion. `Availability<T>` distinguishes release-gated `Unsupported`,
development-build `Unproven`, semantic absence, and available values where
tmux makes those states observable. Both unavailable states are omitted from
the format plan:
`Unsupported` requires a detected release below the field floor, while
`Unproven` records that a release-less development build is admitted only to
the conservative baseline catalog. For a supported field, the descriptor
decides whether zero bytes mean `Absent` or an available empty `TmuxText`;
tmux does not reveal whether its callback returned `NULL` or an empty C
string. A scope-inapplicable field is a plan error rather than `Absent`.

`WindowInfo` describes an underlying window. Session ID, index, active state,
and link flags belong to `WindowLink`. Server-wide window rows become owned
`WindowProjection` values containing one link and one intrinsic window.
`PaneProjection` similarly retains the link identity for each pane row. The
same window or pane may therefore appear more than once without conflating
object identity with discovery-edge identity.

Comma-joined tmux callbacks that contain names remain opaque `TmuxText`:
tmux permits commas in those names and provides no escaping. Typed
relationships come from projection rows and IDs, never by splitting those
display values.

Future handles expose synchronous snapshot getters while live relationships
and commands remain async. The planned `refresh(&mut self)` replaces the
receiver's complete snapshot and returns `&mut Self`; `refreshed(&self)`
returns a new handle. Clones remain independent snapshots sharing only
`Core`.

The private descriptor catalog records stable field name, semantic owner,
required context, admitted list profiles, minimum tmux version, decoder, and
empty-value policy. A `FormatPlan` owns the selected descriptors, rendered
template, and parser order together. Each requested field is rendered once as
`#{q:field}%`, and transport bytes are parsed before any newline split or
UTF-8 decode. The decoder removes each quoting backslash and treats only the
template's unescaped `%` as a field terminator; the LF after the final
terminator ends the row. Value newlines are ordinary payload because rows end
at a counted terminator rather than at the next LF.

`CommandResult` always preserves the exact stdout transport emitted by the
selected tmux, and the plan decodes it through the dialect its version emits.
`TransportDialect::RawQ` covers releases before 3.4 and from 3.6 onward, where
`#{q:}` escaping reaches stdout verbatim. `TransportDialect::Vis` covers 3.4
and 3.5, where `server_client_print` additionally applied
`VIS_OCTAL | VIS_CSTYLE | VIS_NOSLASH`. Both recover arbitrary original non-NUL
callback bytes.

The window boundaries come from upstream commits rather than from observed
behavior: `7e497c7f` and `93b1b781` introduced the transform before tag 3.4,
and `5fd45b38`, "Do not strvis output to terminal from commands", restored
verbatim output before tag 3.6. A `next-X.Y` identifier resolves through the
release it precedes; `master` names no release and selects `RawQ`.

Dialect selection is evidence, not trust. The version probe reads the
configured client executable, but `server_client_print` runs in the daemon that
owns the socket, so the two can disagree. Each dialect therefore rejects the
escapes only the other produces: a mismatched pairing fails loudly at the first
divergent escape instead of decoding into different bytes.

Each decoded field is therefore one callback sample. Dangling escapes,
missing field terminators, missing row LF, and trailing bytes are framing
errors. The parser never splices or retries individual fields or rows.
Tmux still expands fields and rows sequentially, so a byte-exact decoded
listing is not a transactional snapshot of server state.

There is no aggregate `ServerSnapshot`. Future Session, Window, Pane, and
Client listings come from separate tmux commands and cannot form an atomic
capture. Their return floor is an ordered `Vec<T>`, but no public hierarchy
listing exists at the current milestone.

### Native local iteration

The implemented query kernel operates on user-owned `Vec<T>` and slices.
Future hierarchy listings use the same collection floor and preserve tmux
order. Native iterators provide the ordinary local API:

```rust
let tasks = vec![("build", false), ("test", true)];
let pending = tasks.iter().filter(|task| !task.1);
let first = pending.clone().next();
let collected = pending.collect::<Vec<_>>();
```

`QueryIteratorExt` is implemented only for iterators whose item is `&T`.
`matching()` is lazy and preserves order; `vec.iter().matching(expr)` works,
while `vec.into_iter().matching(expr)` intentionally does not. `Matcher<T>`
has a blanket implementation for `Fn(&T) -> bool`, but inline closures use
native `.filter()` because the blanket bound cannot infer an untyped closure
parameter on the MSRV.

`exactly_one()` inspects at most two items and returns `ExactlyOneError` with
distinct zero and multiple variants. `one_or_none()` returns `None`, one
borrowed item, or `MultipleItemsError`. Neither method counts, collects, or
exhausts a potentially infinite iterator. Importing both this extension trait
and `itertools::Itertools` makes the shared `exactly_one` method name
ambiguous; callers in that uncommon case use trait-qualified syntax.

### Portable filters

`FilterExpr<T>` is the only portable predicate value. It is an opaque typed
wrapper over an inert tree of scalar comparisons, ordered `and` and `or`
junctions, `not`, and explicit relation quantifiers. It stores stable field
names and owned literals, never reader closures or executor state. Structural
equality flattens adjacent junctions while retaining operand order; it does
not claim logical equivalence for reordered predicates.

Generated field handles make invalid operations fail to compile on downstream
data through the current public API:

```rust
use libtmux::query::{Filterable as _, QueryIteratorExt as _};

#[derive(libtmux::Filterable)]
#[filterable(target = "task")]
struct Task {
    name: String,
    done: bool,
}

let tasks = vec![Task { name: "build".into(), done: false }];
let fields = Task::filter_fields();
let expression = fields.name.starts_with("build").and(fields.done.eq(false));
let task = tasks.iter().matching(&expression).exactly_one()?;
```

String, boolean, integer, enum, to-one, and to-many handles expose only their
valid operations. To-many relations use `any`, `all`, and `none`; an empty
relation satisfies `all` and `none` but not `any`. To-one relations use `is`
and an absent value does not match. Relation matching is synchronous and may
inspect only relationships already present in the candidate value. Built-in
relation handles therefore land with an explicit hydrated projection rather
than performing hidden tmux I/O or treating unloaded relationships as empty.

Rust membership authoring accepts any `IntoIterator`. The version 1 wire uses
arrays only; membership in an empty array is false and exclusion from it is
true after the candidate meets its field precondition. Python's string-RHS
`in` behavior is not preserved; substring matching uses `contains`. Portable
booleans are JSON booleans, text and enum values are strings, and every
fixed-width integer is a canonical base-10 string. This avoids TypeScript
number precision loss. `isize` and `usize` are excluded because their range is
target-dependent.

`lt`, `lte`, `gt`, and `gte` take one bound rather than a set, and appear in
the schema beside the text operators because an integer rides as text. Which
fields accept them is the crate's business rather than the schema's: a bound
on a text field is refused when it is decoded, the same way an unknown field
is. Ordering compares the decoded integer, not the string, so `"10"` is above
`"9"`. A port reading this grammar has to do the same.

Scalar case-insensitive text operators use Unicode 16.0 default case folding
without normalization. Case-insensitive regex uses the pinned Rust regex
1.13.1 grammar and regex-syntax 0.8.11 Unicode 16.0 tables instead, because
folding regex syntax as ordinary text would change the pattern language. Wire
schema version 1 implies that dialect; the versioned fixtures lock both
semantics.

The optional `derive` feature re-exports `#[derive(Filterable)]` from a
sibling proc-macro package for downstream structs. Built-in libtmux fields
are generated inside the core crate and do not require that feature. The
derive generates typed handles and evaluation for scalar fields and explicit
`Vec<T>` and `Option<T>` relations.

Custom enum fields implement `FilterEnum`, which supplies a closed list of
stable variant names and the current value's name. Generated companion field
types have the same visibility as the source struct and default to
`<Struct>Fields`, with an explicit collision override.

Generated code calls a small `query::__private` runtime ABI. The module is
hidden from ordinary documentation but is compatibility-sensitive because
already-expanded downstream code names it. Core and macro versions are exact,
and compatibility fixtures exercise those signatures; `#[doc(hidden)]` does
not make the boundary private to Rust's linker or type system.

The optional `serde` feature serializes expressions through a private wire
adapter using a versioned tagged grammar with stable field names. Version 1 is
the numeric value `1` delivered by the host serde deserializer. Every signed
and unsigned integer visitor width and an `f64` equal to one are accepted;
accepted values serialize as integer `1`. With `serde_json`, `1`, `1.0`, and
`1e0` are equivalent, and a nearby source such as `1.0000000000000001` can
round to `1.0` before the library sees it. The schema retains exact numeric
`const: 1`, so an exact-decimal validator can reject a source that the host
rounds to one. Other host-decoded integral values are unsupported versions;
non-integral or non-finite floats and nonnumeric values are invalid structure.

The grammar, not Rust method names, is shared with the future TypeScript port.
Unknown versions, fields, operators, quantifiers, and incompatible literal
types are rejected during decoding so `FilterExpr::matches(&T)` remains
infallible. Every object is closed to unknown members. Hosts embed the complete
expression envelope under their own `where` member; `where` is not part of the
expression AST. Any new node, member, or literal representation increments the
schema version.

Duplicate JSON member names are invalid. Rust rejects them while deserializing
the raw text; the future TypeScript and external-input edges must perform the
same duplicate-aware parse before constructing an object because
`JSON.parse` alone collapses duplicates to the last value.

Regular-expression lookups use Rust `regex` syntax deliberately. Python's
`re` syntax includes constructs such as look-around and backreferences that
Rust's linear-time engine rejects. The operator and search semantics remain,
while unsupported pattern syntax is a validated, documented parity-ledger
delta rather than silently changing meaning.

### What an expression can name

An expression names one object's own fields. A `FilterExpr<Pane>` reaches the
`pane_*` catalog and nothing else, so "panes in the session named work" is not
a thing it can say. That is deliberate: the grammar stays per-object so it can
later compile to tmux's own `-f`, which evaluates against one row.

Cross-object questions are asked by narrowing first, which is what tmux does
with a target: `Session::try_panes` and `Window::try_panes` choose the rows,
and the expression chooses among them. `tmux-mcp`'s `find_panes` takes both
for exactly this reason.

Relations are how a parent is asked about, and they need a parent that holds
its children. A `Session` handle does not: it fetches its windows. The shape
that does is `Server::hierarchy`'s, so `SessionTree` and `WindowTree` are
`Filterable`, with a `windows` relation and a `panes` relation respectively.
Their targets are `session_tree` and `window_tree`.

Their field companions are hand-written, because libtmux does not use its own
derive. Each keeps the owned handle's fields under a named field -- `session`
and `window` -- and puts the relation beside it, so a session's own fields and
a question about its contents compose in one expression:

```rust
let sessions = SessionTree::filter_fields();
let windows = WindowTree::filter_fields();

sessions.session.session_name.starts_with("build")
    .and(sessions.windows.any(windows.window.window_name.eq("editor")))
```

Matching delegates: a predicate naming the relation resolves against the
children, and anything else is handed to the owned handle, which already knows
its own catalog. Validation delegates the same way, so an expression naming a
field the inner type does not have is rejected when it is decoded rather than
evaluating to false.

Local `matching()` never pushes down. A later
`server.query_sessions(expression)` family may compile supported predicates
to tmux `-f` before materialization and evaluate the remainder locally. That
slice must prove equivalence with pure local evaluation before exposing a plan
type. The Python `field__operator` syntax is only an edge parser for CLI, MCP,
or configuration input and never becomes the Rust authoring API.

### Object API

Public modules and object names mirror Python. Properties that execute tmux
become async methods; snapshot properties remain ordinary getters. Simple
operations remain methods with ordinary arguments. Operations with several
optional clauses accept a consuming `#[must_use]` options builder through
`impl Into<Options>`.

```rust
let server = Server::new()?;
let session = server.new_session("work").await?;
let window = session.new_window("editor").await?;
let pane = window
    .active_pane()
    .await?
    .ok_or(Error::MissingRelation(Relation::ActivePane))?;
pane.send_keys("cargo test").await?;
```

The same method accepts configured options without adding a second operation:

```rust
let options = NewSessionOptions::new("work")
    .window_name("editor")
    .start_directory("workspace");
let session = server.new_session(options).await?;
```

Options and hooks are exposed as inherent methods on each applicable handle,
not extension traits users must import. Private macros may remove repetitive
wrapper code while keeping generated public methods documented and tested.

Python context-manager cleanup maps to explicit async scoped operations:
`with_server`, `Server::with_session`, `Session::with_window`, and
`Window::with_pane`. Each accepts an async closure, waits for cleanup after
either success or error, and delegates cancellation cleanup to the same
independently owned supervisor used by command execution. Ordinary cloneable
handles do not perform async side effects in `Drop`.

### Errors and lenient collections

The current public `#[non_exhaustive]` `Error` enum covers Foundation server
configuration, command input, process execution, timeouts, shutdown, version
probing, and sanitized source context. The format kernel uses a private
`FormatCodecError`; future discovery converts its safe metadata into the
public domain error surface only when a public operation needs it. Target
disappearance, missing relationships, and unsupported operation capabilities
also remain future variants.

Local iterator cardinality uses the source-less `ExactlyOneError` and
`MultipleItemsError` values directly; it does not pass through `Error`.
Invalid regexes and portable wire data use the focused, source-less
`FilterExpressionError`. Public errors never retain executable arguments,
raw process output, row bytes, snapshot text, regex patterns, or serialized
filter values.

List-shaped object access follows the repository's established lenient
contract:

```rust
let sessions = server.sessions().await;
let sessions = server.try_sessions().await?;
```

The first returns an empty `Vec` when the underlying tmux list operation fails
and records the failure through tracing when enabled. The `try_*` form returns
`Result<Vec<_>, Error>` and preserves command, framing, and decoding failures.
This pair applies consistently to hierarchy collections. Expression
construction, future explicit remote query execution, and mutations remain
loud.

### Capabilities and future engines

The first `EngineCapabilities` is an immutable wrapper around the exact
detected tmux version. That is the only capability state consumed by the
one-shot Foundation and the private version-gated format rendering. Transport
bytes remain a `CommandResult` contract; original callback-byte recovery is a
separate version-sensitive format concern. The version is reported by the
configured executable and does not assert the build of a daemon already
listening at the selected endpoint.

The initial crate does not expose Statement, Program, planner, registry,
approval, or retry policy APIs. The public query and command values can later
be embedded into one immutable graph. When prepared physical work lands, it
must bind to the exact capability value and distinguish logical operation and
node identity, the current Core-scoped dispatch-request identity,
executor-internal attempt identity, and physical transport correlation. A
caller retry creates a new dispatch request. A future executor-internal retry
keeps the accepted dispatch-request identity and assigns separate attempt
identity; a subprocess PID or control-mode block number is correlation evidence
for an attempt, not the public request ID. Result shape must match the
executor's proven correlation granularity.

A future control-mode engine must issue independent requests separately from
semicolon chains, keep draining protocol frames after caller cancellation,
and give each logical node exactly one terminal state: complete, failed,
skipped, or unknown. It may report per-command states only when retained
protocol evidence supports that attribution.

## Control mode

The `control-mode` feature opens one tmux connection and keeps it. A task owns
the pipes; callers hold a `ControlSender` and a `ControlEvents`, which is a
`Stream`.

That task is an actor, and this document previously argued against one on the
grounds that a caller-driven connection buffers and drops nothing out of sight.
The argument was backwards. Control mode exists to act on what the server
reports, and a single object holding both directions needs `&mut` for each, so
a task awaiting an event cannot send the command that event implies. One task
multiplexing the connection is what makes the feature usable at all.

What the earlier framing was right about is kept:

- events are handed over with backpressure, never dropped -- a consumer that
  stops reading stops the connection reading from tmux, which is the
  backpressure tmux already applies to a slow client;
- the connection lives while either handle is in use and ends when both are
  gone, so a caller who only watches and a caller who only sends are both
  ordinary;
- `attach` returns only once tmux has the client attached, so a change made
  immediately afterwards is reported rather than racing the attach.

Correlation is by arrival order, which is sound because tmux answers commands
in order and blocks do not nest. The block number tmux assigns is carried on
the result rather than used to match, because a command that fails early can
leave a caller waiting for a block tmux will never send.

The protocol is parsed as bytes. tmux escapes only what would break the line
protocol -- bytes below `0x20`, and backslash -- so `%output` carries a pane's
bytes literally and a line is not necessarily UTF-8.

## Module and component map

This table spans implemented and planned components. A listed target file is
not a current public module or export until its owning roadmap slice closes.

| Component       | Responsibility                                         | Primary Rust files                      | Python source baseline                    |
| --------------- | ------------------------------------------------------ | --------------------------------------- | ----------------------------------------- |
| Crate facade    | Re-exports, feature gates, package docs                | `src/lib.rs`                            | `src/libtmux/__init__.py`                 |
| Commands        | Redacted arguments, grouping, raw results              | `src/command.rs`                        | `src/libtmux/common.py`                   |
| Versions        | Tmux releases, minimum checks, capabilities            | `src/version.rs`, `src/capabilities.rs` | `src/libtmux/common.py`                   |
| Errors          | Public Foundation/query errors; future domain errors   | `src/error.rs`, `src/query.rs`          | `src/libtmux/exc.py`                      |
| Constants       | Scopes, directions, flags, compatibility constants     | `src/constants.rs`                      | `src/libtmux/constants.py`                |
| Formats         | Typed format descriptors and row parsing               | `src/formats.rs`                        | `src/libtmux/formats.py`, `neo.py`        |
| Snapshots       | Typed object information and winlink projections       | `src/snapshot.rs`                       | `src/libtmux/neo.py`                      |
| Targets         | Validated IDs, typed targets, winlink edges            | `src/target.rs`                         | object modules and `neo.py`               |
| Queries         | Iterator extensions, typed expression AST, cardinality | `src/query.rs`                          | `_internal/query_list.py`, filtering docs |
| Runtime core    | Configuration, capabilities, private executor          | `src/internal/core.rs`                  | command and fetch paths                   |
| Tokio transport | Spawn, capture, timeout, cancellation, reaping         | `src/internal/subprocess.rs`            | `tmux_cmd`                                |
| Server          | Connection and server-wide operations                  | `src/server.rs`                         | `src/libtmux/server.py`                   |
| Session         | Session traversal and operations                       | `src/session.rs`                        | `src/libtmux/session.py`                  |
| Window          | Winlink-aware traversal and operations                 | `src/window.rs`                         | `src/libtmux/window.py`                   |
| Pane            | Pane I/O, layout, popup, mode, and movement operations | `src/pane.rs`                           | `src/libtmux/pane.py`                     |
| Client          | Attached client snapshots and traversal                | `src/client.rs`                         | `src/libtmux/client.py`                   |
| Options         | Typed option scopes, parsing, get/set/unset            | `src/options.rs`                        | `src/libtmux/options.py`                  |
| Hooks           | Hook values and get/set/unset/run operations           | `src/hooks.rs`                          | `src/libtmux/hooks.py`                    |
| Test support    | Isolated sockets, names, retries, temporary objects    | `src/test.rs`, `tests/support/`         | `pytest_plugin.py`, `src/libtmux/test/`   |
| Parity ledger   | Public surface mapping and intentional deltas          | `docs/parity.md`                        | API reference and tests                   |
| Tooling         | Cargo metadata, just recipes, format/lint/CI gates     | crate configuration                     | `pyproject.toml`, workflows               |

The public file names stay close to Python. Runtime-only implementation files
live under private `internal`; no `_internal` module is exported.

## Test architecture

The optional `test-support` feature exports `libtmux::test::TestServer` for
downstream crates. Repository integration tests use the same implementation.

Each guard:

- creates a unique short temporary directory and explicit `-S` socket path;
- exposes the exact socket path and a configured `Server`;
- starts tmux with `-D`, a controlled config, and command-local environment,
  isolates its process group, and retains the foreground daemon child;
- configures every command through the exposed `Server` with tmux's global
  no-start flag so it cannot bootstrap an unowned replacement daemon;
- provides consuming async shutdown that awaits the owned daemon;
- caps graceful observation at five seconds on targets without a safe
  non-reaping child-exit observer, then forces group cleanup and waits;
- reobserves child ownership before each numeric signal phase and permanently
  retires numeric PID, process-group, `Child` signal, and `Child` wait
  operations after `ECHILD` or another untrusted observation failure;
- performs synchronous forced best-effort cleanup in `Drop` without depending
  on a Tokio runtime or claiming that an unreportable failure succeeded;
- retains an opened owned-directory descriptor and removes only the fixed
  socket, configuration, and lock basenames with descriptor-relative cleanup;
- disables lexical recursive temporary-directory cleanup and removes the
  original directory entry through its retained parent descriptor;
- never launches a path-based cleanup client after exposing the socket path.

On Linux, process-group cleanup is followed by an exact-marker sweep. A process
is admitted only when its real UID matches the guard, its live non-zombie
`(pid, uid, start time)` identity is readable, its NUL-delimited environment
contains the exact marker entry, a pidfd opens, and both identity and marker
survive revalidation. Every admitted process is frozen and signaled through
that pidfd. The admitted-pidfd collection owns a best-effort `SIGKILL` fallback:
a later scan, timeout, revalidation, or signal error kills every process already
frozen before the sweep returns failure. The root remains in place because a
best-effort signal is not proof that every target became terminal.

The Linux marker is guard-selection metadata, not authentication or ancestry
proof. A same-UID process that copies the marker is admitted. A genuine
descendant that clears the marker or makes it unreadable before initial
admission is outside the sweep. Initial opacity is skipped because it cannot be
distinguished from another guard's process; opacity after an initial identity
and marker match fails the sweep.

On other Unix targets, successful cleanup proves only process-group signaling
and direct-child waiting. It does not contain descendants that detach with
`setsid` or otherwise leave that group. On every target, observing lost child
ownership makes lifecycle cleanup fail closed: no later numeric process or
group signal and no later `Child` signal or wait is issued, fixed files are
retained, and consuming startup or shutdown reports `ShutdownFailed`. Linux
still attempts its pidfd marker sweep. A successful direct-child wait likewise
permanently retires numeric signaling, so a later containment failure cannot
rearm a stale PID or process group.

The non-reaping observation and the following numeric signal are not one
atomic operation. An uncoordinated external waiter can still reap the leader
between them; this design detects ownership loss at the next observation but
cannot close that final race portably. It also cannot synchronously report a
failure from `Drop` or prove termination of a process stuck indefinitely in
the kernel. Stronger Linux isolation would require an explicit facility such
as a cgroup, PID namespace, or subreaper rather than a stronger claim about the
marker.

Tests use observable polling with deadlines, never fixed sleeps. Later parser
and query tests use hand-written byte fixtures. Behavioral tests execute real
tmux. Foundation transport tests cover timeout cancellation, child reaping, invalid
UTF-8, non-zero status with stdout, stderr without failure, absent daemons,
task abort during a command, cancellation during scoped shutdown, redaction on
every diagnostic surface, daemon PID disappearance, and cleanup after panics.

The Foundation workflow runs the pinned tmux 3.2a floor on Linux. The format
slice adds a dedicated Linux harness that builds pinned 3.2a and 3.6 commits,
supplies each binary directly to `TestServer`, and exercises both sides of the
3.6 linked-window callback transition. An unpinned distro tmux cannot
substitute for either endpoint. The harness also builds pinned 3.4 and 3.5a,
the two releases that visually encoded command output, so the `Vis` dialect is
exercised in CI rather than only on a developer's own machine.

The adversarial transport fixture sets one server option to a value carrying a
field terminator, a row terminator, a `#{q:}` special, a multibyte character,
and two bytes that are invalid UTF-8. It then asserts the exact stdout its
dialect emits and decodes that live transport back to the original bytes. It
runs on both sides of both boundaries: 3.4 and 3.5a on the `Vis` lane, 3.6 and
current 3.7b on the `RawQ` lane. A unit fixture additionally round-trips every
nonzero byte value through the `Vis` grammar, and mismatched dialect pairings
assert a loud framing error rather than altered bytes. The release
compatibility matrix will add maintained
tmux releases on Linux and the latest stable release on macOS. Tests for newer
flags assert both supported behavior and the version-gated error or warning
path when their owning slices land.

A parity ledger maps every pinned-baseline Python public method and property to
its Rust method, intentional syntax delta, tmux version gate, and evidence.
The crate is not feature-complete while any baseline capability remains
`planned` or `in progress`; reviewed omissions are recorded as `excluded`.

## Lint and dependency gates

`clippy.toml` carries what the lint levels alone cannot express. `expect`,
`unwrap`, and `panic` are denied workspace-wide, because a library returns
errors rather than deciding to end the caller's process -- but test code
asserts, so the three `allow-*-in-tests` settings exempt it without every
test file repeating an allow attribute. Files with helpers outside a test
function still need one, since clippy's exemption follows test functions
rather than file paths.

`await_holding_lock` and `await_holding_invalid_type` are denied because the
whole public surface is async and the transport supervisor holds a
`std::sync::Mutex`. Nothing holds a guard across an await today; the lints
keep it that way.

`cargo semver-checks` compares the public API against the previous release
and reports which lints a version bump would violate. It is a release gate
rather than a per-PR one, for two reasons. It needs a published baseline, and
until one exists it has to be pointed at a git revision with
`--baseline-rev`. And every version transition inside `0.x.y-prerelease` space
counts as major, which permits everything -- a run today reports `0 checks,
254 skip`. Forcing the comparison with `--release-type minor` across the
commit that closed the hierarchy shapes reports all six correctly, so the tool
does understand this crate; it simply has nothing to say until there is a
release to compare against. Run `just semver` before publishing.

`deny.toml` gates the dependency tree through `cargo deny`: an allowlist of
the permissive licences the tree actually carries, denial of yanked crates
and unknown registries, and a wildcard ban that exempts intra-workspace path
dependencies, which carry no version because the consumer crates are
`publish = false`.

## Compatibility lanes

`scripts/test-tmux-format-compat.sh` builds each pinned tmux from source and
runs the whole workspace against it, all targets and all features. The lanes
are 3.2a, 3.4, 3.5a, 3.6, and 3.7b: the floor, the ceiling, and the releases
in between that are known to differ. 3.4 and 3.5a are the two that wrapped
command output in `VIS_OCTAL|VIS_CSTYLE|VIS_NOSLASH`, and are the reason the
codec has a second dialect at all.

Running only the codec tests on the middle lanes was tempting and wrong. The
crate's version sensitivity is not confined to the codec: command flags,
control mode, and the stderr wording that separates a missing target from a
refusal all vary by release in principle. Asserting them against one tmux
proves nothing about the others, and the first full run of the floor lane
found a real difference -- `new-session -x -y` sets `default-size` on every
release, but 3.2a still draws a client-less window at 80x23 where 3.6 uses
the default. The crate behaves identically on both; only tmux's rendering
differs, which is why the test asserts the option rather than the geometry.

A ceiling lane matters as much as the floor. Without one, the newest tmux --
the version most people actually run -- would be covered only by whatever the
runner image happens to package, which lags releases by a long way.

## What downstream can construct

Handles and snapshots keep private fields and expose accessors, so adding a
field is not a breaking change. The two shapes `Server::hierarchy` returns are
the exception, because a caller reads `branch.session` and `branch.windows`
directly and an accessor would only be in the way. They are `#[non_exhaustive]`
instead: field access stays, construction and exhaustive destructuring do not,
and a later `clients` relation costs nobody a major version.

`Chooser` and `PaneProgressState` are `#[non_exhaustive]` for the same reason
in the other direction: both enumerate something tmux owns and can extend, so
a downstream `match` needs a `_` arm.

The geometric enums -- `SplitDirection`, `ResizeDirection`, `WindowPlacement`,
`PaneSize` -- are left open deliberately. Their variants are complete by
construction, and exhaustive matching over them is worth more than room to grow
that will not be used.

## Classifying a failure

`Error` has a variant per failure mode; `Error::kind` reduces those to the
decisions a caller actually makes, in the shape of `std::io::Error::kind`.
`ObjectGone`, `Refused`, `Timeout`, `Unreachable`, `UnsupportedVersion`,
`InvalidInput`, `Transport`, and `Decode` each imply a different next step.

Separating `ObjectGone` from `Refused` needs tmux's stderr. tmux exits 1 both
for a target it cannot find and for an argument it does not like, and reports
the first as `can't find <kind>: <target>`. That message is not localized --
tmux has no message catalogue -- and `cmd-find.c` has carried the same four
wordings, plus `no current target`, unchanged from 3.2 through the current
development branch.

Stability of someone else's strings is not something to take on faith, so
`real_tmux_compat_error_missing_target_wording_is_recognized` asserts the
classification against whichever tmux is running, and every compatibility
lane requires that test family to exist. A release that rewords these fails
there rather than silently returning the wrong answer from
`Error::is_object_gone`. A wording the crate does not recognize stays a
refusal, so the cost is the distinction rather than correctness.

One case needs more than the message. A server holding no sessions reports
`no current target` for any command needing one, including `-a` listings that
ask for everything. What that means depends on the request: a server-wide
listing has nothing to list, while a listing or mutation under a target could
not resolve it. The scope, and the request's own `-t`, are what tell them
apart.

Listing accessors come in pairs, and the split is load-bearing here. The
lenient form returns an empty `Vec` for any failure, which suits a status
line. The `try_` form propagates, which is the whole reason it exists -- a
`try_` form that quietly returned no rows for an unreachable daemon would make
the pair meaningless.

## Cost of gathering the hierarchy

`Server::hierarchy` issues three tmux commands whatever the server holds --
`list-sessions`, `list-windows -a`, `list-panes -a`, run concurrently -- and
stitches the rows locally. Walking down instead costs one command per session
and one per window, which is the shape a caller reaches for first.

`tests/command_budget.rs` asserts the counts by observing what the crate
actually ran, so a change that reintroduces per-object commands fails rather
than merely getting slower. `benches/hierarchy.rs` reports the time, measured
on one developer machine against tmux 3.7b:

| server | `hierarchy()` | walking down | ratio |
| --- | --- | --- | --- |
| 1 session, 1 window, 2 panes | 6.7 ms | 22.3 ms | 3.3x |
| 2 sessions, 8 windows, 16 panes | 15.0 ms | 78.1 ms | 5.2x |
| 4 sessions, 32 windows, 64 panes | 26.6 ms | 282 ms | 10.6x |

The gathered column is not flat, and saying it is would be wrong: the command
count is constant, but tmux still has to produce and the crate still has to
parse a row per object. What is constant is the per-object process spawn, which
is what dominates the walking column.

Stitching the three listings groups by the numeric part of each tmux ID, which
is `Copy`, so joining them allocates nothing per object.

## Toolchain and packaging

MSRV is a promise about the published crates. `libtmux` and `libtmux-macros`
build and test on 1.85. The consumer crates, `tmux-mcp` and
`tmux-workspace`, are `publish = false` and exist to exercise the public API
from outside, so they track current stable and their dependency trees do not
constrain the library. The MSRV and packaging gates name the published crates
rather than the workspace for that reason.

The consumers also prove the dependency edge runs one way: `libtmux` names
neither of them, and `cargo tree -p libtmux` lists only caseless, regex,
rustix, thiserror, and tokio.

A declared dependency version is the minimum this crate supports, not the
newest published one. Cargo's version-aware resolver picks the newest release
each toolchain can build, so a floor below the latest is the normal state
rather than staleness. A floor is raised only for a fix the crate needs, and
never above a version whose own `rust-version` exceeds the MSRV -- which is
what rules out criterion 0.8 and trybuild 1.0.120 today.

The crate uses Edition 2024, Cargo resolver 3, `rust-version = "1.85"`, and
the repository's MIT license. `rust-toolchain.toml` pins the development
compiler and installs Rustfmt, Clippy, and rust-src. Rustfmt uses stable
settings only.

The Foundation dependency set stays narrow:

- Tokio with only process, I/O, time, synchronization, runtime, and macro
  features;
- thiserror for the public error enum;
- optional tracing instrumentation without subscriber configuration;
- optional tempfile support for the public test guard.

The query slice adds regex support plus independent optional `serde` and
`derive` features. The derive macro lives in the sibling
`libtmux-macros` proc-macro package; normal snapshot querying does not compile
it. Current Cargo verifies the unpublished workspace packages in dependency
order, while Rust 1.85 remains a build-and-test gate rather than a packaging
toolchain.

The manifest includes complete description, repository, documentation,
readme, license, keywords, categories, docs.rs feature metadata, and an
explicit include list. `Cargo.lock` is committed for repository CI
reproducibility.

Rust lints forbid unsafe code and warn on missing docs, unreachable public
items, unused lifetimes, and unused qualifications. Clippy enables `all` and
`pedantic` with narrow documented exceptions; unwrap, expect, and panic are
denied in library code and permitted in tests. CI promotes warnings to errors.

The justfile recipes call the same direct Cargo gates documented for Rust
users, and `just` alone lists them:

```console
$ just check
```

The aggregate gate runs formatting, Clippy, all-target/all-feature tests,
doctests, documentation with warnings denied, the no-default-features build,
bounded feature-powerset checks, and the MSRV lane. `cargo hack` verifies each
public feature independently and the supported combinations on both the
development toolchain and MSRV. Dependency audit and semver checks remain part
of the final compatibility slice; they are not required to build the crate.

## Delivery sequence

The work is split into independently reviewable subprojects:

1. Crate foundation, raw transport, errors, IDs, capabilities, and real-tmux
   test guard.
2. Format catalog, typed snapshots, parsing, winlinks, native iterator
   extensions, portable filters, and downstream derive support.
3. Server, Session, Window, Pane, and Client discovery, traversal, refresh,
   and environment resolution.
4. Full mutation and interaction parity across the object hierarchy.
5. Options, hooks, keys, buffers, menus, prompts, popups, and control-related
   command families.
6. Documentation, examples, compatibility matrix, parity closure, fuzzing,
   semver checks, and final adversarial review.

Each subproject begins from failing behavior tests, ends with the complete
crate gate, and receives an independent API and Rust-quality review. Spike
code is never promoted; implementation is written afresh from the approved
contracts.

## Acceptance criteria

The crate is complete only when:

- the parity ledger accounts for every pinned-baseline Python public
  capability;
- all intentional differences are documented as Rust syntax or verified bug
  corrections rather than missing behavior;
- the full Rust format, Clippy, test, doctest, docs, feature, and MSRV gates
  pass;
- real-tmux tests pass on tmux 3.2a and the maintained compatibility matrix;
- socket isolation and cleanup pass under parallel tests and panic paths;
- timeout, dropped-future, and task-abort tests prove process-group termination
  and direct-child reaping;
- portable filter serialization matches the versioned grammar and local
  evaluation remains the reference semantics for any later pushdown;
- server-wide linked-window and pane enumeration preserves every winlink;
- an adversarial Rust API review has no unresolved critical or important
  findings;
- no shipped file contains spike code, local paths, private data, unstable
  source links, AI signatures, or unowned scaffolding.