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
//! Delegation authority — scan deployed agents and render a routing section.
//!
//! Why: the orchestrating Claude Code instance needs to know which agents are
//! deployed and what each one handles so it can route work correctly; that
//! list is dynamic (it depends on what was deployed) and must be regenerated
//! at every session start.
//! What: [`scan_agents`] reads every `.md` file in an agents directory, parses
//! its frontmatter, and returns one [`AgentSummary`] per deployable (non-base)
//! agent; [`generate_authority`] folds those summaries into a Markdown section
//! injected into the session launch instructions;
//! [`resolve_roster`] is THE roster resolver every consumer shares — the PM
//! prompt's delegation section, the `tm session start` count, and `tm doctor`
//! (#4588); [`deployed_roster_section`] renders what it returns (#4069).
//! Test: `delegation_authority_tests.rs` covers scanning, foundation-file
//! exclusion and its near misses, the empty directory, and both render
//! branches.
use BTreeMap;
use ;
use parse_kv_line;
/// A deployed agent as advertised to the orchestrating instance.
///
/// Why: the delegation authority section needs a small, render-ready view of
/// each agent — name, role, what it handles, its foundation chain, and a model
/// hint — without exposing the full composed agent body.
/// What: the display fields parsed from a composed agent's frontmatter plus the
/// resolved `extends` chain (base-first, ending in the agent itself).
/// Test: exercised by every `scan_*` and `generate_*` test.
/// File name of the deploy manifest, excluded from agent scans.
const MANIFEST_FILE: &str = "manifest.json";
/// The minimal frontmatter fields a composed agent advertises.
///
/// Why: scanning only needs a handful of YAML keys; a tiny struct avoids a
/// full YAML dependency and keeps parsing explicit.
/// What: the display fields plus `extends` (composed agents normally carry no
/// `extends`, but it is parsed so a hand-written source dir still resolves).
/// Test: exercised indirectly by every `scan_*` test.
/// Parse the leading `---` frontmatter block of a Markdown document.
///
/// Why: composed agents store their metadata in a YAML-ish frontmatter block;
/// the scanner reads just the keys it needs without a YAML library.
/// What: if the document opens with a `---` line, collects `key: value` pairs
/// until the closing `---`; quotes are stripped and keys lower-cased. A
/// document with no frontmatter yields an all-`None` result.
/// Test: `scan_finds_agents` (frontmatter present), `scan_handles_no_frontmatter`.
/// Whether a file stem names a foundation (non-delegatable) template.
///
/// Why (#4589): this is the ONLY rule that removes an agent from the roster, so
/// it must be narrow and it must exist in exactly one place. The `-` is the
/// whole point: `starts_with("base")` silently deletes `baseline-analyzer`,
/// `base64-decoder`, `basecamp-sync` and `based-reviewer` — ordinary names in
/// the project and `~/.claude/agents` tiers tm does not author, which is
/// precisely the failure mode the frontmatter `role:` rule was removed for.
/// Sharing the predicate with the asset-level gate keeps the shipped files and
/// the scanner from drifting apart the way the roster and its count did.
/// What: case-insensitive `base-` prefix on the file stem, so every bundled
/// `BASE-*.md` matches and nothing else plausibly does.
/// Test: `scan_excludes_base_agents`,
/// `near_miss_base_names_are_not_treated_as_foundation_files`.
/// Scan `agents_dir` and return summaries for all non-base agents.
///
/// Why: the orchestrating CC instance needs to know which agents exist
/// and what they handle so it can route work correctly.
///
/// What: reads all .md files in agents_dir, parses frontmatter (name,
/// role, description, model, extends), excludes BASE-* files and the
/// manifest file, returns one AgentSummary per deployable agent.
///
/// Foundation templates are identified by [`is_foundation_file`] — the `BASE-*`
/// FILE NAME convention alone (#4589). An earlier revision also dropped any
/// agent whose frontmatter
/// `role:` began with `base`, which silently deleted three real, dispatchable
/// agents — `memory-manager`, `mpm-agent-manager`, `mpm-skills-manager` — from
/// every roster. `role:` is the wrong signal for the job: the roster is a union
/// over three tiers (see [`deployed_agent_dirs`]) and tm authors the frontmatter
/// in exactly one of them, so no asset edit can stop the same rule from eating
/// an operator's own agent in `<project>/.claude/agents` or `~/.claude/agents`.
/// The file-name convention is tm's own, applies to the files tm actually
/// ships, and is checked before the file is even read.
///
/// Test: `scan_finds_agents`, `scan_excludes_base_agents`,
/// `agent_with_a_base_role_but_no_base_filename_stays_in_the_roster`,
/// `scan_empty_dir`
/// Build a base-first inheritance chain from a single `extends` parent.
///
/// Why: composed agents flatten their inheritance into one file and normally
/// carry no `extends`; when one is present we still record the immediate
/// parent so the rendered "Foundation" line is informative.
/// What: returns `[parent, name]` when an `extends` parent exists, otherwise
/// just `[name]`.
/// Test: `scan_finds_agents` asserts the single-element chain.
/// Generate the delegation authority Markdown section from summaries.
///
/// Why: injected into session launch instructions so the orchestrating
/// instance knows its delegation options.
///
/// What: produces a Markdown section listing each agent with name,
/// description, extends chain, and model hint.
///
/// Zero-information lines are omitted (#4069 review): `Role` is skipped when it
/// merely repeats the `###` heading (it falls back to `name` when unset, true
/// for ~14 of 40 deployed agents) and `Foundation` is skipped for a
/// single-element chain, which renders as the agent citing itself as its own
/// foundation (true for every composed agent, since composition flattens
/// `extends`). This section is re-emitted into EVERY PM session, so ~2 KB of
/// per-session noise is worth deleting.
///
/// Test: `generate_authority_nonempty`, `generate_authority_empty`,
/// `generate_authority_omits_self_referential_lines`
/// Agent directories a launched session can load composed agents from,
/// highest precedence first.
///
/// Why (#4069): the roster the PM must route to is whatever Claude Code will
/// actually load, and that is a union of tiers rather than a single directory.
/// Since #4409 every BUNDLED agent deploys into exactly one of them — the
/// tm-owned `CLAUDE_CONFIG_DIR/agents` — while `<project>/.claude/agents` holds
/// only agents the operator hand-placed (and, in future, project-custom
/// trusty-built agents), and `~/.claude/agents` holds whatever the operator's
/// own generic Claude Code install carries, which tm never writes to. All three
/// are still scanned because a session can legitimately resolve an agent from
/// any of them, and the project tier still WINS on a name collision. Prompt
/// composition only ever receives a `project_dir`, so it resolves all three
/// tiers here rather than depending on a launch-mode value it cannot see.
/// What: returns `<project>/.claude/agents`, then the active managed
/// `CLAUDE_CONFIG_DIR/agents` (the `CLAUDE_CONFIG_DIR` env var when set,
/// otherwise [`managed_claude_config_dir`]), then
/// `FrameworkPaths::default().claude_agents_dir()`. Paths are returned whether
/// or not they exist; [`scan_agents`] treats an unreadable directory as empty.
/// Test: `deployed_agent_dirs_puts_project_tier_first`.
/// Merge the agents found across `dirs` into one roster, earlier dirs winning.
///
/// Why: the tiers returned by [`deployed_agent_dirs`] overlap — the same agent
/// is normally deployed into more than one of them — so a naive concatenation
/// would advertise duplicates. Claude Code resolves a same-named agent by tier
/// precedence, and the rendered roster must describe the copy that actually
/// wins.
/// What: scans each directory in order and keeps the FIRST summary seen per
/// agent name, returning them name-sorted for a stable prompt. The dedup key is
/// LOWER-CASED — agent names are case-insensitive to Claude Code's dispatcher,
/// so `QA` in one tier and `qa` in another are one agent, not two.
/// Test: `roster_from_dirs_dedupes_with_first_dir_winning`,
/// `roster_from_dirs_dedup_is_case_insensitive`.
/// THE agent roster for `project_dir`. Every consumer calls this one function.
///
/// Why (#4588): a roster resolved twice is a roster that drifts. The PM prompt
/// took the three-tier union while `tm session start` printed a count taken
/// from a single directory, so the operator was told 34 when the PM had been
/// given 39 — the two numbers were never wrong in the same way at the same
/// time, because they were never the same computation. There is now exactly one
/// implementation, and `tm session start`, the delegation section injected into
/// the PM prompt, and `tm doctor` all read it.
/// What: unions [`deployed_agent_dirs`] via [`roster_from_dirs`], name-sorted
/// and deduplicated with the highest-precedence tier winning.
/// Test: `session_start_count_matches_the_delivered_delegation_roster`
/// (instruction_pipeline_tests) is the cross-consumer agreement gate; the
/// tier-union behaviour itself is covered by the `roster_from_dirs_*` tests.
/// This function consults machine-global tiers, so it has no hermetic direct
/// test of its own.
/// Render the LIVE deployed roster for a project, or `None` when none is found.
///
/// Why (#4069): `build_instructions` already computed this section via
/// [`generate_authority`], but its output is merged into a `PipelineOutput`
/// string that the prompt composer never reads — `resolve_pm_prompt` fell back
/// to the static `AGENT_DELEGATION.md` asset, so a 42-agent deployment was
/// advertised to the PM as the asset's hand-maintained 8-row table and agents
/// such as `ticketing` and `memory-manager` were invisible. Regenerating here
/// (rather than threading the pipeline's string down) is the only shape that
/// works for every composer: `build_system_prompt_for*` and its callers —
/// `tm session instructions`, the tmux connect path, the daemon spawn — receive
/// a `project_dir` and nothing else, and two of them never run the pipeline at
/// all, so there is no `PipelineOutput` available to thread.
/// What: resolves the roster via [`resolve_roster`] — the one resolver every
/// consumer shares — and renders it with [`generate_authority`]. Returns `None` when no agent is deployed
/// anywhere, so an unprovisioned environment keeps exactly the previous
/// behaviour (the bundled asset alone) instead of being told it has no agents.
/// Test: `roster_from_dirs_ignores_missing_dirs` (the `None` branch's input) and
/// `instruction_overrides::tests::bundled_delegation_appends_deployed_roster`
/// (the rendered output reaching the delivered prompt). This function itself
/// consults machine-global tiers, so it has no hermetic direct test.