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
//! Multi-style output support with version-fallback injection (HR-4).
//!
//! Why: trusty-mpm bundles three Claude Code output styles — professional
//! (`trusty-mpm`), teaching (`trusty-mpm-teacher`), and research
//! (`trusty-mpm-research`). The operator picks the active one via config
//! (`[style] active = …`) or the launch `--style` flag. Modern Claude Code
//! (≥ 1.0.83) honours a native `"outputStyle"` key in `.claude/settings.json`,
//! but older builds ignore it; for those, trusty-mpm must INJECT the active
//! style's content into the PM system prompt instead so the active style is in
//! effect regardless of Claude Code version (DOC-17 §HR-4).
//! What: a registry resolver over [`crate::core::bundle::OUTPUT_STYLES`]
//! ([`resolve_style`], [`resolve_active_style`]), a `[style]` config section
//! ([`StyleConfig`]), Claude Code version detection
//! ([`claude_supports_native_output_style`], [`version_supports_native`]), and
//! prompt injection ([`inject_style_into_prompt`]).
//! Test: `output_style_tests.rs` covers registry resolution (all three ids +
//! unknown-id error), config selection, the version gate (mocked version
//! strings), and prompt injection.
use Path;
use crate;
use crateMpmConfig;
use crateFrameworkContent;
/// The first Claude Code version that honours the native `outputStyle` settings
/// key, as `(major, minor, patch)`.
///
/// Why: claude-mpm established `1.0.83` as the floor for native output-style
/// support; trusty-mpm mirrors it so the version gate matches upstream behaviour.
/// What: the inclusive lower bound — a detected version `>=` this supports native
/// output styles.
/// Test: `version_gate_*` in `output_style_tests.rs`.
pub const NATIVE_OUTPUT_STYLE_MIN_VERSION: = ;
/// How long [`claude_supports_native_output_style`] waits for `claude
/// --version` before giving up on it.
///
/// Why: the probe sits on the session-launch and prompt-composition paths, and
/// a wedged `claude` binary used to hang them forever (#5969). `claude
/// --version` answers in well under a second on a healthy host, so five
/// seconds is generous for the answer and short enough that a wedged binary
/// costs a launch five seconds rather than the whole session.
/// What: the deadline handed to
/// [`crate::core::spawn_disclaim::disclaimed_stdout_with_timeout`]; past it the
/// probe child is killed and the probe fails safe to "no native support".
/// Test: `spawn_disclaim::timeout`'s
/// `disclaimed_stdout_with_timeout_kills_and_reaps_wedged_child`.
pub const CLAUDE_VERSION_PROBE_TIMEOUT: Duration = from_secs;
/// A failure resolving a requested output-style id.
///
/// Why: an unknown style id is a user-facing error (a typo in config or the
/// `--style` flag); callers need a typed surface that names the bad id and the
/// valid set so the message is actionable.
/// What: a single variant carrying the requested id.
/// Test: `unknown_style_id_errors` in `output_style_tests.rs`.
/// Resolve a style id to its bundled entry, erroring on an unknown id.
///
/// Why: the launch path must validate a configured/selected id against the
/// bundle before writing it into `.claude/settings.json` — Claude Code silently
/// falls back to the operator's default on a name mismatch, so a typo would
/// otherwise produce a confusing "style not applied" with no error.
/// What: linear-scans [`OUTPUT_STYLES`] for `id`; returns the matching
/// [`BundledStyle`] or [`StyleError::Unknown`] listing the valid ids.
/// Test: `resolve_style_resolves_all_three_ids`, `unknown_style_id_errors`.
/// Comma-separated list of bundled style ids, in registry order.
///
/// Why: used in the [`StyleError::Unknown`] message and exposed so the CLI can
/// list the choices for `--style`.
/// What: joins every [`BundledStyle::id`] with `", "`.
/// Test: `valid_style_ids_lists_all`.
/// `[style]` section of `~/.trusty-mpm/config.toml`.
///
/// Why: HR-4 makes the active output style configurable; this section records
/// the operator's choice so every launch picks it up without a flag.
/// What: an optional `active` id. `None` (absent section/key) means "use the
/// professional default" ([`DEFAULT_OUTPUT_STYLE_ID`]).
/// Test: `config_style_section_parses`, `active_style_id_defaults_when_unset`.
/// Determine the effective active style id from config + an optional override.
///
/// Why: the active style can come from the launch `--style` flag (highest
/// precedence) or the `[style] active` config key, falling back to the
/// professional default; centralizing the precedence keeps the CLI and the
/// client launch paths consistent.
/// What: returns the `explicit` override if `Some`, else `config.style.active`
/// if set, else [`DEFAULT_OUTPUT_STYLE_ID`]. Does NOT validate the id — callers
/// validate via [`resolve_style`] (or [`resolve_active_style`]) so the
/// unknown-id error surfaces once, with full context.
/// Test: `active_style_id_precedence`.
/// Resolve the active style to a bundled entry, applying config + override.
///
/// Why: the launch path needs the actual [`BundledStyle`] (id + content) for the
/// active selection, validated; this combines [`active_style_id`] with
/// [`resolve_style`].
/// What: computes the active id via [`active_style_id`], then resolves it.
/// Returns [`StyleError::Unknown`] when the selected id is not bundled.
/// Test: `resolve_active_style_uses_config`, `resolve_active_style_unknown_errors`.
/// Parse a `claude --version` output line into `(major, minor, patch)`.
///
/// Why: the version gate must turn the raw CLI output (e.g.
/// `"1.0.84 (Claude Code)"`) into a comparable tuple; isolating the parse makes
/// it testable without spawning the binary.
/// What: extracts the first `\d+.\d+.\d+` triple from `raw`. Returns `None` when
/// no such triple is present (so callers can treat unparseable output as "assume
/// no native support" to stay safe).
/// Test: `parse_version_*`.
/// Parse a single token as a `major.minor.patch` triple.
///
/// Why: helper for [`parse_claude_version`]; a token may carry a pre-release or
/// build suffix (`1.0.84-beta`), so we split on the first non-digit after the
/// patch number.
/// What: splits `token` on `.`; requires at least three leading numeric
/// components; ignores any suffix on the patch component.
/// Test: covered via `parse_version_*`.
/// Decide whether a detected version supports the native `outputStyle` key.
///
/// Why: the launch path branches on this — native key vs. prompt injection.
/// What: returns `true` when `version >= ` [`NATIVE_OUTPUT_STYLE_MIN_VERSION`].
/// Test: `version_gate_supported`, `version_gate_unsupported`,
/// `version_gate_exact_floor`.
/// Detect whether the installed Claude Code supports native output styles.
///
/// Why: when the installed CLI is too old (or undetectable) trusty-mpm must
/// inject the style into the PM prompt instead of relying on the settings key.
/// What: runs `claude --version`, parses the first semver triple, and compares
/// it to [`NATIVE_OUTPUT_STYLE_MIN_VERSION`]. **Fails safe**: any error (binary
/// missing, non-zero exit, unparseable output, or a `claude` that never exits)
/// is logged to stderr and treated as "does NOT support native" so the style is
/// always applied via injection. All logging goes to stderr (the daemon/MCP
/// stdout-clean rule).
/// Test: pure logic is in [`version_supports_native`] /
/// [`detect_native_support_from_output`]; this thin wrapper is side-effecting
/// (spawns a process) and is covered by `detect_from_output_*`. The bounded
/// wait itself is covered by `spawn_disclaim::timeout`'s tests.
/// Map a `claude --version` output string to native-support, failing safe.
///
/// Why: separating the string→bool decision from the process spawn makes the
/// version gate fully unit-testable with mocked version strings (the HR-4
/// acceptance criterion).
/// What: parses `raw` via [`parse_claude_version`]; an unparseable string yields
/// `false` (assume no native support → inject). A parsed version defers to
/// [`version_supports_native`].
/// Test: `detect_from_output_modern`, `detect_from_output_old`,
/// `detect_from_output_unparseable`.
/// Strip a leading YAML frontmatter block (`--- … ---`) from style content.
///
/// Why: when injecting a style into the PM system prompt, the
/// `name:`/`description:` frontmatter is Claude-Code settings metadata, not
/// instructions for the model; stripping it keeps the injected text clean.
/// What: if `content` starts with a `---` line, drops everything through the
/// closing `---` line; otherwise returns `content` unchanged (trimmed).
/// Test: `strip_frontmatter_removes_block`, `strip_frontmatter_passthrough`.
/// Header that delimits an injected output-style block in the PM prompt.
///
/// Why: a clearly-labelled fence tells the launched PM (and a human reading the
/// stashed prompt) that the block is the active output style, injected because
/// the installed Claude Code lacks native support.
/// What: the Markdown heading prepended to the injected style body.
/// Test: `inject_prepends_style_block`.
pub const INJECTED_STYLE_HEADING: &str =
"# Active Output Style (injected — Claude Code lacks native outputStyle support)";
/// Prepend an output style's content to a PM system prompt.
///
/// Why: on older Claude Code the `"outputStyle"` settings key is ignored, so the
/// only way to make the active style take effect is to fold its instructions
/// into the `--append-system-prompt` text. claude-mpm does the same.
/// What: returns a new string of `INJECTED_STYLE_HEADING`, then the style body
/// (frontmatter stripped), then the framework section separator, then the
/// original `prompt`. The original prompt is preserved verbatim so the PM floor
/// and overrides are untouched — the style is purely additive and placed first.
/// Test: `inject_prepends_style_block`, `inject_preserves_prompt`.
/// [`inject_style_into_prompt`] for a style's raw text, bundled or project (#8533).
///
/// Test: `inject_prepends_style_block`,
/// `a_project_style_keeps_the_floor_with_and_without_native_support`.
/// `body` (frontmatter already stripped) under [`INJECTED_STYLE_HEADING`], then `prompt`.
/// Resolve the active style and inject it into `prompt` only when the installed
/// Claude Code lacks native output-style support.
///
/// Why: this is the single launch-time seam (`build_system_prompt_for` calls it)
/// that makes the active style effective regardless of Claude Code version: on a
/// modern CLI the prompt is returned unchanged (native key handles the style); on
/// an old/undetectable CLI the style is injected. Keeping it here means both the
/// CLI and client launch paths get identical behaviour.
/// What: if [`claude_supports_native_output_style`] is `true`, returns `prompt`
/// unchanged. Otherwise resolves the active style (config + `explicit`
/// override); on success injects it via [`inject_style_into_prompt`]; on an
/// unknown id logs a warning to stderr and falls back to the professional
/// default style (per DOC-17: unknown style → `trusty-mpm` default). When the
/// home dir / config is unavailable the caller passes [`MpmConfig::default`].
/// Test: `maybe_inject_*` in `output_style_tests.rs` drive the gate via the
/// injected `native_supported` flag.
/// Convenience: resolve the active style id for a project, reading the user
/// config and gating on the live Claude Code version.
///
/// Why: the launch sites want a one-call entry point that does the real work
/// (load config, detect version, inject) without each re-implementing the
/// precedence; this wraps [`maybe_inject_active_style`] with the real config and
/// live version detection.
/// What: loads [`MpmConfig::load_default`], detects native support via
/// [`claude_supports_native_output_style`], and injects (or not) into `prompt`.
/// `_project_dir` is accepted for symmetry with the other launch helpers and to
/// allow future project-scoped style overrides; it is currently unused.
/// Test: side-effecting (loads real config, spawns `claude`); the pure core is
/// covered by `maybe_inject_*`.
/// Apply the output-style injection decision with the `native_supported` flag
/// supplied explicitly (no live `claude --version` probe).
///
/// Why: [`apply_output_style_to_prompt`] spawns `claude --version`, which makes
/// every caller (and every test that exercises a launch-prompt path) depend on
/// whether `claude` is installed on the host — the exact host-dependence that
/// broke `prepare_session_stash_reflects_override` on CI (issue #1409). This
/// seam lets tests pin the decision BOTH ways while production still does real
/// version detection (and fails safe to injection) via the wrapper above.
/// What: selects the style [`select_style_under`] picks for the default
/// framework root. When `native_supported`, Claude Code delivers the style
/// file itself, so `prompt` is returned unchanged — preceded by the
/// [`style_floor`] for a project style whose composite is not active
/// ([`composite_is_active`]). Otherwise the style's
/// [`delivered_style_text`] (a project style's prose plus the floor) is
/// injected ahead of `prompt`.
/// #8533: the style is chosen by [`effective_style_id`] and resolved
/// against the project's own style files too, so a project style reaches an
/// older Claude Code; an unknown id warns and injects the default.
/// Test: `a_project_style_keeps_the_floor_with_and_without_native_support`,
/// `a_bundled_style_gets_no_second_floor_on_either_path`; the
/// stash/launch invariant under both flag values is covered by
/// `prepare_session_stash_reflects_override` in `session_launch/tests.rs`.
/// [`apply_output_style_to_prompt_with_native`] for an already-resolved
/// profile (#8453): the launch passes the one value it resolved.
/// The injection decision for a selected style; see
/// [`apply_output_style_to_prompt_with_native`].
// #8533: the floor appended to a project style (owner ruling 2026-09-25).
pub use ;
// #8533: project-local styles live in a child module (SLOC headroom).
pub use ;