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
//! Model injection for Claude Code sessions (issue #390).
//!
//! Why: Claude Code silently ignores the `model:` field in agent frontmatter.
//! trusty-mpm must instead build the `claude` CLI invocation with an explicit
//! `--model` flag so the resolved model actually takes effect. This module
//! centralises the command-string construction so every launch path (CLI
//! `tm launch`, `tm session start`, daemon) emits the same correctly-formed
//! command.
//! What: [`build_claude_command`] composes the full shell string passed to
//! `tmux send-keys`; it optionally appends `--model <id>` and
//! `--append-system-prompt-file <path>` flags. [`write_prompt_file`] handles
//! the temp-file side of that second flag.
//!
//! Issue #4467: this module owns THREE launch-line builders, and every one of
//! them must be built here rather than hand-written at a call site. That is not
//! style — `daemon::doctor_transcript_saving`'s `launch_lines()` can only read
//! builders the library owns, so a hand-built line is structurally invisible to
//! the check that exists to catch an unscrubbed spawn. Three separate call sites
//! had hand-built their own and all three silently saved no transcript.
//! [`build_inplace_session_command`] and [`build_client_session_command`]
//! deliberately do NOT carry [`SETTING_SOURCES_FLAG`]: their paths do not
//! relocate `CLAUDE_CONFIG_DIR`, so dropping the `user` tier would change which
//! of the operator's own settings and agents load.
//! Test: `claude_command_bare`, `claude_command_with_model`,
//! `claude_command_with_prompt`, `claude_command_with_both`,
//! `write_prompt_file_returns_path`,
//! `inplace_session_command_scrubs_inherited_session_markers`,
//! `client_session_command_scrubs_inherited_session_markers`.
use ;
use crateMpmConfig;
use crateAgentSummary;
/// Write the session prompt text to a unique temp file.
///
/// Why: `claude --append-system-prompt-file` requires a file path; callers
/// must create that file before spawning `claude`. This helper encapsulates the
/// temp-file creation so every launch path handles it consistently.
/// What: writes `prompt` to `<tmp>/trusty-mpm-system-prompt-<uuid>.txt` and
/// returns the path. Returns `None` and logs a warning on any I/O error.
/// Test: `write_prompt_file_returns_path`.
/// Resolve the model for a PM-session launch (no named agent).
///
/// Why: the top-level `tm launch` / `tm session start` path spawns Claude
/// Code as the PM, not as a named specialist agent. The model resolution still
/// reads from the config (using the special key `"pm"` or the configured
/// `models.default`) so operators can pin the PM tier.
/// What: looks up `config.models.agents["pm"]` first, then falls back to
/// `config.models.default`, then to the compiled-in default (`"sonnet"`).
/// `explicit` (from a `--model` CLI flag) always wins. All values are expanded
/// through [`MpmConfig::expand_model_alias`].
/// Test: `pm_model_resolution`.
/// The setting-sources flag a spawn that does NOT relocate `CLAUDE_CONFIG_DIR`
/// carries.
///
/// Why (issue #1269 / step 4): the operator's global `~/.claude/settings.json`
/// carries hooks (e.g. claude-mpm's) that bleed into — and interfere with —
/// trusty-mpm sessions. `--setting-sources project,local` tells Claude Code to
/// load ONLY the project (tm-owned workspace `.claude/settings.json`) and local
/// settings sources, excluding `user`. This is the correct isolation lever: it
/// does NOT touch `~/.claude.json`, so the ambient OAuth login tm relies on is
/// preserved. It stays the flag for launch paths that do NOT inject their own
/// `CLAUDE_CONFIG_DIR` (`tm launch`, `tm connect`), where the `user` tier still
/// resolves to the operator's real `~/.claude` and must stay excluded.
/// What: the literal flag string appended to those launch commands.
/// Test: `claude_command_includes_setting_sources`.
pub const SETTING_SOURCES_FLAG: &str = "--setting-sources project,local";
/// The setting-sources flag a spawn that DOES relocate `CLAUDE_CONFIG_DIR`
/// carries.
///
/// Why (issue #4451): Claude Code discovers subagents (`agents/*.md`) per
/// settings tier, and `$CLAUDE_CONFIG_DIR/agents` IS the `user` tier — the tier
/// [`SETTING_SOURCES_FLAG`] deliberately drops. Since #4437 moved the bundled
/// roster into the tm-managed `CLAUDE_CONFIG_DIR`, a managed session carrying
/// the `project,local` flag saw ZERO bundled specialists: `Agent type
/// 'rust-engineer' not found`, every delegation degrading to `general-purpose`,
/// while the 42 files sat correctly on disk. Adding `user` back is safe here
/// precisely BECAUSE the spawn relocates `CLAUDE_CONFIG_DIR`: the `user` tier
/// then resolves to the tm-owned config home, not the operator's `~/.claude`,
/// so the #1269 isolation goal (keep claude-mpm's global hooks out) is still met
/// — by the relocation rather than by the exclusion.
///
/// Verified live against `claude` 2.1.220 with
/// `CLAUDE_CONFIG_DIR=~/.trusty-tools/trusty-mpm/claude-config` from a cwd with
/// no `.claude/`: `project,local` resolves 5 built-ins only, while both the
/// flag-less default and `user,project,local` resolve all 42 bundled agents and
/// NONE of the operator's own `~/.claude/agents` entries.
/// What: the literal flag string appended to relocated launch commands.
/// Test: `relocated_setting_sources_flag_loads_the_user_tier`,
/// `setting_sources_flag_picks_by_config_dir`.
pub const SETTING_SOURCES_FLAG_RELOCATED: &str = "--setting-sources user,project,local";
/// Pick the setting-sources flag matching a spawn's `CLAUDE_CONFIG_DIR` posture.
///
/// Why (issue #4451): the two flags are not interchangeable, and picking the
/// wrong one fails silently in opposite directions — `project,local` on a
/// relocated spawn hides the bundled roster, `user,project,local` on a
/// non-relocated spawn re-admits the operator's global hooks (#1269). Deriving
/// the choice from the one input that decides it (does this spawn inject
/// `CLAUDE_CONFIG_DIR`?) removes the chance of a call site getting it wrong.
/// What: [`SETTING_SOURCES_FLAG_RELOCATED`] when `config_dir` is `Some`
/// (the spawn points `CLAUDE_CONFIG_DIR` at a tm-owned home), otherwise
/// [`SETTING_SOURCES_FLAG`].
/// Test: `setting_sources_flag_picks_by_config_dir`.
/// The settings tiers a `--setting-sources <list>` flag actually enumerates.
///
/// Why (issue #4203): a deploy step that writes agents into a tier the spawn's
/// flag does NOT name is a silent no-op — Claude Code never loads them, and
/// nothing reports an error. Parsing the flag instead of hard-coding the tier
/// names keeps that invariant honest if either flag's value ever changes: the
/// tier check and the spawned command can never drift apart.
/// What: splits `flag` at its first space and then the comma-separated tier
/// list, trimming each name.
/// Test: `setting_source_tiers_parses_the_flag`,
/// `relocated_setting_sources_flag_loads_the_user_tier`.
/// The settings tiers [`SETTING_SOURCES_FLAG`] enumerates.
///
/// Why/What: see [`setting_source_tiers_of`]; this is the non-relocated
/// (`tm launch` / `tm connect`) tier list.
/// Test: `setting_source_tiers_parses_the_flag`.
/// The settings tiers [`SETTING_SOURCES_FLAG_RELOCATED`] enumerates.
///
/// Why (issue #4451): `tm doctor` needs this list to assert that the tier the
/// bundled roster deploys into is one a managed session actually loads. Reading
/// it from the flag — rather than re-stating `["user", "project", "local"]` —
/// is what makes the doctor check a real gate instead of a tautology: change
/// either the flag or the deploy destination alone and the check fails.
/// What: see [`setting_source_tiers_of`].
/// Test: `relocated_setting_sources_flag_loads_the_user_tier`.
/// Name the Claude Code settings tier a `.claude/` deploy destination lands in,
/// relative to the cwd the harness is spawned with.
///
/// Why (issue #4203): `FrameworkPaths::default()` deploys agents into
/// `$HOME/.claude` — the `user` tier — while every trusty-mpm spawn carries
/// [`SETTING_SOURCES_FLAG`], which excludes `user`. Deploy and load never
/// intersect. Naming the tier turns that mismatch into something a test can
/// assert on directly, rather than comparing hard-coded path strings that would
/// both pass vacuously if either side moved.
/// What: `"project"` when `claude_home` IS the harness cwd (the payload lands
/// in `<cwd>/.claude`, which both the `project` and `local` tiers read);
/// `"user"` otherwise.
/// Test: `settings_tier_of_names_project_for_the_harness_cwd`,
/// `settings_tier_of_names_user_for_anything_else`.
/// The permission-mode flag every trusty-mpm-spawned `claude` carries.
///
/// Why: tm runs Claude Code in unattended orchestration mode; `--dangerously-skip-permissions`
/// allows the session to operate without any permission prompts, which is required
/// for fully automated multi-agent orchestration where no human is present to approve
/// individual tool calls. This is appropriate because tm session run in provisioned,
/// tm-controlled tmux panes under the operator's explicit supervision.
/// What: the literal flag string appended to every launch command.
/// Test: `claude_command_includes_permission_mode`.
pub const PERMISSION_MODE_FLAG: &str = "--dangerously-skip-permissions";
/// Build the full `claude` command string for `tmux send-keys`.
///
/// Why: the command passed to `tmux send-keys` must be a single shell string;
/// constructing it in one place keeps the CLI `launch`, `session start`, and
/// future daemon-driven paths from drifting apart. It is also the single place
/// the session-isolation flag ([`SETTING_SOURCES_FLAG`]) and the
/// unattended-permission flag ([`PERMISSION_MODE_FLAG`]) are applied so every
/// spawn path stays unattended and isolated (issue #1269).
///
/// Issue #4467: the line is prefixed with `env <-u marker…>` from
/// [`crate::core::claude_env_scrub::env_unset_flags`]. This builder's own doc
/// claimed to be the place "every spawn path stays unattended and isolated", but
/// it emitted a BARE `claude` with no `env` prefix at all — so `tm launch` and
/// `tm connect` launched with the inherited `CLAUDE_CODE_CHILD_SESSION` marker
/// intact and silently saved no transcript.
///
/// Round-2 review correction: earlier wording here (and the doctor's label) also
/// advertised this as the AGENT-DELEGATION launch line via [`build_agent_command`].
/// That was wrong and would have sent an operator chasing a `transcript_saving`
/// failure to a path that does not exist: `build_agent_command` has no production
/// caller anywhere in the workspace — only its own definition and test. It is
/// left in place rather than deleted because `trusty-mpm` is a published crate
/// with no `publish = false`, so removing a `pub fn` is a semver-breaking change
/// that does not belong in a bug-fix PR; removing it is filed as follow-up.
/// There are no `NAME=VALUE` assignments on this prefix (unlike
/// `runtime::claude_code::env_bin_prefix`, this path does not relocate
/// `CLAUDE_CONFIG_DIR`), so the POSIX ordering constraint is satisfied trivially
/// — but the flags still lead the line for consistency with that builder.
///
/// The `env` token becoming the line's PROGRAM is deliberate and safe on the two
/// CLI callers, both of which wrap this output in
/// [`crate::core::spawn_disclaim::disclaim_pane_command`] (#2997). That yields
/// `<tm> internal-spawn-disclaimed env -u … claude …`, so the shim
/// `posix_spawn`s `env` disclaimed and `env` then EXECs `claude` in that same
/// process — the disclaim is a process attribute and survives the exec, and
/// because `env` exec's rather than forks, the process tree
/// `crate::core::process` walks (`pane sh → tm shim → claude`) is unchanged.
/// The daemon path composes the two the other way round (`env … <tm>
/// internal-spawn-disclaimed <abs claude>`); both shapes end with `claude`
/// running as its own responsible process. Note also that the shim resolves its
/// program through `PATH`, and `env` is no less resolvable than the bare
/// `claude` this line used to emit — so this is not a new `PATH` dependency.
/// What: always starts with `env <-u marker…> claude`; appends `--model <model>`
/// when `model` is `Some`; appends `--append-system-prompt-file <path>` when
/// `prompt_file` is `Some`; then ALWAYS appends [`SETTING_SOURCES_FLAG`] and
/// [`PERMISSION_MODE_FLAG`]. Returns the composed string.
/// Test: `claude_command_bare`, `claude_command_with_model`,
/// `claude_command_with_prompt`, `claude_command_with_both`,
/// `claude_command_includes_setting_sources`,
/// `claude_command_includes_permission_mode`,
/// `claude_command_scrubs_inherited_session_markers`.
/// The `claude` launch line for an in-place `tm session start` pane.
///
/// Why (issue #4467, review round 2 HIGH): `bin/tm`'s `start_session_in_place`
/// hand-built `format!("claude {PERMISSION_MODE_FLAG}")` and sent it straight to
/// a fresh tmux pane — a SIXTH interactive launch line with no marker scrub, no
/// test, and no doctor coverage. It is reachable on the documented "just run
/// claude here" case (`tm session start` outside a GitHub-backed git repo), so
/// those sessions silently saved no transcript. Living in the library rather
/// than the binary is the point: `daemon::doctor_transcript_saving`'s
/// `launch_lines()` can only read builders the library owns, so a binary-crate
/// builder is structurally invisible to the check that exists to catch exactly
/// this.
///
/// Deliberately NOT routed through [`build_claude_command`]: that would also add
/// [`SETTING_SOURCES_FLAG`], and `--setting-sources project,local` DROPS the
/// `user` tier. On this path `CLAUDE_CONFIG_DIR` is not relocated, so the user
/// tier is the operator's own `~/.claude` — dropping it would change which
/// settings and agents an in-place session loads. This fix scrubs the markers
/// and changes nothing else.
/// What: `env <-u marker…> claude --dangerously-skip-permissions`. The scrub
/// flags lead the line and there are no `NAME=VALUE` assignments, so POSIX
/// option termination is satisfied trivially.
/// Test: `inplace_session_command_scrubs_inherited_session_markers`,
/// `inplace_session_command_keeps_the_permission_flag_and_nothing_else`.
/// The `claude` launch line the daemon CLIENT sends to a fresh tmux pane.
///
/// Why (issue #4467, found by the round-2 anti-drift scan): `DaemonClient`'s
/// `launch_session` and `connect_session` — the TUI/bot surface behind
/// `/connect <dir>` — each hand-built `format!("claude --append-system-prompt-file
/// {}", …)` with no marker scrub. Two more interactive launch lines that saved no
/// transcript, neither of them named in the review that found the sixth. Sharing
/// one builder is what stops the seventh: a hand-built line in a caller is
/// invisible to `daemon::doctor_transcript_saving`'s `launch_lines()`, which can
/// only read builders.
///
/// Deliberately NOT routed through [`build_claude_command`], for the same reason
/// as [`build_inplace_session_command`]: that would add [`SETTING_SOURCES_FLAG`]
/// and drop the `user` settings tier on a path that does not relocate
/// `CLAUDE_CONFIG_DIR`. This scrubs the markers and changes nothing else.
/// What: `env <-u marker…> claude` plus `--append-system-prompt-file <path>` when
/// `prompt_file` is `Some`. The caller falls back to `None` when writing the
/// prompt file failed, which is why the argument is optional.
/// Test: `client_session_command_scrubs_inherited_session_markers`,
/// `client_session_command_appends_the_prompt_file`.
/// Resolve and build the full `claude` invocation for an agent session.
///
/// Why: agent delegations need the same model-aware command building as PM
/// sessions, but also carry a named agent and a frontmatter model hint.
/// What: calls [`crate::core::config::resolve_agent_model`] for the four-level
/// precedence, then delegates to [`build_claude_command`].
/// Test: `agent_command_uses_config_model`.
// ──────────────────────────────────────────────
// Tests
// ──────────────────────────────────────────────