trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
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
//! Renders `references/doctor.md` from a maintained literal list of `tm
//! doctor` checks (source #5 of the issue #2913 design-research brief).
//!
//! Why: unlike the CLI tree, MCP catalog, and agent/skill rosters, doctor
//! checks have no single data table — each is a `DoctorCheck::new("literal",
//! ...)` call scattered across `run_doctor` and five sibling files
//! (`doctor_fs_checks.rs`, `doctor_deploy_validate.rs`,
//! `doctor_staleness.rs`, `doctor_output_style.rs`,
//! `doctor_agent_skills.rs`). The design-research brief's recommendation for
//! this "semi-extractable" surface is a maintained literal list cross-checked
//! by a count/name assertion test against `run_doctor`'s actual output,
//! rather than fragile literal-string grepping across six files.
//! What: [`DOCTOR_CHECKS`] — name + one-line description, in `run_doctor`'s
//! execution order — plus [`render`]. The drift guard lives in this
//! module's test suite: `doctor_checks_match_run_doctor_names` calls the real
//! `run_doctor` and asserts the name list is identical, so an added/removed/
//! renamed check fails the test suite (not just a docs staleness bug).
//! Test: `doctor_checks_match_run_doctor_names`,
//! `doctor_render_contains_known_check`.

use std::fmt::Write as _;

/// Every `tm doctor` check, in `run_doctor`'s execution order.
///
/// Why: `run_doctor`'s own doc comment (`crates/trusty-mpm/src/daemon/
/// doctor.rs`) is the closest thing to a spec for this list; this constant
/// mirrors it so the generator has a single, reviewable source instead of
/// re-deriving prose per release.
/// What: `(name, what it probes)` pairs. Kept in sync by
/// `doctor_checks_match_run_doctor_names`, which fails the test suite the
/// moment `run_doctor`'s actual check set diverges from this list.
/// Test: `doctor_checks_match_run_doctor_names`.
pub(crate) const DOCTOR_CHECKS: &[(&str, &str)] = &[
    (
        "instructions",
        "Framework instructions deployed and non-empty for the target project.",
    ),
    (
        "agents",
        "Bundled agent roster deployed under the operator/workspace `.claude/agents/` tier.",
    ),
    (
        "agent_reachability",
        "Fails when bundled agents deploy into a settings tier a managed session's `--setting-sources` flag never loads — presence-only checks stay green while every delegation degrades to `general-purpose` (issue #4451).",
    ),
    (
        "asset_tier",
        "Fails when tm-owned agent files sit in a project's `.claude/agents/` — that tier outranks the canonical `$CLAUDE_CONFIG_DIR/agents/` deploy, so a stale or stub copy shadows the real agent while every presence-only check stays green (issue #4442). Warns for leftovers in `~/.claude/agents/`, which a managed session no longer reads. The check itself is read-only; `tm doctor --fix-agents` PREVIEWS the removal and `tm doctor --fix-agents --yes` applies it, removing only the copies this tier's `.trusty-mpm-manifest.json` records as tm's, with a framework-owned origin, whose bytes still match the recorded checksum. It refuses a bundled-named DIRECTORY, an untracked file, a ledger entry the operator owns, a copy hand-edited after deployment, a project tier that is a symlink or cannot be listed, and any tier resolving onto the canonical deploy dir or `~/.claude/agents`. Every removal is backed up first; `tm doctor --fix` never runs it (issues #4442, #6649).",
    ),
    (
        "asset_duplicates",
        "Warns when ONE asset name is claimed by two entries in the SAME tier — `qa.md` beside a `qa/` directory, or `QA.md` beside `qa.md`, in either the agent or the skill tier. Only one of the two ever loads, which one is loader order, and on a case-insensitive filesystem the two names are one file — so the copy the operator edits may not be the copy that runs. `asset_tier` and `skill_project_tier` both compare one directory against another and are structurally unable to see this. Ownership is not the question here (both entries may be the operator's own), which is why it is a separate row rather than folded into either. Scans the canonical agent and skill deploy dirs plus the project's own `.claude/agents` and `.claude/skills`; reports UNKNOWN for a tier it cannot list. Report-only, with NO repair: tm cannot know which entry the operator meant to keep, so the operator deletes or renames one (issue #6649).",
    ),
    (
        "transcript_saving",
        "Fails when a managed spawn would leave Claude Code transcript saving disabled — an inherited `CLAUDE_CODE_CHILD_SESSION` marker costs the session all native `--resume`/`--continue`/`/rewind` recovery, and also fails if the scrub would wrongly take `CLAUDE_CONFIG_DIR` (issue #4467).",
    ),
    (
        "skills",
        "Bundled skill catalog deployed under the operator/workspace `.claude/skills/` tier.",
    ),
    (
        "skill_source",
        "The framework's own skill source directory is present and readable.",
    ),
    (
        "skill_reachability",
        "Fails when a skill the framework's own source roster declares deployable reached no tier the harness reads, or when the copy that would load has frontmatter that does not parse, declares no `name`, or declares a `name` that disagrees with the name it is deployed under. This is the skill mirror of `agent_reachability`: every other skill probe audits something INSIDE the tiers it is handed — `skill_staleness` compares checksums, `skill_unmanaged` compares against a deploy ledger, `skill_project_tier` reports a retired duplicate — and none can fail when a rostered skill reaches no tier at all, which is what `deploy_skills_filtered` did to directory-shaped skills for weeks while every presence-only probe stayed green (issue #4949). Warns instead when one skill is deployed at two tiers, naming both paths and the copy to delete: only the higher-precedence copy ever loads, and bundled skills are user-tier only since the 2026-09-01 owner ruling (#6586). Reports UNKNOWN — never `Ok` — for an empty roster or a tier that exists and cannot be listed. Scans `$CLAUDE_CONFIG_DIR/skills`, `~/.claude/skills`, and the project's `.claude/skills`. Read-only; removes nothing it reports (issue #4947).",
    ),
    (
        "output_style",
        "The `trusty-mpm` Claude Code output style is configured and its file exists (DOC-28 F4).",
    ),
    (
        "output_style_staleness",
        "Deployed output-style file content matches the bundled catalog, and no orphaned files linger under `output-styles/` (issue #2333). Scans BOTH style tiers and names the tier in every finding: the operator's `~/.claude/output-styles/` and the managed `$CLAUDE_CONFIG_DIR/output-styles/`, which is the copy a tm-launched session actually reads. Scanning only the first let `tm doctor --fix --yes` redeploy the operator's copy and report clean while the managed copy stayed stale (issue #7423). `tm doctor --fix --yes` redeploys whichever tier drifted; an orphaned file is named and never deleted.",
    ),
    (
        "output_style_legacy_ids",
        "Warns when a legacy/unresolvable `outputStyle` id lingers in a currently-shadowed settings layer (e.g. `settings.local.json`) even though the effective layer resolves fine (issue #3453).",
    ),
    (
        "deployment",
        "Full manifest-completeness diff of the deployed payload against the canonical bundled roster (issue #2158).",
    ),
    (
        "skill_staleness",
        "Deployed skill content matches the RUNNING BINARY's own embedded bundled asset, at every deploy tier (`$CLAUDE_CONFIG_DIR/skills`, `~/.claude/skills`, the project's `.claude/skills`). Reads the deployed FILE, not the deploy manifest, and compares against the compiled-in asset rather than the `~/.trusty-mpm/framework/skills` extraction cache — that cache can itself lag the installed binary, which made every skill it covered report clean regardless of what shipped (issue #4604). Distinguishes drift a redeploy repairs from drift that is FROZEN (hand-edited, so `tm install` deliberately skips it), and reports UNKNOWN — never `Ok` — for anything it cannot verify. Read-only; `tm doctor --fix-skills` is the repair — a bare `--fix-skills` PREVIEWS the redeploy and writes nothing, and `tm doctor --fix-skills --yes` applies it, backing up each overwrite first (issues #2876, #4604, #6620). At the managed `$CLAUDE_CONFIG_DIR/skills` tier the audited set is the deploy ledger UNION this binary's own bundled roster, so a skill the binary ships and no deploy has ever written reports `Missing` there instead of producing no finding at all; `tm doctor --fix --yes` then deploys it. The operator-home and project tiers keep auditing the ledger alone — bundled skills are user-tier only since the 2026-09-01 owner ruling (issues #6586, #7423).",
    ),
    (
        "skill_unmanaged",
        "Reports UNKNOWN when a bundled skill is deployed to a tier whose `.trusty-mpm-skills-manifest.json` does not track it — the tier planner classifies it project-custom and drops it from every deploy, so no `tm` command can refresh it and `skill_staleness` (which compares against that same manifest) cannot see it at all. Never `Ok` for such a skill: content alone cannot distinguish an orphaned tm deployment from a deliberate customization. Scans `$CLAUDE_CONFIG_DIR/skills`, `~/.claude/skills`, and the project's `.claude/skills`. Read-only; `tm install --reconcile-skills` is the repair (issue #4605).",
    ),
    (
        "skill_project_tier",
        "Warns when a bundled skill is still deployed at the PROJECT tier (`<project>/.claude/skills`). Bundled skills are user-tier only since the 2026-09-01 owner ruling, the same principle #4448 settled for bundled agents, so the deploy sites no longer write them there — but they cannot remove a copy an earlier binary already wrote, and nothing refreshes it afterward. The user-tier copy is the one Claude Code loads (for skills, personal outranks project). Reads the tier from DISK, not from the deploy manifest: a stray the earlier deploy left behind is recorded MANAGED in that manifest, so filtering by it hid the one shape this check exists to catch. The check itself is read-only; `tm doctor --fix-skills` PREVIEWS the removal and `tm doctor --fix-skills --yes` applies it, removing the copies the tier's ledger proves tm wrote and dropping them from it. It refuses any copy the ledger does not record, any whose subtree holds a file tm never deployed or one that was edited after deployment, a bundled-named entry that is not a skill directory (which this check counts too, so check and repair agree), a project tier that is a symlink, cannot be listed, or resolves onto a tier bundled skills deploy to. Since #6620 both `--fix-skills` halves answer to `--yes`, so a bare `--fix-skills` writes nothing at all — not the sweep's removal, not the redeploy's rewrite, and no backup directory; the redeploy also skips refreshing the copies the sweep is removing, so the two halves never work against each other (issues #6586, #6620).",
    ),
    (
        "legacy_sources",
        "No legacy global instruction sources linger from a pre-migration install (issue #2876).",
    ),
    (
        "legacy_overrides",
        "The project carries none of the five RETIRED `.trusty-mpm/` instruction override files. They are no longer read, so a leftover one means the project's instructions are not reaching the PM — migrate the content to `CLAUDE.md` named sections (issue #4286).",
    ),
    (
        "session_profile",
        "Which instruction profile a launch in this project resolves. `Warn` when the project's committed `.trusty-mpm.toml` asks for `profile = \"supervisor\"` but the operator has not allow-listed it under `[supervisor] projects` in the user-level `~/.trusty-mpm/config.toml` — the session then runs the full PM profile. A project cannot exempt itself from the PM rules by editing its own file (issue #8453).",
    ),
    (
        "pm_guard",
        "Whether launches write the `tm hook --pm-guard` `PreToolUse` entry, read from `[pm_guard] enabled` in `~/.trusty-mpm/config.toml`. `Ok` when the guard is on — including a missing key or section, and a config that cannot be read or parsed, which fail closed to guarded. `Warn` with \"pm-guard disabled by [pm_guard] enabled = false\" when the key turns it off: every project-hook writer (launch, resume, `tm install`) then omits the entry and strips an existing one, leaving every other managed hook in place. A running Claude Code session keeps the hooks it loaded at startup, so the change applies at its next launch. Read-only (issue #9018, owner ruling 307).",
    ),
    (
        "org_accounts",
        "Whether the `[accounts]` table in `~/.trusty-mpm/config.toml` reads. It maps a GitHub org to the `gh` account that clones and spawns for it. `Ok` naming how many orgs it maps, or \"missing\" when there is no table (clones and spawns use the registry pin or the ambient identity). `Warn` when the file has a TOML syntax error and no line names `accounts` (a `[accounts]`, `[\"accounts\"]` or `[accounts.x]` header, or an `accounts = …` or `accounts.org = …` key), which is read as an empty table. `Fail` with the error when the table cannot be read — a syntax error in a file that holds the table, a value that is not a string, an invalid login, two orgs differing only in case — since clones then refuse and spawned sessions' `gh` authenticates as nobody. Read-only (issue #9091).",
    ),
    (
        "instruction_fold",
        "How much the compose-time instruction fold saved this project, measured from its most recently compiled PM prompt: `Ok` with both byte counts and the percentage when the prompt came out smaller than the instruction bodies it was built from, `Warn` naming INACTIVE and both counts when it did not. Before this check the decline was a daemon-log line only, so nothing an operator reads said whether the fold was doing anything (issue #7616). Named `instruction_compression` until issue #7867 reserved \"compression\" for tool-output compression; this row claims nothing about the `💸` segment, which measures that instead.",
    ),
    (
        "tool_output_compression",
        "What tool-output compression has saved, folded from the same ledger rows the `💸` statusline segment renders — `compress` (a `tm compress` run, rtk or native) and `divert` (a bulk read answered by a cheap worker), both credited per tool call. `Ok` naming the row count, the tokens and estimated bytes saved and the last row's timestamp; `Warn` when no such row exists yet; `Warn` naming the IO error when the ledger exists and cannot be read, since an unreadable ledger is not an empty one. Never `Fail`. Read-only (issue #7867).",
    ),
    (
        "statusline",
        "Whether the `💸` savings segment can render at all: the `statusLine` command in each settings tier (present, and pointing at a binary that still exists), whether the savings ledger and the per-session statusline record store are readable, and whether the render rule produces a figure for a synthetic fold. `Fail` when no tier is wired, when an input cannot be read, or when the render rule declines; `Warn` when it can render but has no rows yet, or when a stale tier sits beside a wired one. Remediation: `tm doctor --fix --yes`, which seeds or repoints the entry in the project and user tiers (issue #7617).",
    ),
    (
        "agent_skills",
        "Every agent's declared `skills:` frontmatter resolves to a real skill — dangling references fail (DOC-42, issue #2889).",
    ),
    (
        "agent_skills_prose_hints",
        "Informational: skill names mentioned in agent prose but not declared in `skills:` frontmatter (always `Ok`, issue #2906).",
    ),
    (
        "memory",
        "trusty-memory sidecar reachability probe (bounded by `PROBE_TIMEOUT`).",
    ),
    (
        "search",
        "trusty-search sidecar reachability + expected-index-present probe (bounded by `PROBE_TIMEOUT`).",
    ),
    (
        "search_index_pin",
        "Resolves the index id the project's `.mcp.json` actually PINS (`trusty-search serve --index <id>`) against `GET /indexes/{id}/status`, and FAILS on a 404 — the pin names an index the daemon does not have, so every `search`/`grep` call in the session returns \"unknown index\". Index registration is fail-open at every step, so the pin advances even when creation failed and the `search` check above still reports healthy; 4 of 75 live worktrees had an index when this was measured. Read-only; never creates or reindexes (issue #5045).",
    ),
    (
        "worktrees",
        "No orphaned git worktrees under the managed workspace root (Fix 1b, #1840).",
    ),
    (
        "worktree_disk",
        "Bytes held by every git-registered worktree, and how much sits on already-merged \
         pull requests with no unsaved work (issue #2919).",
    ),
    (
        "disk_usage",
        "The MOUNT holding the worktree store against `disk.max_usage_pct` (default 90): Ok below \
         the threshold, Warn at or above it — where new worktrees are being REFUSED — and Unknown \
         when the mount could not be measured (issue #7497).",
    ),
    (
        "worktree_registry",
        "Count and GiB per project from the append-only worktree ledger `~/.trusty-mpm/worktrees.jsonl` alone — never git, never a filesystem walk; sizes are the last `measured` events `tm worktrees` recorded. Ok with no ledger yet (`tm worktrees` backfills it), Warn on malformed ledger lines, Unknown when the ledger cannot be read (issue #8994).",
    ),
    (
        "rust_build_env",
        "This machine's Rust build settings, for a project whose detected stack includes Rust — \
         a non-Rust project (or none supplied) reports the row as not applicable, never as a \
         warning. Reports the shared `CARGO_TARGET_DIR` (`build.cargo_target_dir`, defaulting to \
         `~/.trusty-tools/cargo-target/<owner>/<repo>` derived from the `origin` remote): whether \
         it exists, whether it is writable — probed by WRITING a file, since mode bits say only \
         whether somebody may write — and how much it holds, measured under a 2 s budget that \
         reports \"at least\" rather than a wrong total. Also the resolved `build.build_jobs` \
         (default: half the host's cores, minimum 2 — six concurrent cold builds crashed the \
         reference host on 2026-08-08), whether `sccache` is on PATH, and whether \
         `build.rustc-wrapper` is wired in `~/.cargo/config.toml`. Every message closes with the \
         one line a PM pastes into an engineer brief: `CARGO_TARGET_DIR=<dir> \
         CARGO_BUILD_JOBS=<n> [RUSTC_WRAPPER=sccache] SKIP_UI_BUILD=1`. `Ok` when the directory \
         exists and is writable; `Warn` when it does not exist yet (naming `tm doctor --fix \
         --yes`), or when the config asks for sccache and no wrapper is wired; `Fail` ONLY when \
         the directory exists and cannot be written; UNKNOWN — never `Ok` — when the repo \
         identity cannot be derived, stack detection was cut short, or the directory could not \
         be measured. An absent `build:` section is the shipped defaults, never a finding. \
         Read-only; `tm doctor --fix --yes` creates the directory with its parents and seeds the \
         `build:` section when and only when no `build` key is present, preserving every \
         existing key and comment. Neither the check nor the repair ever writes \
         `~/.cargo/config.toml` — it is machine-global for every Rust project on the host \
         (issue #6868).",
    ),
    (
        "base_clone",
        "The base clone each live worktree resolves through still has its git identity — \
         fails naming the base path and how many worktrees hang off it (issue #3605).",
    ),
    (
        "gh_account",
        "Active `gh` CLI identity is unambiguous — warns on multi-account ambiguity.",
    ),
    // #7311: rtk is an install dependency; never run rtk init.
    (
        "rtk",
        "Warns when the `rtk` binary is not on PATH. `tm compress` shells out to rtk and falls back to a slower native compressor when it is absent, silently — so an install that never pulled rtk in reads exactly like one that did. Advisory: the fallback keeps compression working, so a missing rtk never turns `tm doctor` red. Remediation: install with `brew install rtk`; do not run `rtk init`, tm invokes rtk directly — `rtk init` / `rtk init -g` install a competing PreToolUse Bash hook. The Homebrew formula carries `depends_on \"rtk\"`, so only `cargo install` users have to install it themselves (issue #7311).",
    ),
    (
        "issue_audit_recent",
        "Warns when an OPEN issue created in the last 7 days misses what the ticketing standard requires — a GitHub Project, a milestone (or a `no-milestone: <reason>` comment), or a component label — naming the failing issue numbers and the requirement each missed. Advisory: ticket hygiene never turns `tm doctor` red. A `gh` that is absent, unauthenticated, or erroring reports UNDETERMINED rather than a pass, since an audit that did not run has not found the tickets clean. Run `tm issue audit <N>` for one issue's detail (issue #7097).",
    ),
    (
        "oauth_token",
        "Warns when a managed session risks the `CLAUDE_CONFIG_DIR`-keyed Keychain login loop (issue #2246).",
    ),
    (
        "launchd_secrets",
        "Fails when a `com.trusty.*` LaunchAgent plist holds a plaintext credential, naming the file, the KEY and the file's mode and never the value. UNKNOWN rather than OK when a plist cannot be read, parsed, is a BINARY `bplist00` file, or is a symlink — a scan that did not run has not shown the host clean. `tm doctor --fix --yes` migrates each registered credential into the credential store, confirms a byte-equal read-back and only then strips the key; the credential still has to be rotated (issue #8236).",
    ),
    (
        "credential_reach",
        "Reports, for each credential the daemon consumes, whether it resolves: present, absent, the store's error kind, or timed out — never the value. A timeout means a macOS Keychain approval dialog may be on screen, which is the expected state after a `cargo install` rebuild; approve it once. Runs on the blocking pool and is bounded, so it can hang neither `tm doctor` nor the runtime (issue #8236).",
    ),
    (
        "hooks_contamination",
        "Warns when a project's `.claude/settings*.json` still carries tm hook entries from a pre-fix `tm install` — suggests `tm hooks clean` (issue #2940).",
    ),
    (
        "hooks_foreign_conflict",
        "Informational: warns when a project's `.claude/settings*.json` carries foreign (claude-mpm) hook entries that would fire inside a tm session — never auto-removed (issue #2940).",
    ),
    (
        "hooks_build_tree_binary",
        "Warns when a hook or `statusLine` command in a project's `.claude/settings*.json` runs a binary that lives in a Cargo build tree, naming the file and each offending command. Such a command stops working the moment the artifact is rebuilt away, which silently disables PM-guard enforcement. `tm doctor --fix` previews repointing every one of them at the installed `tm` binary — machine-wide, not just this project — and `--yes` applies it, taking a timestamped snapshot of each file first; `statusLine.command` is repaired by the same pass (issue #7262).",
    ),
    (
        "hooks_missing_tm_group",
        "Warns when a tm-provisioned project `.claude/settings*.json` carries NO tm hook group for one of the six lifecycle events, naming the file and each missing event. Only `prepare_session` wrote those groups and no resume or in-place relaunch reached it, so a project provisioned before an event existed never gained it — a missing `SessionStart` means `tm hook` never fires, no savings row is written under the live Claude session id, and the 💸 statusline segment stays hidden. `tm doctor --fix` previews merging the groups back in and `--yes` applies it, preserving every other entry and snapshotting the file first (issue #7490).",
    ),
    (
        "tcc_taint",
        "macOS: whether managed panes spawn `claude` with TCC responsibility disclaimed so its data-access prompts aren't attributed to the shared tmux server (issue #2997).",
    ),
    (
        "scaffold_tracking",
        "Warns when a harness-scaffolding path (`.claude/agents/`, `.claude/skills/`, `.claude/output-styles/`) is BOTH tracked in git AND regenerated locally by tm — the precondition for a `git merge --ff-only` \"would be overwritten\" collision; reports the exact true-intersection paths, never auto-modifies the index (issue #3427).",
    ),
    (
        "push_guard",
        "Warns when the project's clone carries no trusty-mpm cross-branch `pre-push` guard, or an older revision of it — the guard installs itself only on the clone path, so a base provisioned before it shipped is silently unprotected and a worktree tracking a foreign branch can force-push over that branch's reviewed lineage. Names the `tm repair push-guard` retrofit; doctor never writes into a repository (issue #2867).",
    ),
    (
        "maintenance_config",
        "Warns when a registered base clone carries more than 3 worktrees without `maintenance.auto=false` pinned in its own local git config — operator-run git in any of those worktrees can still trigger a `git maintenance run --auto` repack against the shared object store. Names the exposed base clone(s) and the `git config --local` fix. Read-only (issue #7171).",
    ),
    (
        "maintenance_processes",
        "Warns when more than one `git maintenance run` process is live on the host at once — the storm pattern from the originating incident (41 concurrent repacks against one shared object store). Read-only: `ps -A -o command=`, never a signal or a kill (issue #7171).",
    ),
    (
        "background_sweeps",
        "One line per background maintenance sweep — the merged-PR worktree reclaim and the in-project hygiene pass: the environment variable that switches it off (`TRUSTY_MPM_WORKTREE_RECLAIM`, `TRUSTY_MPM_INPROJECT_HYGIENE`), whether it is on, whether a pass is in flight, and how long the last completed pass took. Warns while a pass is running or when the last one exceeded 60 s, because that is the state that costs request latency. Read from the state each sweep updates at both edges of a pass — the daemon's own in-memory timings plus a marker under `~/.trusty-mpm/sweeps/`, because `tm doctor` runs daemonless and in-memory timings alone reported every sweep idle while one was running. A marker whose writing daemon is gone reads as idle, never as a pass stuck in flight (issues #7965, #8059).",
    ),
    (
        "binary_provenance",
        "Where the RUNNING binary came from and whether that source still exists: reads cargo's own `$CARGO_HOME/.crates2.json` install ledger and compares it against the running executable. Fails when the same binary is provided by more than one install, when the running binary is OLDER than the ledger's record for that same file or the two cannot be ordered as semver, or when a `cargo install --path` source directory has been reaped (no provenance, no upgrade path). Warns for a live path/git install, which is invisible to registry update detection. Reports UNKNOWN — never `Ok` — when the ledger is unreadable, does not cover the binary (a prebuilt-installer or package-manager install), or records a version OLDER than what is running, which means the ledger no longer describes the file on disk (issue #4964). Read-only; never installs, moves, or deletes (issue #4033, ADR-0021).",
    ),
    (
        "bundled_asset_lag",
        "Warns when the skill assets tm DEPLOYS differ from the `origin/main` `content/skills` tree — the only row whose reference point is the merged tree rather than the deploy source itself. Until #9012 the skills were `include_str!`-embedded, so an installed binary deployed the text as of its build: on 2026-09-23 a binary built at 07:26:46Z kept deploying pre-fix `tm-epic/references/manual-procedure.md` text for nine hours after the fix merged, and then REVERTED six files a newer build had already deployed correctly. Since #9012 the skills are runtime content, so the same lag lives in the content tm reads: a checkout's working tree behind `origin/main`, or an installed content bundle older than it. `skill_staleness` cannot see this — it compares deployed files against that same source by design (#4604). Applies ONLY in `bobmatnyc/trusty-tools`, resolved from the `origin` remote through the same derivation `rust_build_env` uses; anywhere else the row reports not applicable and issues no git command. Hashes every skill of the content tm resolves for the project with `skill_bundle_stamp`'s own per-entry concatenation (the bundle path, a NUL byte, the contents, a newline), reads the same paths out of `origin/main`, and names the lagging keys, the content source, this binary's build-id timestamp, and the newest asset commit's. Reports UNKNOWN — never `Ok` — when the source tree or the content could not be read (no `origin/main`, no git, not a repo, an empty asset tree, a non-UTF-8 blob, no instructional content): a staleness detector that passes when it cannot read the source converts \"unknown\" into \"fine\". Remedy: from a checkout, fast-forward it (`git merge --ff-only origin/main`); from an installed bundle, `tm content update`; then redeploy, which happens automatically on the next managed spawn or explicitly via `tm reinstall` or `tm doctor --fix-skills`. Read-only; it never fetches, installs, or deploys (issues #8482, #9012).",
    ),
    (
        "content",
        "The runtime instructional-content source and pin (ADR-0064): the source `tm` reads (`dev` for a trusted trusty-tools checkout above the project, `bundle` for the installed release, `none`), the pinned `content-vX.Y.Z` tag and sha256 from `~/.trusty-mpm/content/content-lock.toml`, and the binary version, which may legitimately differ from the content version. The bundle is verified with the runtime resolver itself — sha256 against the lock, the manifest tag, the archive caps and the schema major. `Ok` for a dev checkout or a verified bundle; reports INFO (an `Ok` row whose message starts `info:`) when nothing is installed while this binary still compiles its content in, and WARNS once ADR-0064 PHASE_1 removes it, either way naming `tm content update` and the manual install (`gh release download <tag>` then `tm content install --from <dir>/<tag>.tar.gz`); never fetches the release itself (#9396); FAILS when the lock or the bundle fails verification and no checkout serves (issue #8378). Read-only.",
    ),
    (
        "session_store",
        "Whether `~/.trusty-mpm/session-manager/sessions.json` still LOADS — validated against the same type the daemon deserializes, so this check can never call a store healthy that the daemon rejects. A store the daemon cannot read blocks every write (each write path reloads before it saves) while `tm ls` keeps serving the daemon's in-memory copy, so the condition is otherwise invisible until someone attempts a mutation. Fails with the byte offset where the valid document ends, and names `tm repair session-store` only when truncating there would actually leave a loadable store. An absent store is `Ok` (a machine that has never run a managed session has none); an unreadable one is UNKNOWN, never `Ok`. Read-only — it never truncates or writes (issue #5007).",
    ),
    (
        "session_claudes",
        "Whether `~/.trusty-mpm/session-claudes.json` — the first-`SessionStart` registry that binds each session id to its `claude` process (#8531) — is a file the daemon trusts, read through the daemon's own rules. A file that is unreadable, corrupt, owned by another uid, or open to other users SEALS the registry: no session may repair its own delegation records, `PATCH /sessions/{id}/pid` is refused for every session, and the session reaper skips every session. Warns — never `Ok` — naming the refusal. The daemon reads the file only at start, so it stays sealed until restart even after the file is fixed: fix or remove the file, then `tm restart`. An absent file is `Ok` (no session has announced itself over the socket yet). Read-only (issue #8980).",
    ),
    (
        "stop_spool",
        "Undelivered `SubagentStop` records parked under `~/.trusty-mpm/unposted-stops/`. A stop the hook cannot POST is written there so the daemon's reap loop can replay it instead of the delegation sitting `Running` for the six hours of `RUNNING_STALE_AFTER_SECS`. `Ok` when nothing is parked, and equally when every record is younger than 120 s — the drain runs on the reap loop's 60 s tick, so a fresh park is the mechanism working and the row reports the count without calling it a fault. Warns with the count and the oldest record's age once that age passes two ticks, which is the first point a drain has provably not run; names the cap when the spool is saturated (further stops are DROPPED); and warns when the spool is not writable BY THIS PROCESS — probed by creating and removing one temp file, not by reading the mode bits, which say only whether somebody may write — the one branch where a stop is lost outright. Creates nothing that outlives the check (issue #6556).",
    ),
    (
        "stray_mcp_json",
        "Warns when a `.mcp.json` sits ABOVE the workspace or in a temp root. Claude Code discovers `.mcp.json` by walking UP from a session's cwd, so such a file silently supplies the MCP servers of every session started beneath it — agent scratchpads under `/tmp` included — with nothing in the project to point at. The scan is bounded: the workspace's strict ancestors up to the home directory (never the filesystem root, never a recursive descent) plus `$TMPDIR` and `/tmp`. Each finding names the servers it declares and what tm can PROVE about who wrote it, read from the `mcp-json-provenance.json` ledger rather than guessed from content — a file full of `trusty-*` servers may equally be one the operator wrote. Read-only. `tm doctor --fix` quarantines only ledger-proven tm writes (renaming them aside, never deleting); everything else is refused and needs `tm doctor --quarantine-mcp <path>`.",
    ),
    (
        "session_scope",
        "Names what a project's sessions load — tm's trusty-* builtins, the operator's user-scope servers from the protected `.claude.json` (which load in every session since issue #7892), and each `.mcp.json` entry with the approval state Claude Code itself holds for it — plus the installed Claude Code plugins the project does NOT load and the `.trusty-mpm.toml` `[session] plugins` key that opts each one back in (issue #7422). Also compares the project's `.claude/settings.json` against the `enabledPlugins` map `prepare_session` would write and names every missing or divergent key: until issue #7678 the check reported the DECISION while a project that missed the write — a session paused before it existed, or resumed across the upgrade that added it — kept loading every user-tier plugin, with nothing on screen saying so. `Warn` when a plugin is excluded, the settings file does not carry the decision, or the protected `.claude.json` could not be read; `Ok` otherwise; never `Fail`. Read-only; `tm doctor --fix --yes` owns the write, re-applying both the project-tier `enabledPlugins` map and the composed session-MCP file through the launch path's own writers, and only for a project carrying a `.trusty-mpm/` marker.",
    ),
    (
        "tmux_options",
        "Whether the live tmux SERVER's globals still match tm's spec — `history-limit`, `mouse`, and the window-scoped `alternate-screen`. `create_managed_session` applies and verifies them before every pane tm creates, but a server tm did not start carries none of them: a tmux-continuum restore recreates `tm-*` sessions through tmux-resurrect's own bare `new-session`, so restored panes bake tmux's factory 2000-line scrollback and can enter the alternate screen (issue #6469). Warns naming each drifted option; UNKNOWN — never `Ok` — when no option could be read (no tmux binary, or no server running). A green row means NEW panes will be correct: `history-limit` is captured into a pane's ring buffer at creation and cannot be grown in place, so an affected session has to be restarted. Read-only — it reads options, never sets one.",
    ),
    (
        "launchd_process_type",
        "The launchd `ProcessType` of the two tm jobs that start tmux servers — the mpm agent and its supervisor (`trusty_common::launchd_labels::MPM` and `MPM_SUPERVISOR`), read from `~/Library/LaunchAgents`. A tmux server inherits the class of the job that started it, and so does every session inside it: the supervisor plist declared `Background`, which held tmux, the PM sessions and their `cargo` gates at Darwin priority 4 on efficiency cores with throttled I/O, a clamp `taskpolicy -B` cannot lift (issue #8415). FAILS on `Background`; warns on any other value short of `Interactive`, including an absent key, which is launchd's throttled `Standard` default. Each finding names `tm doctor --fix --yes` first, which rewrites the plist file and never reloads launchd (issue #8562), or the `plutil -replace` command, then when launchd applies the class for that label: when it next LOADS the label — at login, or `launchctl bootout` then `bootstrap`, with the path single-quoted — never on a `launchctl kickstart` or a crash respawn, and a tm daemon restart never reloads the supervisor. An `Interactive` plist changed since this boot WARNS `written; pending reload`: the row does not read the loaded class, because `launchctl print` prints the job's EnvironmentVariables; a tmux server already running keeps its class until it exits. A duplicated key resolves to the last one, as CoreFoundation does. UNKNOWN when a plist exists but cannot be judged: a binary plist, an empty `<string/>`, or a non-string value. `Ok` when neither plist is installed. `TRUSTY_MPM_LAUNCH_AGENTS_DIR` points the row at another directory, and every message then names that directory. Read-only.",
    ),
    (
        "tmux_priority",
        "The Darwin scheduling priority of the RUNNING tmux server, read with `tmux display-message -p '#{pid}'` and `ps -o pri= -p <pid>`. A server keeps the class it started with, so fixing a plist `launchd_process_type` flagged does not lift a server that is already clamped (issue #8415). FAILS below priority 20 (a `Background` job's tree runs at 4), naming the server PID, the observed priority, and the remedy: fix the plist, then restart the tmux server (`tmux kill-server` ends every session in it; `tm` resumes them). WARNS from 20 to 30, launchd `Standard` throttling, pointing at the `launchd_process_type` row. `Ok` at 31 (an interactive shell's priority) or above, and when no tmux server is running. macOS only: on other platforms `ps -o pri` uses a different scale, so the row reports not applicable. UNKNOWN — never `Ok` — when tmux cannot be run, `ps` fails, or either answer cannot be parsed. Read-only.",
    ),
    (
        "tcp_listeners",
        "Which trusty-* processes hold a TCP socket in LISTEN state, graded against the ADR-0032 allowlist `crates/trusty-mpm/src/daemon/tcp_listener_allowlist.tsv` — the file the CI lint `scripts/check_no_tcp_listeners.sh` reads too (issue #8926). Only trusty-console may listen on TCP. Probed through libproc on macOS and `/proc` on Linux, never `lsof`. `Ok` when only the console listens, or nothing does; WARNS naming the issue for a `temporary` row's daemon still on TCP (tm #6288); FAILS on any other trusty-* listener, a trusty-search one included (no trusty-search process binds TCP since #9214 and #9542), including an opt-in `--http`/`--api` server. UNKNOWN — never `Ok` — when the probe cannot run or a trusty-* process's sockets cannot be read. Read-only.",
    ),
    (
        "pty_headroom",
        "How much pseudo-terminal capacity the host has left. Every tmux pane holds one pty and macOS caps the total at `kern.tty.ptmx_max` (511 by default), so a session leak presents as a bare `ENXIO` on the next spawn, naming neither the limit nor the leak — 456 sessions the stalled orphan-GC never reaped put this host over the cap (issue #6529). Warns at 80% of the cap, FAILS at 95%, and names both numbers plus what actually reaps the sessions holding them. UNKNOWN — never `Ok` — when either number could not be read. macOS only; other platforms report the skip. Read-only — it counts device nodes and reaps nothing.",
    ),
    (
        "log_drain",
        "Whether the cloud log drain is configured, where it points, and how its last pass ended (issue #6535). The drain uploads the daemon's own log files to an object store on an interval; it is OFF unless `log_drain.enabled` is set in `~/.trusty-tools/trusty-mpm/config.yaml`, and a host that never configured one reports `Ok`. A `log_drain:` section that does not resolve — a malformed destination URI, a zero interval, a source with no root — reports `Fail`, because the daemon refuses to start the scheduler and no bytes move. Enabled but never observed running reports `Warn`. The last pass's verdict is read from `~/.trusty-mpm/log-drain/status.json`, and a pass that errored — including one that finished with per-file failures — reports `Fail`, never a drained-looking `Ok`. Read-only: this probe never drains and never connects to the destination.",
    ),
    (
        "startup_context",
        "Whether this project's turn-1 startup context — what the first assistant turn re-sent, `input + cache_creation + cache_read` — sits inside its budget (issue #7424, parent #4513). Every managed session records that one number when its first turn lands, keyed by Claude session id beside the savings ledger under `~/.trusty-mpm/usage/`; this row samples the newest ones recorded for the project `tm doctor` was run in and compares the median and the latest against `startup_context.ceiling_tokens` in `~/.trusty-tools/trusty-mpm/config.yaml` (default 50,000 tokens over 10 sessions). Warns — never Fails — when either reaches the ceiling, because the ceiling is an operator budget and a prompt the operator deliberately grew is a preference, not a defect. A project with no reading yet reports UNKNOWN rather than `Ok`: nothing was measured, so nothing passed. It opens NO transcript — each number was measured by the session that owned it — so it cannot read another project's session data.",
    ),
    (
        "auto_memory",
        "Whether this project's auto-memory FALLBACK posture is correct (issue #7685). `trusty-memory` is the memory; Claude Code's own auto memory (`MEMORY.md`) stays on only while trusty-memory is unavailable (owner ruling 2026-09-12), and a managed launch turns it off two ways — the project-tier `autoMemoryEnabled: false` settings key and `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` on the spawned session — both written only when trusty-memory answered. The settings key is written TWO-WAY: when trusty-memory does not answer the launch writes `autoMemoryEnabled: true` instead, so a project an earlier healthy launch turned off gets its fallback back rather than keeping a `false` nothing could ever revert. Neither is observable afterwards, so this row reads the EFFECTIVE `autoMemoryEnabled` value across the same four layers Claude Code consults — project local, project, user local, user — names the layer it resolved from, reads whether the project's `MEMORY.md` still holds facts, and grades the pair against a live trusty-memory health probe. `Fail` when trusty-memory is healthy and auto memory is on (an unset key counts as on: Claude Code documents it ON by default), when a healthy trusty-memory sits beside an index that still holds facts, or when a settings file could not be parsed. `Warn` when trusty-memory is down and auto memory is OFF — this project has NO memory at all, the state the two-way write exists to prevent. `Ok` when trusty-memory is healthy, auto memory is off and the index is empty or absent, and equally when trusty-memory is down and auto memory is carrying the project — the fallback working as designed. Read-only; `tm doctor --fix --yes` writes the key into the PROJECT tier through the same merge the launch path uses, in the direction reachability dictates — `false` beside a healthy trusty-memory, `true` to restore the fallback beside one that is not answering — preserving every other key, and never rewrites a `settings.local.json` the operator owns. `--fix` never migrates — `tm memory import-auto-memory` stores each fact in the palace, archives the file, and only then empties the index, and the row names it.",
    ),
    (
        "ancestor_claude_md",
        "Every `CLAUDE.md`, `CLAUDE.local.md` and `.claude/CLAUDE.md` in a directory ABOVE the project root (issue #7673). Claude Code loads memory files from the session's cwd up to the filesystem root, so one file at `$HOME` is prepended to every turn of every agent in every project beneath it — and no other doctor row looks above the project, which is how a tm seed template sat at `$HOME` on 2026-09-12 costing every turn. The project root is the NEAREST enclosing directory holding a `.git` entry or a `.trusty-mpm` directory, whichever comes first walking up from the directory `tm doctor` runs in; `$HOME` itself is never a boundary, because `~/.trusty-mpm` is tm's global state directory and a dotfiles `.git` would otherwise swallow every project under it. A linked worktree nested inside its main checkout (Claude Code's `.claude/worktrees/<name>`) resolves to that checkout, which is the project identity Claude Code itself gives it; an independent clone and a submodule keep their own boundary. With no boundary, the start directory is the root. Reports each file's path, byte size and a token estimate (bytes/4, a rule of thumb, not a measurement). `Ok` when nothing above the root loads — including when every such file is already named in `claudeMdExcludes`, read as the union across the project, user and managed settings layers. `Warn` when a file with real content loads, and `Warn` naming the error when the scan cannot run at all (a directory that does not resolve, an ancestor probe failing for a reason other than a denied permission) — a scan that did not happen is never `Ok`. An ancestor directory that denies permission is skipped rather than failing the scan: the row is at least `Warn` and names each such directory as not checked. `Fail` when a file is tm's own seed template with no project content added. Read-only. `tm doctor --fix --yes` renames a pure seed template aside to `<name>.stale-seed-<YYYYMMDD>` (reversible, refused when that name exists) and adds a content-carrying file's absolute path to `claudeMdExcludes` in the resolved root's own `.claude/settings.local.json`, leaving the file untouched. The same scan drives a WARN at session launch and a stderr notice from `tm session instructions`. Nothing at or below the root is ever a finding or a target.",
    ),
];

/// Render the full doctor-check reference.
///
/// Why: an operator triaging a `tm doctor` `Warn`/`Fail` needs to know what
/// each check name actually probes without reading five source files.
/// What: one table row per [`DOCTOR_CHECKS`] entry, in `run_doctor`'s
/// execution order (not re-sorted — order communicates the probe sequence).
/// Test: `doctor_render_contains_known_check`.
pub(crate) fn render() -> String {
    let mut out = String::new();
    out.push_str("# Doctor Check Reference\n\n");
    out.push_str(
        "Generated from a maintained literal list cross-checked against \
         `run_doctor`'s actual check names (see this module's \
         `doctor_checks_match_run_doctor_names` test — an added, removed, or \
         renamed check fails the test suite). Source: \
         `crates/trusty-mpm/src/daemon/doctor.rs` and its five sibling \
         `doctor_*.rs` files. Regenerate with `tm generate capabilities`.\n\n",
    );
    let _ = writeln!(out, "{} checks, in execution order.\n", DOCTOR_CHECKS.len());

    out.push_str("| # | Check | What it probes |\n|---|---|---|\n");
    for (i, (name, description)) in DOCTOR_CHECKS.iter().enumerate() {
        let _ = writeln!(out, "| {} | `{name}` | {description} |", i + 1);
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use trusty_mpm::daemon::doctor::run_doctor;

    #[test]
    fn doctor_render_contains_known_check() {
        let rendered = render();
        assert!(rendered.contains("`instructions`"), "{rendered}");
        assert!(rendered.contains("`oauth_token`"), "{rendered}");
    }

    #[test]
    fn doctor_render_is_deterministic() {
        assert_eq!(render(), render());
    }

    /// The drift guard: `DOCTOR_CHECKS`'s name list must exactly match what
    /// `run_doctor` actually produces, in the same order. This is the
    /// "unit test asserting the generator's doctor list length == the
    /// doctor's actual check count" the issue #2913 brief requires — and
    /// goes further by also asserting name equality, not just length.
    #[tokio::test]
    async fn doctor_checks_match_run_doctor_names() {
        // #8415 owner rule: the bin builds the library without `cfg(test)`, so
        // point the `launchd_process_type` row at a temp path that does not
        // exist, never the operator's real `~/Library/LaunchAgents`. An
        // injected override, not an env write (#5544).
        trusty_mpm::daemon::doctor_launchd_process_type::override_launch_agents_dir(
            std::env::temp_dir().join(format!(
                "tm-bin-test-no-launch-agents-{}",
                std::process::id()
            )),
        );
        let report = run_doctor(None, None, &[], None).await;
        let actual: Vec<&str> = report.checks.iter().map(|c| c.name.as_str()).collect();
        let expected: Vec<&str> = DOCTOR_CHECKS.iter().map(|(name, _)| *name).collect();
        assert_eq!(
            actual, expected,
            "generate::doctor::DOCTOR_CHECKS has drifted from run_doctor's actual checks"
        );
    }
}