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
//! Composes the PM system prompt, and detects retired override files.
//!
//! Why: the project's customization surface is named sections in its root
//! `CLAUDE.md`, and nothing else (#4286). Five files under
//! `<project>/.trusty-mpm/` used to be a second, higher-precedence surface with
//! whole-document granularity. They are RETIRED: not read, not honoured. The
//! retirement had to be loud rather than silent — a project whose rules stop
//! reaching the PM with no error is the worst outcome available — so this module
//! also owns the detector both halves of that signal share.
//!
//! What: [`resolve_pm_prompt`] composes the effective PM prompt. It scans
//! `CLAUDE.md` for named-section overrides via
//! [`crate::core::claude_md_sections`], folds in the auto-derived stack profile
//! and the live agent roster, and *always* appends the non-overridable framework
//! floor last. Two composers exist and the roster alone selects between them: with
//! a roster the typed [`crate::core::instruction_package::InstructionPackage`]
//! composes every section; with none (no agent deployed in any tier) it degrades
//! to [`assemble_sections`], where only `WORKFLOW`, `MEMORY` and
//! `AGENT-DELEGATION` are independently addressable and any other named override
//! is reported by `warn_unapplied` rather than dropped in silence.
//!
//! [`detect_legacy_overrides`] reports leftover retired files;
//! [`warn_legacy_overrides`] logs them on every resolution, and the
//! `legacy_overrides` `tm doctor` check fails on them.
//!
//! Naming note: `base_pm()` and the `# Framework Instructions` heading are
//! historical labels. `BASE_PM.md` the FILE was deleted by #4183 and
//! `scripts/check_instruction_floor.sh` fails the build if it returns; the floor
//! is authored as four `fixed`-tier sections and reconstituted by that function.
//!
//! Test: the `tests` module inverts one gate per retired file (a retired file
//! must change nothing), pins the survival set across every reachable
//! configuration, and covers the detector; `claude_md_sections_tests.rs` covers
//! the named-section wiring end to end.
use Path;
use crateProjectOverrides;
use crateSectionId;
use crate;
/// Directory under the project root that holds the override files.
///
/// Why: the override files live alongside the inspectable instruction stash
/// (`last-instructions.md`) that `prepare_session` already writes, so they share
/// the project-local `.trusty-mpm/` directory documented in `BASE_PM.md`.
/// What: the literal directory name, joined onto the project root.
/// Test: every `resolve_*` test writes files under `<project>/.trusty-mpm/`.
pub const OVERRIDE_DIR_NAME: &str = ".trusty-mpm";
// RETIRED OVERRIDE FILE NAMES (#4286).
//
// These five names are no longer an override surface. Nothing below reads them
// as instruction content any more — they are retained ONLY so the retirement is
// DETECTABLE: [`detect_legacy_overrides`] reports a leftover file, and both the
// prompt resolver and the `legacy_overrides` doctor check turn that report into
// a loud, actionable signal. Deleting the constants would make a leftover file
// silent, which is the one outcome the retirement may not produce.
/// Retired: formerly a full replacement of the PM instruction body.
pub const FILE_PM_DEPLOYED: &str = "PM_INSTRUCTIONS_DEPLOYED.md";
/// Retired: formerly replaced the bundled `AGENT_DELEGATION` section.
pub const FILE_AGENT_DELEGATION: &str = "AGENT_DELEGATION.md";
/// Retired: formerly replaced the bundled `WORKFLOW` section.
pub const FILE_WORKFLOW: &str = "WORKFLOW.md";
/// Retired: formerly replaced the memory guidance section.
pub const FILE_MEMORY: &str = "MEMORY.md";
/// Retired: formerly appended (additive) project rules.
pub const FILE_INSTRUCTIONS: &str = "INSTRUCTIONS.md";
/// Every retired override file name, in former precedence order.
///
/// Why: the detector and the doctor check must agree on the set exactly. Two
/// hand-maintained lists would drift, and a name missing from one of them is a
/// file that gets silently ignored — the failure mode #4286 exists to end.
/// What: the five names, joined onto `<project>/.trusty-mpm/`.
/// Test: `legacy_override_files_are_the_five_retired_names`.
pub const LEGACY_OVERRIDE_FILES: = ;
/// The migration sentence every leftover-file signal must carry.
///
/// Why: a warning that says only "this file is ignored" leaves the operator
/// with a dead file and no next step. One shared constant keeps the resolver's
/// log line and the doctor check's message from drifting into two different
/// pieces of advice.
/// What: the one-line migration, naming `CLAUDE.md` and the marker grammar.
/// Test: `legacy_file_signal_names_the_migration`,
/// `legacy_overrides_fail_names_the_migration`.
pub const LEGACY_MIGRATION_HINT: &str = "move project facts into CLAUDE.md as plain prose, and section overrides into a \
`<!-- TRUSTY-MPM: <SECTION> START v=1 -->` … `<!-- TRUSTY-MPM: <SECTION> END -->` \
block in CLAUDE.md, then delete the file (#4286)";
/// Report which retired override files a project still carries.
///
/// Why: the hard cut (#4286) stops READING these files, and a silent stop would
/// delete a project's rules with no error — the unacceptable outcome. Detection
/// is therefore a first-class, testable function rather than an incidental log
/// line, and it is shared by the resolver and by `tm doctor` so the two can
/// never disagree about whether a project is affected.
/// What: the existing `<project>/.trusty-mpm/<name>` paths, in
/// [`LEGACY_OVERRIDE_FILES`] order. A missing `.trusty-mpm/` directory yields an
/// empty vector. Presence only — the contents are never read.
/// Test: `no_legacy_files_detected_in_a_clean_project`,
/// `every_retired_file_is_detected`.
/// Emit the resolver-side loud signal for any leftover retired override file.
///
/// Why: `tm doctor` only fires when someone runs it. The prompt resolver runs on
/// EVERY session launch and on every `tm session instructions`, so it is the one
/// place guaranteed to observe the condition at the moment it matters. `error!`
/// rather than `warn!` deliberately: the project's instructions are no longer
/// reaching the PM, which is a broken configuration, not a style note.
/// What: one `tracing::error!` per leftover file, naming the path and
/// [`LEGACY_MIGRATION_HINT`]. Silent when the project is clean.
/// Test: `legacy_file_signal_names_the_migration`.
/// Heading that delimits a `MEMORY.md` override block.
///
/// Why: there is no standalone bundled "memory" asset — the memory guidance is
/// the "Context-First Protocol (MANDATORY)" subsection inside `PM_INSTRUCTIONS`.
/// Rather than fragile surgical excision of that subsection, a `MEMORY.md`
/// override is slotted in as a clearly-delimited replacement block placed
/// immediately after `PM_INSTRUCTIONS` (and before `WORKFLOW`). The heading
/// makes it unambiguous to a reader — and to the launched PM — that the project
/// has overridden memory behaviour.
/// What: the Markdown heading prepended to the `MEMORY.md` body when slotted.
/// Test: `a_named_memory_override_is_slotted_on_the_roster_absent_path`.
const MEMORY_OVERRIDE_HEADING: &str = "## Memory Behavior (project override)";
/// Resolve the effective PM prompt for `project_dir`, applying any overrides.
///
/// Why: this is the single source of truth #381 requires — both the live prompt
/// delivered to `claude` (`--append-system-prompt-file`) and the inspectable
/// stash (`tm session instructions` / `.trusty-mpm/last-instructions.md`) call
/// it, so the inspectable copy can never diverge from what the PM actually
/// received (the #382 concern).
///
/// What: reads override files from `<project_dir>/.trusty-mpm/` and assembles
/// the prompt per the `BASE_PM.md` "Customizing PM Behavior" table. Branch (1):
/// when `PM_INSTRUCTIONS_DEPLOYED.md` is present the body is *its* contents
/// (full replacement of PM_INSTRUCTIONS + WORKFLOW + AGENT_DELEGATION + MEMORY),
/// then `INSTRUCTIONS.md` (if present) is appended — **SHORT-CIRCUIT**. Branch
/// (2): otherwise PM_INSTRUCTIONS (bundled) → optional MEMORY override block →
/// WORKFLOW (override or bundled) → AGENT_DELEGATION (override or bundled), then
/// `INSTRUCTIONS.md` (if present) appended. In **both** branches the
/// non-overridable `BASE_PM.md` floor is appended **last** — it is never
/// replaceable, preserving the framework-floor guarantee `BASE_PM.md` itself
/// states. Sections are joined with [`SECTION_SEPARATOR`].
///
/// #4399: within branch (2), "WORKFLOW (override or bundled)" and its two
/// siblings each additionally fall back to a CLAUDE.md named-section override
/// for that SAME section before reaching the bundled default — so a legacy
/// file that forces this branch for one section (say `AGENT_DELEGATION.md`)
/// can no longer shadow a CLAUDE.md override authored for a DIFFERENT,
/// unrelated section (say `WORKFLOW`). A legacy file still wins over a named
/// override for its OWN section — that collision is deliberate and unchanged.
///
/// Per-project stack profile: in **both** branches an auto-derived
/// [`crate::core::stack_profile::stack_profile_section`] block is folded in right
/// after the PM body. It states the project's detected stack (or a neutral
/// detect-first profile when nothing is detected) so the PM is primed from the
/// project's real stack, never a hardcoded default (#1971). It is framework
/// context, not a user override, so it is not replaceable.
///
/// Robustness: a missing `.trusty-mpm/` directory, missing files, empty files,
/// and unreadable files all fall back to the bundled defaults without failing.
///
/// MEMORY.md slotting: there is no standalone bundled memory asset (the memory
/// guidance lives inside `PM_INSTRUCTIONS`). A `MEMORY.md` override is therefore
/// slotted as a clearly-delimited [`MEMORY_OVERRIDE_HEADING`] block placed
/// immediately after `PM_INSTRUCTIONS`, which the launched PM reads as the
/// authoritative, later-and-more-specific memory instruction.
///
/// Delegation roster: the DEFAULT delegation section is composed by
/// [`delegation_with_roster`], which appends the LIVE deployed-agent roster to
/// the bundled asset (#4069). Like the stack profile it is auto-derived framework
/// context — but unlike it, it lives inside the overridable delegation section,
/// so an `AGENT_DELEGATION.md` override still replaces section and roster alike.
///
/// Composition source (#4183): the BUNDLED-FALLBACK configuration — no
/// delegation, workflow or memory override, and a roster present — is composed
/// through [`crate::core::bundled_pm_package`] and its typed
/// [`crate::core::instruction_package::InstructionPackage`], byte-identically to
/// the legacy assembly below. Every other configuration is deliberately still
/// assembled by [`assemble_sections`]; see the branch comment for why.
///
/// Test: `no_overrides_uses_bundled`,
/// `a_retired_instructions_file_no_longer_reaches_the_prompt`,
/// `a_retired_workflow_file_no_longer_replaces_the_workflow_section`,
/// `a_retired_delegation_file_no_longer_suppresses_the_live_roster`,
/// `bundled_delegation_appends_deployed_roster`,
/// `a_retired_deployed_file_no_longer_replaces_the_body`,
/// `a_named_memory_override_is_slotted_on_the_roster_absent_path`,
/// `stack_profile_present_when_detected`, `stack_profile_neutral_when_undetected`,
/// the robustness tests, and (in `claude_md_sections_tests.rs`)
/// `a_legacy_file_cannot_shadow_a_named_override_because_it_is_not_read`.
/// Which composer produced a resolved prompt.
///
/// Why: the two composers are byte-identical by contract (#4183), so the
/// delivered string cannot tell you which one ran. That is exactly the property
/// that let a call-site mutation survive a green suite — a test asserting
/// byte-equality through [`resolve_pm_prompt`] would keep passing even if the
/// package branch were deleted outright. Reporting the source turns "the
/// composed path was actually taken" into an assertable fact, and gives the
/// operator a log line saying which model produced their prompt.
/// What: [`PromptSource::Package`] for the re-sourced bundled fallback,
/// [`PromptSource::Legacy`] for the two override configurations and for the
/// compose-error degradation.
/// Test: `resolve_pm_prompt_takes_the_package_path_when_a_roster_is_deployed`,
/// `the_roster_alone_selects_the_composer`, `no_retired_file_can_divert_the_composer`.
pub
/// [`resolve_pm_prompt`], additionally reporting which composer ran.
///
/// Why: see [`PromptSource`] — byte-identical outputs make the path
/// unobservable from the result alone, so the seam is what keeps the acceptance
/// gate anchored to the branch it claims to cover.
/// What: the resolved prompt plus its source, with the deployed-agent roster
/// scanned from the real tiers.
/// Test: `resolve_pm_prompt_takes_the_package_path_when_a_roster_is_deployed`,
/// `the_roster_alone_selects_the_composer`, `no_retired_file_can_divert_the_composer`.
pub
/// [`resolve_pm_prompt_with_source`] with the agent roster supplied by the
/// caller.
///
/// Why: the roster is the one composition input this function cannot control
/// and cannot re-derive. [`crate::core::delegation_authority::deployed_roster_section`]
/// unions three tiers — the project tier, `$CLAUDE_CONFIG_DIR/agents` and
/// `~/.claude/agents` — on EVERY call, and the latter two are machine-global
/// mutable state that live `tm` sessions rewrite at launch; `scan_agents` also
/// drops a file silently on a transient read failure. Two successive scans can
/// therefore disagree. A byte-equality test that scans once per side is
/// consequently racing, and it fails with a message indistinguishable from a
/// genuine delivered-prompt regression — observed at ~1 red in 4 full-suite
/// runs on a provisioned workstation, and invisible in CI, where no ambient
/// tier exists. Injecting the roster gives both sides of that comparison ONE
/// value without touching `$HOME` (which would cost the serial `HOME_LOCK`) and
/// without altering the wiring under test.
///
/// What: identical to [`resolve_pm_prompt_with_source`] except that
/// `roster_source` supplies the rendered `## Delegation Authority` block. It is
/// a closure, not a value, so it stays LAZY: an `AGENT_DELEGATION.md` override
/// short-circuits before any scan, exactly as before.
/// Test: `resolve_pm_prompt_is_byte_identical_to_the_legacy_assembly`,
/// `a_retired_instructions_file_cannot_feed_the_project_addendum`,
/// `gate_is_deterministic_across_repeated_runs`.
pub
/// The legacy section-by-section assembly (branch 2).
///
/// Why: extracted so the configurations #4183 leaves on the legacy path share
/// one implementation with the byte-equality oracle the package path is tested
/// against — an oracle that is dead test code proves nothing.
/// What: `PM_INSTRUCTIONS` → stack profile → optional memory block → workflow →
/// delegation → optional addendum → the non-overridable floor, joined with
/// [`SECTION_SEPARATOR`] and with empty sections dropped.
/// Test: `no_overrides_uses_bundled`,
/// `a_named_memory_override_is_slotted_on_the_roster_absent_path`, and
/// `composed_package_is_byte_identical_to_the_legacy_bundled_fallback` in
/// `bundled_pm_package_tests.rs`.
pub
/// The DEFAULT delegation section: bundled routing doctrine + the live roster.
///
/// THE ROSTER-PRECEDENCE NOTE, slotted between the doctrine and the roster,
/// resolves two ambiguities the append creates, both raised in review.
///
/// (1) PRECEDENCE. The retained asset contradicts the roster on concrete points
/// — it calls the generic `ops` agent DEPRECATED while the roster lists `ops`,
/// and it advertises agents the roster may not carry. Reconciling the asset is
/// #4183's job; until then the PM needs one unambiguous tie-break rule.
///
/// (2) LOADABILITY. The roster is the UNION of three tiers, but no launch mode
/// reads all three: the daemon managed-spawn passes `--setting-sources
/// project,local`, which excludes both user-level tiers. A narrower per-mode
/// tier set is not available here — `tm launch` / `tm connect` deploy into
/// `~/.claude/agents` (`FrameworkPaths::default()`) and then spawn with that
/// very flag, so the tier they populate is the tier the flag excludes. Passing
/// "the tier the flag reads" would render an EMPTY roster there and silently
/// revert those paths to the stale asset — reintroducing #4069. Passing "the
/// tier that was deployed to" would advertise unloadable agents anyway. Until
/// that deploy/read mismatch is fixed, the union plus an explicit re-route
/// instruction is the shape that cannot silently under-advertise; a failed
/// dispatch is self-correcting, a missing agent is not.
///
/// The note is a Markdown blockquote stating the roster wins on WHICH agents
/// exist, and to re-route rather than retry on an unknown-agent-type error. Since
/// #4318 its prose is authored in the instruction manifest
/// (`assets/instructions/pm-instruction-package.json`) as the delegation section's
/// second block, not as a Rust literal — so its position relative to the doctrine
/// and the roster is *declared*, and both composers read the same bytes without a
/// second copy. [`crate::core::instruction_pipeline::delegation_doctrine`]
/// projects doctrine-plus-note back out as one string.
///
/// Why (#4069): the delegation section is meant to be constructed from what is
/// actually deployed, and `build_instructions` does compute that roster via
/// `generate_authority` — but its output lands in a `PipelineOutput` string no
/// prompt composer reads, so the fallback here shipped the static
/// `AGENT_DELEGATION.md` asset verbatim. That asset's agent table has been
/// hand-maintained since 2026-07-03 and names 8 agents; a real deployment
/// carries ~40, so agents like `ticketing` and `memory-manager` appeared **zero
/// times** in the delivered prompt and the PM could not route to them. The
/// bundled asset is still emitted in full because it carries routing doctrine
/// the roster does not (make/mise routing, keyword routing, the ops-agent
/// table) — the live roster is *appended*, never substituted, so no doctrine is
/// lost. Restructuring the asset itself is out of scope (epic #4183).
///
/// This is the FALLBACK only: a project/user `AGENT_DELEGATION.md` override
/// still replaces the whole section, unchanged, exactly as documented.
///
/// What: returns the manifest's delegation doctrine (asset plus the precedence
/// note) with the rendered `## Delegation Authority` block from
/// [`crate::core::delegation_authority::deployed_roster_section`] appended when
/// any agent is deployed. With no deployed agents the asset is returned alone,
/// without the note — the note only makes sense above a roster (pre-#4069
/// behaviour). Takes the already-rendered roster rather than scanning itself
/// (#4183) so `resolve_pm_prompt` scans the agent tiers exactly once whichever
/// composition path it takes.
/// Test: `bundled_delegation_appends_deployed_roster`, `no_overrides_uses_bundled`,
/// `a_retired_delegation_file_no_longer_suppresses_the_live_roster`.
pub
/// The AGENT-DELEGATION section body when no legacy `AGENT_DELEGATION.md` file
/// forced full replacement, honoring a CLAUDE.md named-section override if the
/// project authored one (#4399).
///
/// Why: an unrelated legacy file (`WORKFLOW.md`/`MEMORY.md`) can force
/// [`resolve_pm_prompt_with_roster`] onto this string-assembled branch even
/// though the project never touched `AGENT_DELEGATION.md`. Before #4399 that
/// meant a CLAUDE.md `AGENT-DELEGATION` override was silently dropped in favor
/// of [`delegation_with_roster`]'s bundled doctrine — the project's override
/// was declined for a reason that has nothing to do with delegation. This
/// mirrors [`crate::core::instruction_package::InstructionPackage::with_overrides`]'s
/// authored/generated split for the SAME section on the packaged path: the
/// override replaces the bundled doctrine (and its roster-precedence note),
/// while the live roster — host-computed, never authored by a project — still
/// follows when any agent is deployed.
///
/// What: `Some(body)` returns `body` (trimmed), followed by the roster when
/// `roster` is `Some`; `None` defers to [`delegation_with_roster`] unchanged.
/// Test: `a_legacy_file_cannot_shadow_a_named_override_because_it_is_not_read`
/// (`claude_md_sections_tests.rs`).
/// Join resolved sections with the framework separator, dropping empties.
///
/// Why: a defensive filter keeps a stray empty section from producing a dangling
/// `---` rule; centralizing the join keeps the separator consistent with the
/// bundled [`crate::core::instruction_pipeline::assemble_system_prompt`].
/// What: trims each section, drops empties, joins the rest with
/// [`SECTION_SEPARATOR`].
/// Test: exercised by every `resolve_*` test via the public entry point.