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
//! Provision the shared tm-owned `CLAUDE_CONFIG_DIR` for daemon-managed sessions
//! (DOC-34 / #1996).
//!
//! Why: the daemon managed-spawn path (`ClaudeCodeAdapter`) previously launched
//! `claude` with NO `CLAUDE_CONFIG_DIR`, so Claude Code read the *target
//! project's* committed `.claude/` — for trusty-tools that is a partial, stale
//! agent roster (missing most specialists, carrying a malformed `rust-engineer`)
//! that shadowed the framework set and forced the general-purpose fallback
//! (#1996). The **standalone** driver already does the right thing via
//! [`super::standalone::global_config::ensure_global_config_dir`]; this module
//! gives the DAEMON path the same isolation primitive, pointed at
//! `~/.trusty-tools/trusty-mpm/claude-config/` (the shared #1220 base) instead of
//! the standalone `~/.trusty-mpm/claude-config/`.
//!
//! CRITICAL: the config dir must carry the **COMPLETE** agent roster (engineer,
//! rust-engineer, research, qa, local-ops, version-control, ticketing,
//! documentation, security + every specialist) and all `tm-*` skills — never a
//! manifest-filtered subset. We therefore deploy from the resolved framework
//! *source* dirs ([`FrameworkPaths::agent_source_dir`] / `skill_source_dir`,
//! submodule-aware — the same source `session_launch::prepare_session` uses)
//! with an unfiltered `deploy_agents` / `deploy_skills`, so the full set lands
//! whether the binary is a dev checkout (git submodule source) or an installed
//! binary (`~/.trusty-mpm/framework/*`).
//!
//! WHICH LAYER ACTUALLY LOADS THE ROSTER (re-corrected 2026-07-31, issue
//! #4451 — this note supersedes the 2026-07-30 #4409 correction it replaces):
//! the AGENTS deployed here are read by a daemon-managed session ONLY because
//! that session's `--setting-sources` list now names the `user` tier.
//!
//! The 07-30 correction concluded from a probe matrix that
//! `$CLAUDE_CONFIG_DIR/agents/` "is discovered", and on that basis #4437 moved
//! the roster here while the daemon spawn still carried `--setting-sources
//! project,local`. That probe matrix ran a fresh headless `claude -p` with NO
//! `--setting-sources` flag, so it measured Claude Code's DEFAULT tier set (all
//! tiers, `user` included) — not the managed spawn's. Re-run against `claude`
//! 2.1.220 from a cwd with no `.claude/`, with the same `CLAUDE_CONFIG_DIR`:
//!
//! no flag -> all 42 bundled agents resolve
//! --setting-sources project,local -> 5 built-ins only, zero bundled
//! --setting-sources user,project,local -> all 42 bundled agents resolve
//!
//! So the ORIGINAL reading — `project,local` excludes the `user` tier
//! `CLAUDE_CONFIG_DIR` relocates — was right about the mechanism; only its
//! conclusion (that the project layer must therefore deliver the roster) is
//! obsolete. The fix keeps the roster here and teaches the spawn to load the
//! tier: see `core::model_inject::SETTING_SOURCES_FLAG_RELOCATED`.
//!
//! `~/.claude/agents/` still goes unread, which is documented behavior — with
//! `CLAUDE_CONFIG_DIR` set, every `~/.claude` path lives under that directory
//! instead. That is also why re-admitting `user` does not re-admit the
//! operator's global hooks (#1269). So this config dir is the ONE tier bundled
//! agents deploy into, on both the daemon and the flag-less standalone `tm run`
//! path, which is why provisioning the complete set here remains mandatory. It
//! also still carries AUTH + TRUST isolation (keychain / `.credentials.json` /
//! `.claude.json` keyed to this path).
//!
//! SKILLS ride the same tier (corrected #4873). The older note here said they
//! "continue to deploy per-workspace via `prepare_session`" and that this
//! module's skill deploy was belt-and-suspenders. That predates
//! `SETTING_SOURCES_FLAG_RELOCATED`: the managed spawn resolves `config_dir`
//! to `Some`, so it launches with `--setting-sources user,project,local` and
//! the `user` tier this directory relocates is read for skills exactly as it
//! is for agents. The deploy below is load-bearing, not a backup — measured
//! 2026-08-05, 49 of 52 skills under `$CLAUDE_CONFIG_DIR/skills/` were
//! byte-identical to the running binary's bundled source, and all three
//! exceptions were checksum-frozen hand edits the deployer is correct to
//! decline.
//!
//! And this tier WINS for skills (#4958). Claude Code resolves skills
//! `enterprise > personal > project > bundled`, and `CLAUDE_CONFIG_DIR`
//! relocates the personal tier — so `$CLAUDE_CONFIG_DIR/skills` outranks the
//! workspace's `<project_dir>/.claude/skills`. AGENTS invert that (project
//! beats user), which is why the same directory needs `--reset-agents` to
//! clear a project-tier shadow but no equivalent for skills. Do not carry
//! either order across to the other artifact type.
//!
//! What: [`ensure_managed_config_dir`] (1) runs the canonical standalone
//! scaffolding ([`super::standalone::global_config::ensure_global_config_dir`]
//! — settings.json + the MPM hook
//! triad + `.mcp.json` + output-styles, plus a best-effort deploy from the
//! `framework/*` dirs), then (2) refreshes the framework source and deploys the
//! FULL agent/skill roster from the resolved source dirs so the complete,
//! spawnable set is guaranteed present. Idempotent — safe to call on every
//! spawn (the deployers checksum-skip unchanged files). Since #4840 the AGENT
//! half of step (2) also REFRESHES the bundled agent source from the
//! compiled-in bundle first, via
//! [`crate::core::agent_source::autodeploy_agents_for`] — before that, only the
//! manual `tm install` ever wrote `~/.trusty-mpm/framework/agents/`, so a
//! merged, compiled-in `BASE-AGENT.md` change silently reached no running
//! agent. Since #4873 the SKILL half warns (once, bounded) about every file it
//! declined to refresh — the deploy itself already ran on all three run paths;
//! only the evidence of it was missing. Since #4880 step (2) also refreshes the
//! session workspace's PROJECT skill tier (`<project_dir>/.claude/skills`),
//! which previously went stale on resume and in-place relaunch; that half is
//! stamp-gated on the project manifest, so
//! it writes nothing when nothing changed. See
//! [`crate::core::project_skill_tier`].
//! Test: `ensure_managed_config_dir_deploys_full_roster`,
//! `ensure_managed_config_dir_is_idempotent`,
//! `ensure_managed_config_dir_refreshes_stale_bundled_agents`,
//! `ensure_managed_config_dir_survives_an_unwritable_agent_target`,
//! `crates/trusty-mpm/src/core/managed_config_tests.rs`.
use Path;
use crateFrameworkPaths;
use cratedeploy_all_skill_tiers;
use crateensure_global_config_dir_with_exe;
use cratepreseed_managed_trust;
/// Provision (idempotently) the tm-owned managed `CLAUDE_CONFIG_DIR`.
///
/// Why: called from the daemon managed-spawn path immediately before the
/// launched `claude` is pointed at `config_dir` via `CLAUDE_CONFIG_DIR`, giving
/// the session an isolated, fully-scaffolded config home (auth, trust, MCP
/// stubs, output-styles) plus the full framework roster/skills. The AGENT
/// roster deployed here is load-bearing on BOTH the daemon and the standalone
/// path (issue #4409 — see the module doc's corrected discovery note), and so
/// is the SKILL deploy since the spawn began passing
/// `--setting-sources user,project,local` (#4873 — the module doc's SKILLS
/// paragraph retires the older "belt-and-suspenders" reading). Runs on every
/// spawn, resume, and in-place relaunch because it is cheap and idempotent,
/// matching the standalone `tm run` contract.
/// What: resolves the framework layout from the daemon's fixed home-relative
/// root ([`FrameworkPaths::default`]), then:
/// 1. calls [`super::standalone::global_config::ensure_global_config_dir`]`(&fw.root, config_dir)` for the shared
/// scaffolding (settings.json, the MPM hook triad, `.mcp.json`, output-styles,
/// credential seed) — this also deploys agents/skills from the `framework/*`
/// dirs, which may be empty on a dev checkout;
/// 2. refreshes the bundled skill source ([`crate::core::skill_source::ensure_skill_source_fresh`],
/// non-fatal) and deploys the COMPLETE, unfiltered agent + skill roster from
/// the resolved source dirs ([`FrameworkPaths::agent_source_dir`] /
/// `skill_source_dir`) into `<config_dir>/agents` and `<config_dir>/skills`,
/// guaranteeing the full spawnable set regardless of install/submodule state.
/// The AGENT half routes through
/// [`crate::core::agent_source::autodeploy_agents_for`] (#4840), which
/// re-materializes the bundled agent source from the compiled-in bundle when
/// its stamp differs, then deploys it — and which never returns an error, so
/// a failed deploy degrades to a warning instead of blocking the spawn.
///
/// Test: `ensure_managed_config_dir_deploys_full_roster`,
/// `ensure_managed_config_dir_is_idempotent`,
/// `ensure_managed_config_dir_refreshes_stale_bundled_agents`.
/// [`ensure_managed_config_dir`] with the hook binary pinned by the caller
/// (#7244 — see [`ensure_managed_config_dir_with_root_and_exe`] for why).
///
/// Test: `prepare_managed_config_writes_no_mcp_json_and_no_approval`.
/// Hermetic core of [`ensure_managed_config_dir`], taking an explicit framework
/// layout so tests can point the source dirs at a temp tree.
///
/// Why: [`FrameworkPaths::default`] reads `dirs::home_dir()`, which unit tests
/// must not depend on; injecting the layout keeps the roster-deploy assertions
/// hermetic and parallel-safe.
/// What: performs the two-phase provisioning described on
/// [`ensure_managed_config_dir`], using `fw` for both the `ensure_global_config_dir`
/// managed-root argument (`fw.root`) and the full-roster source dirs.
///
/// #4873 — WHY THE SKILL HALF ALSO RUNS ON RESUME AND IN-PLACE RELAUNCH.
/// This function is the choke point all THREE run paths share, because each
/// reaches it through `runtime::claude_code::prepare_managed_config`:
/// `ClaudeCodeAdapter::spawn` (fresh), `ClaudeCodeAdapter::spawn_resume`
/// (`daemon::managed_routes::lifecycle::resume_managed`), and
/// `runtime::claude_code::build_inplace_resume_command` (bare `tm` in a
/// managed pane). So the skill deploy below already satisfies the "deploy on
/// every run, not just `tm install`" ruling, exactly as the agent half does —
/// what was missing was any SIGN of it: a skill the deployer declines is
/// silent, which is why three checksum-frozen skills read as "deployment never
/// runs". [`skill_skip_summary`] supplies that sign.
///
/// Test: `ensure_managed_config_dir_deploys_full_roster`,
/// `ensure_managed_config_dir_is_idempotent`,
/// `ensure_managed_config_dir_refreshes_stale_bundled_agents`,
/// `ensure_managed_config_dir_survives_an_unwritable_agent_target`,
/// `ensure_managed_config_dir_refreshes_a_stale_managed_skill`,
/// `ensure_managed_config_dir_skill_deploy_is_a_noop_when_unchanged`,
/// `ensure_managed_config_dir_preserves_a_project_custom_skill`,
/// `ensure_managed_config_dir_skips_a_frozen_skill`,
/// `ensure_managed_config_dir_emits_the_frozen_skill_warning`,
/// `ensure_managed_config_dir_deploys_the_project_skill_tier`,
/// `ensure_managed_config_dir_project_tier_is_a_noop_when_unchanged`,
/// `ensure_managed_config_dir_heals_a_bare_repo_palace_alias`.
///
/// #4880 — WHY THE PROJECT TIER IS DEPLOYED FROM HERE TOO. The user tier this
/// function refreshes every run is OUTRANKED by `<project_dir>/.claude/skills`,
/// which until now was written only by `session_launch::prepare_session` and
/// `tm sessions sync-assets` — neither of which runs on resume or in-place
/// relaunch. Hanging the project-tier trigger off this same choke point is what
/// makes it reach all three run paths; see
/// [`crate::core::project_skill_tier`] for the stamp that keeps it a no-op when
/// the project manifest has not moved.
/// [`ensure_managed_config_dir_with_root`] with the hook binary pinned by the
/// caller.
///
/// Why (#7244): phase 1 writes the managed hook triad, and that write refuses a
/// build-artifact binary. A test process IS one, and a CI runner has no
/// installed `tm` for the PATH fallback, so every roster/skill assertion in this
/// module aborted on the refusal before reaching what it was written to check.
/// Pinning the path is the same seam `ensure_managed_hooks_with_exe` already
/// offers, carried one layer up so the tests can reach it.
/// What: forwards `exe_override` to
/// [`super::standalone::global_config::ensure_global_config_dir_with_exe`], then
/// runs the phases [`ensure_managed_config_dir_with_root`] documents unchanged.
/// Test: `ensure_managed_config_dir_is_idempotent`,
/// `ensure_managed_config_dir_deploys_full_roster`.
/// One bounded warning line for the skills a deploy declined to write, or
/// `None` when it declined nothing.
///
/// Why: this closes for SKILLS the half of #4840 that PR #4848 closed for
/// agents. `deploy_one_file` skips a checksum-frozen (hand-edited) or
/// unmanaged skill and reports it only in the returned `DeployStats`, which
/// every caller here discarded — so a skill frozen against the manifest stayed
/// stale on every run, forever, with no warning anywhere. That silence is what
/// #4873 was filed as: three frozen skills read as "skill deployment never
/// runs on resume", when deployment had in fact run and correctly declined
/// them. The skip itself is CORRECT and is deliberately left alone — a hand
/// edit must survive, and `tm doctor --fix-skills --include-frozen` is the
/// remedy the pointer names.
/// What: a count plus a five-entry preview and that remedy pointer. Matches
/// [`crate::core::agent_source::deploy_summary_lines`]'s policy (issue #2504:
/// count + preview, never one line per file) because this runs on EVERY spawn,
/// resume, and in-place relaunch and the frozen set is not reconciled until
/// someone runs the doctor — one line per file would be permanent log spam.
/// Pure — no I/O, no logging.
/// Test: `skill_skip_summary_is_none_on_a_clean_deploy`,
/// `skill_skip_summary_counts_and_previews`,
/// `skill_skip_summary_elides_beyond_five`,
/// `a_declined_skill_reaches_skill_skip_summary_from_a_real_deploy`,
/// `ensure_managed_config_dir_emits_the_frozen_skill_warning` (the call site).
/// Resolve, provision, and trust-seed the tm-owned `CLAUDE_CONFIG_DIR` for an
/// INTERACTIVE launch (`tm launch`, `tm connect`).
///
/// Why (issue #4181, ADR-0042 decision 6): both commands spawned `claude` with
/// no `CLAUDE_CONFIG_DIR` at all and `--setting-sources project,local`. Under
/// ADR-0042 the MCP declaration lives once in the user tier of
/// `<CLAUDE_CONFIG_DIR>/.claude.json`, and a session that neither relocates nor
/// loads `user` reads no MCP servers whatsoever. Relocating is what lets the
/// `user` tier be loaded WITHOUT re-admitting the operator's global
/// `~/.claude/settings.json` hooks — #1269's isolation guarantee is then carried
/// by the relocation instead of by the exclusion, exactly as it already is on
/// the daemon path. The rejected alternative was adding `user` to
/// `--setting-sources` while still reading the operator's real `~/.claude`,
/// which drops that guarantee outright.
///
/// This is the interactive counterpart of
/// `runtime::claude_code::prepare_managed_config` and deliberately mirrors it
/// step for step rather than growing a second mechanism: resolve, provision
/// (non-fatal), seed trust into `<config_dir>/.claude.json` (NEVER
/// `~/.claude.json`).
///
/// // #4181 (ADR-0042): the four `_pinned` parameters are gone with the
/// `enabledMcpjsonServers` approval they gated. So is the derivation difference
/// this doc used to warn about — that this path applied no `project_scope_mcp_names`
/// subtraction (#2739) and so pre-approved a repo `[mcp.custom]` name that
/// collided with an operator registry name. Nothing is pre-approved now, and
/// #7894 removed the `[mcp.custom]` table itself.
/// What: `Some(dir)` when
/// [`crate::core::trusty_tools_config::managed_claude_config_dir`] resolves —
/// having provisioned it via [`ensure_managed_config_dir`] and seeded trust via
/// [`preseed_managed_trust`], both non-fatal so a failure warns and the session
/// still launches under the relocated dir (strictly safer than silently falling
/// back to the operator's `~/.claude`). `None` when the home is unresolvable (a
/// stripped environment): falls back to
/// [`crate::core::home_trust_seed::preseed_home_trust`] and the caller keeps
/// `SETTING_SOURCES_FLAG`'s `user`-excluding posture.
/// Test: `interactive_config_dir_seeds_trust_in_the_managed_dir`,
/// `interactive_config_dir_never_writes_the_home_claude_json`,
/// `interactive_config_dir_survives_a_malformed_managed_claude_json`,
/// `interactive_config_dir_withholds_builtins_when_a_pin_failed`.
/// [`prepare_interactive_config_dir`] with the user home named (#8545).
///
/// Why: the `guided_fallback_*` tests drive the real launch path; a named home
/// keeps the managed config dir and its framework source under a temp dir.
/// What: the same steps, with `<home>/.trusty-tools/trusty-mpm/claude-config`
/// as the config dir; `None` for `home` takes the unresolved-home arm.
/// Test: `guided_fallback_prepares_the_session_in_the_worktree_not_the_base_clone`.
/// Hermetic core of [`prepare_interactive_config_dir`]: provision `config_dir`
/// and seed `workspace`'s trust into it.
///
/// Why: split out so tests drive a tempdir framework root and config dir instead
/// of the operator's real `$HOME` — the same hermetic-core split
/// [`ensure_managed_config_dir_with_root`] already uses. Keeping the resolution
/// in the wrapper is also what makes the "never writes `~/.claude.json`"
/// invariant testable at all.
/// What: [`ensure_managed_config_dir_with_root`] then [`preseed_managed_trust`],
/// each non-fatal — a failure of either warns and returns normally, because a
/// partially-provisioned relocated config home is still safer for the session
/// than not relocating.
/// Test: see [`prepare_interactive_config_dir`].