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
//! Thin adapter over `trusty_common::tmux` + the crate's single
//! tmux-spawning entry point (#2398 architecture consolidation, #3004
//! shared-layer extraction).
//!
//! Why: trusty-mpm hosts each Claude Code session inside a named tmux session
//! (the primary control model — see `docs/research/session-control-models.md`).
//! `TmuxTarget`/`TmuxCommand`/`tmux_argv` used to live here, but issue #3004
//! extracted them (plus `scrollback_option_commands` and the
//! options-before-`new-session` ordering guarantee) into `trusty_common::tmux`
//! once a scrollback bug report against trusty-agents' INDEPENDENT tmux
//! implementation revealed the real problem: #2398/#2399 could only fix the
//! crate they lived in. This module re-exports the shared pure layer
//! unchanged (every existing call site in this crate keeps compiling against
//! `crate::core::tmux::{TmuxTarget, TmuxCommand, tmux_argv, ...}`) and keeps
//! everything that is genuinely trusty-mpm-specific: binary resolution
//! glue, TCC-disclaimed process spawning (issue #2819/#2820 — NOT moved to
//! the shared layer, since it is a macOS/trusty-mpm-signing concern, not a
//! tmux concern), and `create_managed_session`, which layers this crate's
//! own `TrustyToolsConfig` resolution on top of the shared
//! `managed_session_commands` recipe.
//!
//! Originally this module was argv-construction only, with each of the
//! daemon (`daemon::tmux::TmuxDriver`), the CLI (`bin/tm`'s launch/connect/
//! session-start commands), the TUI client (`client::http_client`), and the
//! control-plane backend (`control::backend::tmux`) independently spawning
//! `std::process::Command::new("tmux")`. That let FIVE call sites bypass the
//! #2398 scrollback/mouse ergonomics entirely (a QA-caught regression) simply
//! by not knowing about `daemon::tmux::TmuxDriver::create_session`. Per Bob's
//! architecture directive, this module now ALSO owns the actual process
//! spawning for output-capturing tmux commands
//! ([`run_tmux_with_bin`]/[`run_tmux`]) and the single choke point for
//! creating a session with ergonomics applied first
//! ([`create_managed_session`]) — every session-creating call site in the
//! crate routes through it, so nothing can bypass the scrollback/mouse
//! options again. [`daemon::tmux::TmuxDriver`](crate::daemon::tmux::TmuxDriver)
//! wraps [`run_tmux_with_bin`] rather than spawning independently.
//!
//! Scope note (#2398 follow-up filed for stragglers): a handful of read-only
//! probes — `tmux display-message -p '#S'` (current-session-name lookups in
//! `bin/tm/commands/tmux_attach.rs` and `bin/tm/commands/statusline/
//! branch.rs`), `tmux show-environment` (`bin/tm/commands/guided_inplace.rs`),
//! and `tmux display-message -t <s> -p '#{pane_pid}'`
//! (`core::process::tmux_pane_pid`) — still shell out directly. They are
//! read-only queries against an EXISTING session, not session-creation, so
//! they cannot bypass the scrollback/mouse ergonomics; migrating them was
//! judged out of scope for this PR (no `TmuxCommand` variant models
//! `display-message`/`show-environment` yet) to keep the diff reviewable.
//! `bin/tm/commands/tmux_attach.rs::tmux_attach` (the actual `attach-session`/
//! `switch-client` spawn) DOES route its binary resolution through
//! [`resolve_tmux_binary_or_bare`] even though its interactive,
//! stdio-inheriting `.status()` call shape does not fit [`run_tmux`]'s
//! output-capturing signature.
//! What: re-exports `TmuxTarget`/`TmuxCommand`/`tmux_argv`/
//! `scrollback_option_commands`/the scrollback defaults from
//! `trusty_common::tmux`; [`resolve_tmux_binary`]/[`resolve_tmux_binary_or_bare`]
//! (binary resolution), [`run_tmux_with_bin`]/[`run_tmux`] (the shared spawn
//! primitive), and [`create_managed_session`] (the session-creation choke
//! point with ergonomics baked in, config-resolved locally).
//! Test: `cargo test -p trusty-mpm-core` covers the mpm-specific spawn/config
//! glue; argv construction and the options-before-new-session ordering
//! guarantee are tested once in `trusty_common::tmux` and reused here without
//! re-testing (`managed_session_command_sequence_matches_shared_layer`
//! asserts THIS crate's config-resolved call routes through it correctly).
use warn;
pub use ;
/// Resolve the `tmux` binary, preferring live `PATH` and falling back to
/// well-known daemon dirs (Homebrew, user bins) via `trusty_common::bin_resolve`
/// (#2398 architecture consolidation).
///
/// Why: a daemon process under launchd inherits a minimal `PATH` (#1298);
/// resolving through the SAME helper for every caller (daemon, CLI, TUI
/// client) — rather than a bare `"tmux"` PATH lookup for some callers and a
/// well-known-dirs-aware lookup for others — means resolution behavior is
/// identical everywhere, and there is exactly one place to fix if it ever
/// needs to change.
/// What: delegates to `trusty_common::bin_resolve::resolve_binary("tmux")`.
/// Test: `resolve_tmux_binary_does_not_panic`.
/// [`resolve_tmux_binary`], falling back to the literal `"tmux"` (a plain
/// `PATH` lookup at spawn time) when resolution itself comes up empty.
///
/// Why: callers that do not need [`daemon::tmux::TmuxDriver::discover`]'s
/// (crate::daemon::tmux) fail-fast-if-truly-absent contract — the CLI and TUI
/// client, which already surface their own "tmux command failed" error on the
/// subsequent spawn — just want a best-effort binary name to run. Falling
/// back to the bare string preserves their pre-#2398 behavior (trust the
/// interactive shell's own `PATH`) as the worst case.
/// What: returns the resolved absolute path as a `String` when found, else
/// `"tmux"`.
/// Test: `resolve_tmux_binary_or_bare_never_empty`.
/// Run a typed tmux command against an EXPLICITLY resolved binary path,
/// returning the raw process `Output` (#2398 architecture consolidation).
///
/// Why: this is the ONE place in the crate that actually spawns an
/// output-capturing `tmux` child process — every caller
/// ([`daemon::tmux::TmuxDriver`](crate::daemon::tmux::TmuxDriver), `bin/tm`'s
/// CLI commands, `client::http_client`'s TUI-driving methods, and
/// `control::backend::tmux::TmuxBackend`) routes through here so argv
/// construction and process-spawning can never drift or be re-implemented ad
/// hoc at a call site — the exact drift that let 5 CLI/client call sites
/// bypass the scrollback/mouse ergonomics entirely (the QA finding this
/// consolidation closes). Takes an explicit `tmux_bin` (rather than
/// re-resolving on every call) so callers that already cache a resolved path
/// — like `TmuxDriver`, which resolves once in `discover()` for its
/// fail-fast-if-absent contract — do not pay repeated resolution cost.
/// What: renders `cmd` via [`tmux_argv`] and spawns `tmux` through
/// [`crate::core::spawn_disclaim::disclaimed_output`], which on macOS disclaims
/// TCC responsibility so the forked tmux server (and every `claude`/agent
/// descendant) is its own responsible process rather than rolling attribution up
/// to the signed `trusty-mpm` binary (issue #2819). On non-macOS it is a plain
/// `Command::output`. Every session-creating call site routes through here, so
/// the disclaim covers the one spawn that forks the shared server regardless of
/// which tmux sub-command triggers the fork. This TCC-disclaim wrapping is WHY
/// process spawning stayed in trusty-mpm rather than moving to
/// `trusty_common::tmux` in #3004 — it is a trusty-mpm-signing-identity
/// concern, not a tmux-argv concern, and trusty-agents does not need it (no
/// equivalent signed-binary/TCC-prompt problem reported for it). Callers own
/// interpreting the exit status / stderr — this function does not classify
/// failures.
/// Test: `disclaimed_output_captures_stdout` (and the sibling
/// `disclaimed_output_*` tests) in `crate::core::spawn_disclaim` cover the
/// disclaim/capture behaviour; this thin adapter is exercised transitively by
/// every call site (a live `tmux` binary is required to observe real output —
/// see the `#[ignore]` integration tests).
/// [`run_tmux_with_bin`], resolving the tmux binary via
/// [`resolve_tmux_binary_or_bare`] first.
///
/// Why: the common-case convenience for callers (CLI, TUI client) that do not
/// already hold a cached, resolved tmux path.
/// What: resolves then delegates to [`run_tmux_with_bin`].
/// Test: exercised transitively; see [`run_tmux_with_bin`].
/// Build the exact ordered [`TmuxCommand`] sequence [`create_managed_session`]
/// issues, given already-resolved tmux options (#3004).
///
/// Why: extracted as a pure function (no process spawning, no config
/// loading) purely so a hermetic unit test can assert THIS crate's
/// session-creation call routes through `trusty_common::tmux`'s shared
/// ordering guarantee with the right, config-resolved parameters — the
/// per-consumer "integration assertion" issue #3004 asks for, without
/// re-testing the ordering guarantee itself (already covered once in
/// `trusty_common::tmux`'s own tests).
/// What: delegates straight to
/// [`trusty_common::tmux::managed_session_commands`] with `idempotent: true`
/// (trusty-mpm's `-A`-attach creation semantics) and no initial command.
/// Test: `managed_session_command_sequence_matches_shared_layer`.
/// Outcome of [`create_managed_session`] (#3386 review finding).
///
/// Why: a `warn!`-and-continue on a failed scrollback-ergonomics
/// verification is exactly the silent-degrade #3386 was originally filed
/// against — only grep-able, never surfaced to whoever asked for the
/// session. Wrapping the raw `new-session` `Output` together with an
/// explicit `options_verified` flag forces every call site to at least
/// LOOK at whether the pane it just got may be capped at tmux's factory
/// 2000-line history-limit, rather than the flag being available only to a
/// caller who happens to read the log.
/// What: `output` is the `new-session` process output — callers classify
/// session-creation success/failure from it exactly as before
/// (`output.status.success()`); `options_verified` is `true` only when an
/// apply-and-verify cycle (server-up, `set-option -g` x2, `show-options`
/// probe) confirmed `history-limit` landed on the server before this
/// `new-session` call ran.
/// Log an operator-visible `error!` naming `session_name` when `outcome`'s
/// scrollback ergonomics were not verified (#3386 review).
///
/// Why: shared by every `tracing`-based call site
/// ([`daemon::tmux::TmuxDriver::create_session`](crate::daemon::tmux::TmuxDriver::create_session),
/// `client::http_client::session_connect`) so the exact message stays in
/// one place and each call site only pays ONE line, keeping
/// `daemon::tmux` (already near its 500-SLOC production cap) from growing
/// just to inline this check. A no-op when `outcome.options_verified` is
/// `true`.
/// What: `tracing::error!` (not `warn!` — a `warn!`-only log is the exact
/// silent-degrade #3386 was filed against) with a `session` field.
/// Test: exercised transitively by `daemon::tmux`'s and
/// `client::http_client::session_connect`'s own tests; the condition itself
/// is covered by `apply_and_verify_scrollback_options_returns_false_after_exhausting_retries`.
/// Create a tmux session with the configured scrollback + mouse ergonomics
/// applied FIRST — THE single choke point for "create a managed session"
/// used by every session-creating call site in the crate (#2398 architecture
/// consolidation).
///
/// Why: `history-limit` is captured into a pane's ring buffer AT CREATION
/// TIME, so the scrollback/mouse `set-option -g` calls MUST run before
/// `new-session` — see [`trusty_common::tmux::scrollback_option_commands`].
/// Baking both steps into one function (rather than asking every caller to
/// remember "apply options, then create") is what makes it structurally
/// impossible for a future call site to bypass the ergonomics again, which
/// is exactly how the #2398 QA finding happened the first time (5 call
/// sites independently shelled out to `tmux new-session`, none aware the
/// daemon-side ergonomics existed) — and, per #3004, exactly how
/// trusty-agents' independent tmux implementation missed the fix entirely.
/// What: resolves the tmux binary (`tmux_bin` if given, else
/// [`resolve_tmux_binary_or_bare`]), loads
/// [`crate::core::trusty_tools_config::TrustyToolsConfig`] and resolves the
/// `tmux:` section, then runs [`apply_and_verify_scrollback_options`] (#3386
/// — retries the WHOLE apply-and-verify cycle, not just server-up, since a
/// successful `start-server` does not prove the following `set-option -g`
/// landed) before issuing `new-session`. Never blocks session creation on a
/// degraded outcome — `new-session` always still runs — but the returned
/// [`ManagedSessionOutcome::options_verified`] tells the caller whether it
/// can trust the ergonomics landed.
/// Test: argv construction and the ordering guarantee are covered once in
/// `trusty_common::tmux`'s own tests; this crate's config-resolved call is
/// covered by `managed_session_command_sequence_matches_shared_layer`. The
/// live-process call itself needs a real `tmux` binary, so it is exercised
/// transitively by every migrated call site (daemon `#[ignore]` integration
/// tests; CLI/TUI paths are exercised end-to-end by the existing `tm
/// launch`/`tm connect` integration coverage); the #3386 apply-and-verify
/// behavior itself is covered by `create_managed_session_confirms_server_before_applying_options`
/// and the `apply_and_verify_scrollback_options_*` tests.
/// Number of attempts [`ensure_server_up`] makes before giving up (#3386).
const START_SERVER_MAX_ATTEMPTS: u8 = 3;
/// Delay between [`ensure_server_up`] retry attempts (#3386).
const START_SERVER_RETRY_DELAY: Duration = from_millis;
/// Number of attempts [`apply_and_verify_scrollback_options`] makes before
/// conceding the ergonomics could not be confirmed (#3386 review finding).
const APPLY_VERIFY_MAX_ATTEMPTS: u8 = 3;
/// Delay between [`apply_and_verify_scrollback_options`] retry attempts.
const APPLY_VERIFY_RETRY_DELAY: Duration = from_millis;
/// Confirm the tmux SERVER exists before any `set-option -g`/`new-session`
/// call relies on it (#3386).
///
/// Why: see [`TmuxCommand::StartServer`]'s doc for the full root-cause
/// explanation — `set-option -g` does NOT auto-start the server the way
/// `new-session` does, so a caller must confirm the server is actually up
/// first rather than assuming the first tmux command of the sequence takes
/// care of it.
/// What: runs `tmux start-server` (idempotent against an already-running
/// server) up to [`START_SERVER_MAX_ATTEMPTS`] times, waiting
/// [`START_SERVER_RETRY_DELAY`] between attempts, returning `Ok(())` on the
/// first success. Exhausting every attempt returns an `Err` describing the
/// last failure — this is the "error loudly rather than proceed silently"
/// signal callers must not swallow into an unconditional success path.
/// Test: `ensure_server_up_retries_then_succeeds`,
/// `ensure_server_up_fails_loudly_after_exhausting_retries` (both drive a
/// scripted fake `tmux` binary — no live tmux server required).
///
/// Visibility (#3823): `pub(crate)` (not private) so
/// [`crate::daemon::tmux::TmuxDriver`] can call it directly to guarantee the
/// server exists before the FIRST tmux call of the session-creation/resume
/// flow (`list-sessions`, issued by name-collision checks well before
/// [`create_managed_session`] is ever reached) — not just before this
/// module's own `set-option -g`/`new-session` sequence.
pub
/// Run [`ensure_server_up`], apply every entry of `options`, then confirm
/// via [`probe_history_limit`] and [`probe_alternate_screen`] — retrying the
/// WHOLE cycle up to
/// [`APPLY_VERIFY_MAX_ATTEMPTS`] times (#3386 review finding): a successful
/// `start-server` does NOT by itself prove the FOLLOWING `set-option -g`
/// landed (the server could still be torn down between the two calls, or
/// the value could be clamped/rejected by a tmux version quirk), so the
/// verification probe — not just the server-up check — must gate whether
/// another attempt is worth making.
///
/// Why: [`create_managed_session`] must never silently proceed to
/// `new-session` believing the ergonomics landed when they did not — the
/// original #3386 bug was exactly a `warn!`-and-continue on a single failed
/// attempt.
/// What: returns `true` as soon as one cycle's probes confirm BOTH
/// `history-limit == expected_history_limit` (global scope) and
/// `alternate-screen == expected_alternate_screen` (window scope, #5151);
/// returns `false` once [`APPLY_VERIFY_MAX_ATTEMPTS`] cycles are exhausted
/// without a confirmed match on both. Every failure within a cycle is logged
/// via `warn!`; never panics, never blocks the caller from proceeding to
/// `new-session` either way — the return value is the caller's signal to
/// surface the degraded outcome, not a reason to abort session creation.
///
/// Each probe reads the SAME tmux option scope its `set-option` wrote —
/// `-g` for `history-limit`, `-wg` for `alternate-screen`. A probe that reads
/// a different scope than it wrote would report success for a `set-option`
/// that silently did nothing (measured: `set-option -pg alternate-screen off`
/// exits 0 and leaves the pane still on the alternate screen), which is the
/// same fail-open shape #3386 was filed against.
/// Test: `apply_and_verify_scrollback_options_succeeds_on_second_attempt`,
/// `apply_and_verify_scrollback_options_returns_false_after_exhausting_retries`,
/// `apply_and_verify_scrollback_options_fails_when_only_alternate_screen_is_wrong`,
/// `apply_and_verify_probes_the_same_scope_it_sets` (all drive a scripted
/// fake `tmux` binary — no live tmux server required).
/// Read back the tmux server's current `history-limit` global option
/// (#3386).
///
/// Why: the verification counterpart to [`ensure_server_up`] and the
/// `set-option -g` loop in [`create_managed_session`] — confirms the value
/// actually landed on the server rather than trusting a `set-option -g`
/// exit code alone.
/// What: runs `tmux show-options -g -v history-limit` and parses the single
/// line of stdout as `u32`. Any spawn failure, non-zero exit, or unparsable
/// output is returned as `Err` describing why the probe could not confirm
/// the value — callers decide how loudly to react.
/// Test: `probe_history_limit_reads_back_configured_value`,
/// `probe_history_limit_errors_on_unparsable_output`.
/// Read back the tmux server's current WINDOW-scoped `alternate-screen`
/// option (#5151).
///
/// Why: the verification counterpart to the
/// `set-option -wg alternate-screen` entry
/// [`scrollback_option_commands`](trusty_common::tmux::scrollback_option_commands)
/// emits, and deliberately scope-matched to it. `alternate-screen` is a
/// window/pane option, not a server option like `history-limit`; probing
/// `show-options -g` (or `-pg`) here would read a scope the `set` never
/// wrote, which is how a wrong-scope `set-option` verifies green while the
/// pane still enters the alternate screen.
/// What: runs `tmux show-options -wg -v alternate-screen` and maps the single
/// line of stdout — `on` → `true`, `off` → `false`. Any spawn failure,
/// non-zero exit, or unrecognised value is returned as `Err`; a value tmux
/// does not document (anything but `on`/`off`) is NOT guessed at, because a
/// wrong guess here is a silent pass.
/// Test: `probe_alternate_screen_reads_back_on`,
/// `probe_alternate_screen_reads_back_off`,
/// `probe_alternate_screen_errors_on_unrecognised_value`,
/// `probe_alternate_screen_errors_on_nonzero_exit`.
/// Type `text` into a tmux pane, then press Enter (#2398 consolidation).
///
/// Why: every call site that starts `claude` in a freshly-created pane needs
/// this exact idiom — mirrors
/// [`daemon::tmux::TmuxDriver::send_line`](crate::daemon::tmux::TmuxDriver::send_line)'s
/// literal-then-Enter pattern (two `send-keys` invocations: `-l` literal text,
/// then the `Enter` key name), now shared so CLI/TUI callers do not
/// re-implement it as a single bare `send-keys <text> Enter` invocation
/// (functionally equivalent for a non-key-name string, but a second,
/// independently-typed-out version of the same idiom is exactly the kind of
/// drift #2398 closes).
/// What: runs the literal `send-keys -l <text>` via [`run_tmux_with_bin`]
/// (`tmux_bin` if given, else resolved via [`resolve_tmux_binary_or_bare`]);
/// if that invocation itself fails to spawn, the error is returned
/// immediately WITHOUT sending `Enter`. Otherwise sends `Enter` and returns
/// ITS `Output` — callers check `output.status.success()` exactly as they
/// did with the single-invocation form.
/// Test: argv shapes covered by `trusty_common::tmux`'s
/// `send_keys_literal_argv`/`send_keys_keyname_argv`; the live-process call
/// needs a real `tmux` binary, exercised transitively by every migrated call
/// site.