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
//! The bundled FRAMEWORK-TIER manifest — the declared always-deploy set (#4760).
//!
//! Why: before this module, "which agents always deploy" was not declared
//! anywhere. It was COMPUTED, as the complement of `LANGUAGE_ENGINEERS`:
//! `language_agent_scope` emitted an `exclude` list of the language engineers a
//! project did not match, and everything else deployed by virtue of not being in
//! that one table. Nothing could be retired, because there was no list to remove
//! it from — `ops` has been marked DEPRECATED in `agent-delegation.md` prose
//! since long before this file, with zero code-level effect, and still reached
//! every roster. A declared catalog makes deprecation an edit rather than a wish.
//! What: [`FRAMEWORK_MANIFEST_TOML`] embeds `assets/framework-manifest.toml` —
//! a `HarnessManifest` document in the SAME format, parsed by the SAME
//! [`HarnessManifest::from_toml`], differing only in tier. Its
//! `[agent_categories]` section partitions the bundled catalog into `universal`
//! (no detection), `language` / `framework` / `platform` (marker-gated, each
//! entry carrying its OWN markers since #4765), and `deprecated` (never
//! deployed); its `[skill_categories]` section declares the bundled skill
//! roster. [`parse_framework_manifest`] and [`parse_framework_skills`] validate
//! those declarations against the bundled catalogs; [`agent_scope_from`]
//! composes the agent side with `project_lang`'s marker evaluation into the
//! [`AgentSet`] `ManifestSources::resolve` applies as its lowest override layer.
//!
//! **One authority.** Owner ruling 2026-08-04: "authoritative agent/skill
//! bundling should be in framework-manifest.toml or project manifest.toml.
//! That's it." Membership, category, and gate condition are all manifest fields.
//! No bundled document restates them; `assets/skills/tm.md` and
//! `assets/instructions/sections/agent-delegation.md` point at the manifest, and
//! `tm generate capabilities` renders it mechanically.
//!
//! **Naming caution.** ADR-0025's "four-category agent model" (universal
//! bundled / stack-specific bundled / project custom / user custom) is a
//! different axis from these four DEPLOYMENT categories. ADR-0025 classifies by
//! WHO AUTHORED an agent and WHERE it lives; this file classifies, within
//! ADR-0025's bundled categories 1–2 only, by WHAT GATES its deployment. The two
//! are orthogonal: every stem named in `framework-manifest.toml` is an ADR-0025
//! category-1 or category-2 agent, and this file says nothing about ADR-0025's
//! categories 3–4 (operator-authored agents), which no manifest declares.
//! Test: `crates/trusty-mpm/src/core/manifest/framework_tests.rs`.
use ;
use Path;
use Arc;
use Detection;
use MarkerProbe;
use ;
/// The nested walk's declared depth, re-exported beside [`StackDetection`].
///
/// Why: #7781 round-3 — a renderer reporting [`StackDetection::depth_limited`]
/// has to name the depth, and the walk itself is private to this module tree.
/// Re-exporting the one constant keeps the rendered number from drifting into a
/// hand-copied literal.
/// What: `super::nested::MAX_NESTED_DEPTH`.
/// Test: `core::stack_profile::tests::depth_limited_detection_says_so`.
pub use MAX_NESTED_DEPTH;
/// File name of the bundled framework-tier manifest.
///
/// Why: named once so the error messages, the docs, and any future on-disk
/// materialisation cannot drift.
/// What: `framework-manifest.toml`.
/// Test: `bundled_framework_manifest_is_valid`.
pub const FRAMEWORK_MANIFEST_FILE: &str = "framework-manifest.toml";
/// The bundled framework manifest, embedded at compile time.
///
/// Why: this is the framework's own catalog declaration, not operator input. It
/// travels with the binary so a binary-only install has it, and so a MISSING
/// manifest is a compile error — the loudest failure available — rather than a
/// runtime condition anything could paper over with a fallback.
/// What: the contents of `crates/trusty-mpm/src/assets/framework-manifest.toml`.
/// Test: `bundled_framework_manifest_is_valid`.
const FRAMEWORK_MANIFEST_TOML: &str = include_str!;
/// Why the bundled framework manifest could not be used.
///
/// Why: requirement #4760.4 — a missing or malformed manifest must fail LOUDLY,
/// never silently fall back to deploying everything and never silently deploy
/// nothing. Every failure mode is a distinct variant so the operator-facing
/// message names the actual defect, and so tests can pin each one. Crucially,
/// NONE of these variants is reachable from "the project matched no marker":
/// that is an ordinary `Ok` with an empty detected set, on a different code path.
/// What: parse failure, a missing/empty section, and each partition-invariant
/// violation.
/// Test: `malformed_manifest_is_an_error`, `missing_section_is_an_error`,
/// `empty_universal_is_an_error`, `unknown_stem_is_an_error`,
/// `undeclared_bundled_agent_is_an_error`, `duplicate_declaration_is_an_error`,
/// `gated_entry_without_markers_is_an_error`,
/// `missing_skill_section_is_an_error`, `unknown_skill_is_an_error`,
/// `undeclared_bundled_skill_is_an_error`, `duplicate_skill_is_an_error`.
/// Every bundled agent stem the binary can actually deploy.
///
/// Why: the partition invariant is only meaningful against the real catalog.
/// Deriving the catalog from `core::bundle::ALL` — the same embedded artifact
/// table the installer writes — means a newly added agent that nobody declared
/// fails the invariant loudly instead of quietly never deploying.
/// What: the `agents/<stem>.md` entries of [`crate::core::bundle::ALL`], minus
/// the `BASE-*` foundation templates, which are inheritance fragments rather
/// than dispatchable agents and are never selected by name.
/// Test: `bundled_agent_stems_excludes_foundations`.
/// Parse and fully validate a framework-tier manifest document.
///
/// Why: this is the single place the always-deploy declaration is checked, and
/// it is deliberately a pure function over `(raw, catalog)` so every failure
/// mode is testable without touching the shipped asset. Requirement #4760.4
/// lives here: it returns `Err` — never a permissive default — for a malformed
/// document, a missing section, an empty `universal`, or any violation of the
/// partition invariant.
/// What: parses `raw` with the shared [`HarnessManifest::from_toml`], takes its
/// `[agent_categories]`, and verifies that the five lists (a) name only stems
/// present in `catalog`, (b) name each catalog stem exactly once, (c) never
/// repeat a stem across lists, (d) gate only on markers that exist, and (e)
/// leave `universal` non-empty.
/// Test: every `*_is_an_error` test, plus `bundled_framework_manifest_is_valid`.
/// Every declared stem paired with the TOML key that declared it.
///
/// Why: the partition passes must walk ALL five lists — the two plain stem lists
/// and the three gated ones — and naming them once keeps a newly added category
/// from being silently skipped by one pass.
/// What: `(key, stem)` pairs, in declaration order.
/// Test: covered by every partition-invariant test.
/// The three marker-gated category lists paired with their TOML key.
///
/// Why/What/Test: as [`labelled_stems`], for the passes that need the markers
/// rather than only the stem.
/// Every bundled skill stem the binary can actually deploy.
///
/// Why: the skill roster gets the same guarantee the agent roster does — a
/// bundled skill nobody declared is a hard error, not a silent default.
/// What: the top-level `skills/<stem>.md` entries of [`crate::core::bundle::ALL`].
/// A nested `skills/<name>/references/*.md` is a skill's own reference material,
/// not a catalog entry, and is excluded — the same predicate
/// `tm generate capabilities` uses for its skill count.
/// Test: `bundled_skill_stems_excludes_reference_files`.
/// Parse and fully validate a framework-tier manifest's SKILL roster (#4765).
///
/// Why: the owner ruling makes `framework-manifest.toml` the authority for skill
/// bundling as well as agent bundling. Validating exhaustively against the real
/// bundle is what makes the declaration load-bearing rather than decorative:
/// adding a `skills/*.md` row to `bundle_all.rs` without declaring it here fails
/// the build's test gate.
/// What: pure over `(raw, catalog)`, like [`parse_framework_manifest`]. Returns
/// `Err` for a malformed document, a missing `[skill_categories]` section, an
/// unknown stem, a duplicate, or any bundled skill left undeclared.
/// Test: `bundled_skill_roster_is_valid`, `missing_skill_section_is_an_error`,
/// `unknown_skill_is_an_error`, `undeclared_bundled_skill_is_an_error`,
/// `duplicate_skill_is_an_error`.
/// The validated skill roster declared by the bundled framework manifest.
///
/// Why/What/Test: [`parse_framework_skills`] over [`FRAMEWORK_MANIFEST_TOML`]
/// and the real [`bundled_skill_stems`] catalog. Test:
/// `bundled_skill_roster_is_valid`.
/// The validated categories declared by the bundled framework manifest.
///
/// Why/What/Test: [`parse_framework_manifest`] over [`FRAMEWORK_MANIFEST_TOML`]
/// and the real [`bundled_agent_stems`] catalog. Test:
/// `bundled_framework_manifest_is_valid`.
/// Compose declared categories with detected markers into a deploy selection.
///
/// Why: this is the single, auditable statement of how the always-deploy set and
/// the marker-gated sets combine. Keeping it a pure function of
/// `(categories, project_dir)` means the composition can be tested against
/// synthetic categories, so a test that a deprecated agent does not deploy
/// cannot pass merely because the real manifest happens to omit it.
/// What: builds an [`AgentSet`] that leaves `include` empty (no allowlist) and
/// states the gate as an explicit `exclude`, derived ENTIRELY from the declared
/// categories:
///
/// ```text
/// exclude = deprecated
/// u ((language u framework) \ detected stacks)
/// u (platform \ detected platforms)
/// ```
///
/// A `universal` stem appears in no term, so nothing can exclude it — that is
/// what makes the manifest the source of truth for the always-deploy set.
///
/// Two deliberate asymmetries:
///
/// * **Unknown project type** (no stack marker recognised at all — e.g. the
/// daemon's own framework root): the stack term contributes nothing, so every
/// `language` and `framework` engineer still deploys, exactly as
/// `language_agent_scope` behaved before #4760. Zero regression.
/// * **Platform has no such fallback.** A project with no platform marker
/// excludes every `platform` stem. That is the intended behaviour change
/// #4760 carries: `gcp-ops`/`vercel-ops` used to deploy everywhere.
///
/// **Why an exclusion and not an `include` allowlist.** An allowlist naming only
/// the bundled stems would silently drop anything the SOURCE DIRECTORY carries
/// that the manifest does not name — the five `BASE-*` inheritance fragments,
/// and every agent a `ContentSource::Catalog` checkout adds. Deploying less than
/// asked, silently, is the failure class this work exists to remove, not one to
/// introduce. The exhaustiveness invariant in [`parse_framework_manifest`] buys
/// what the allowlist would have: a bundled agent missing from the manifest is a
/// hard error, so it can never be forgotten into deploying.
/// Test: `universal_agents_deploy_with_no_markers`,
/// `language_engineers_still_gate_on_detection`,
/// `deprecated_agent_never_deploys`, `platform_agent_absent_without_marker`,
/// `platform_agent_deploys_with_marker`,
/// `unknown_project_keeps_every_stack_engineer`,
/// `undeclared_source_agents_still_deploy`.
/// Fold an already-detected marker result into the framework-tier [`AgentSet`].
///
/// Why: #7806 split composition from probing so the memoized path can compose
/// the SAME selection from the one probe pass it also derives
/// [`StackDetection`] from. The selection rules — and the two asymmetries
/// [`agent_scope_from`] documents — live here, once.
/// What: the exclusion list, from `deprecated` unconditionally, the unselected
/// stack entries when any stack matched, and every unselected platform entry.
/// Test: `universal_agents_deploy_with_no_markers`,
/// `unknown_project_keeps_every_stack_engineer`,
/// `platform_agent_absent_without_marker`.
/// A project's detected stack engineers, and why the scan stopped where it did.
///
/// Why: [`detected_stack_engineers`] used to answer with a bare set, so a set
/// short one engineer because a scan bound stopped the walk was indistinguishable
/// from a project that genuinely has no such stack — the round-1 #7781 finding.
/// Round-2 answered it with one flag that the depth bound set on most real
/// repositories, which is useless as a fail-closed signal; round-3 splits it.
/// What: `truncated` means the engineer set may be INCOMPLETE because a resource
/// cap stopped detection — EVERY such cap, in either discovery pass: the
/// declared-member and pattern caps of `super::workspace`, and the nested walk's
/// scanned-directory and shared member caps (#7781 round-3). A consumer that
/// persists a negative conclusion must fail closed on it. `depth_limited` means
/// manifests deeper than the declared depth were not probed, which is the
/// design's scope, informational only. The tripped resource cap is named in a
/// `tracing::warn!` and the depth bound in a `tracing::debug!`.
///
/// `#[non_exhaustive]`: a future bound gets a third flag, and adding one must
/// not be a breaking change for a downstream consumer that matched on the
/// struct. Construction stays inside this crate, which is where every
/// bound that could set a flag lives.
/// Test: `detected_stack_engineers_matches_the_manifest`,
/// `truncated_detection_is_reported_to_the_caller`,
/// `declared_member_cap_is_reported_to_the_caller`,
/// `core::stack_profile::tests::truncated_detection_says_so`.
/// The stack engineers `project_dir`'s markers select, per the bundled manifest.
///
/// Why: `core::stack_profile` primes the PM prompt with the project's actual
/// stack and must read the SAME declaration the deployer reads — a prompt that
/// names `rust-engineer` for a project that never received it is the drift #1971
/// exists to prevent. Before #4765 it imported `project_lang`'s marker table
/// directly; that table is now a manifest field, so this is the entry point.
/// What: a [`StackDetection`] holding the union of the declared `language` and
/// `framework` stems whose markers are present, plus that type's two independent
/// flags. An unusable manifest yields an EMPTY, unflagged set, which
/// `stack_profile_section` renders as its neutral "detect before routing"
/// block — the safe answer for a prompt. The DEPLOY path does not share that
/// leniency: [`framework_agent_scope`] refuses to resolve at all.
/// Test: `detected_stack_engineers_matches_the_manifest`,
/// `truncated_detection_is_reported_to_the_caller`,
/// `core::stack_profile::tests::detected_rust_lists_rust_engineer`.
/// `project_dir`'s detection, from the memo when it is live (#7806).
///
/// Why: the one place the nested walk is actually paid for. Before this, the
/// five-plus manifest resolutions in a single launch each re-walked the tree and
/// re-read every manifest for an answer that cannot change between them.
/// What: [`super::detect_memo::memoized`] over ONE [`MarkerProbe`] pass, folded
/// into both views — the composed [`AgentSet`] and the [`StackDetection`] with
/// its two scan-bound flags. An unusable bundled manifest returns `Err` and is
/// never cached; the two callers differ only in what they do with it. See
/// `super::detect_memo` for what invalidates an entry.
/// Test: `stack_detection_is_memoized_per_project`,
/// `invalidating_picks_up_a_changed_tree`.
/// The framework-tier agent selection for `project_dir`.
///
/// Why: the one entry point `ManifestSources::resolve` calls.
///
/// **Panics** — deliberately, per requirement #4760.4 — when the bundled
/// manifest is unusable. [`FRAMEWORK_MANIFEST_TOML`] is a compile-time constant,
/// so an `Err` here means the SHIPPED asset is corrupt: a programmer error and a
/// build-integrity failure, not operator input, which is exactly the case this
/// project's "no `unwrap()` in library code" rule reserves `expect`-style aborts
/// for. The alternatives are both forbidden by the requirement — returning an
/// empty selection deploys nothing, and returning the permissive default deploys
/// everything, each silently. It is unreachable in a valid build:
/// `bundled_framework_manifest_is_valid` fails the test suite before merge if
/// the asset ever stops validating.
/// What: [`detection_for`]'s memoized pass, which is
/// [`framework_agent_categories`] then the same composition [`agent_scope_from`]
/// performs (#7806 — `ManifestSources::resolve` calls this five-plus times per
/// launch, and only the first pays for the walk).
/// Test: `bundled_framework_manifest_is_valid` (the guard),
/// `framework_agent_scope_selects_universal_agents`.