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
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
//! The `tm doctor` diagnostic engine.
//!
//! Why: a misconfigured trusty-mpm stack fails in confusing ways — sessions
//! launch with no instructions, agents never deploy, memory recall silently
//! returns nothing. `tm doctor` collapses every "is this wired correctly?"
//! question into one command so the operator gets a single, actionable verdict.
//! What: [`run_doctor`] runs independent probes — the instruction pipeline,
//! agent deployment, skill deployment, the DOC-28 output-style
//! configuration check (see `doctor_output_style`), and the trusty-memory /
//! trusty-search sidecars — and folds their outcomes into a
//! [`DoctorReport`]. Each network probe is bounded by [`PROBE_TIMEOUT`] so an
//! unreachable service cannot hang the report.
//! Test: `cargo test -p trusty-mpm-daemon doctor` exercises the filesystem
//! probes against temp directories and the HTTP probes against an in-process
//! test server.
use ;
use Duration;
use crate;
use crateFrameworkPaths;
// Split out to keep this file under the 500-SLOC production cap (#5947 — the
// worktrees probe now reads the reconciled inventory, and its counts type and
// remediation constant came with it).
use check_worktrees;
pub use gather_worktree_counts;
pub use ;
use ;
// Split out to keep this file under the 500-SLOC production cap (DOC-28 R4(a)).
use ;
// Split out to keep this file under the 500-SLOC production cap (A2,
// tm-skills-portfolio epic — adding check_skill_source pushed it over).
use ;
// Split out to keep this file under the 500-SLOC production cap (issue #2158
// — the full manifest-completeness diff, layered on top of the narrower
// check_agents/check_skills/check_output_style probes above).
use check_deployment_completeness;
// #4451: the resolvability half — `check_agents` and
// `check_deployment_completeness` above are both presence-only and stayed green
// while the harness could not reach a single deployed agent.
use check_agent_reachability;
// #4442: the shadowing half — every probe above looks only at the canonical
// deploy tier, so a stale copy of the same agent in a project's
// `.claude/agents/` outranks it and wins resolution with all checks green.
use check_asset_tier;
// #4467: the same silent-failure shape as #4451, one layer down — a managed
// spawn that inherits `CLAUDE_CODE_CHILD_SESSION` has transcript saving turned
// off, so the session is unrecoverable and nothing reported it.
use check_transcript_saving;
// Split out to keep this file under the 500-SLOC production cap (issue #2876 —
// the skill-staleness and legacy-instruction-source probes).
use check_legacy_instruction_sources;
// #4604: deployed-skill drift, compared against the RUNNING BINARY's own
// embedded assets. It used to live in `doctor_staleness.rs` and compare the
// `~/.trusty-mpm/framework/skills` extraction cache against the deploy
// manifest — both sides could be the same stale content, which is how a
// drifted `tm-workflow` reported clean at three tiers at once.
use check_skill_staleness;
// #4033: install provenance of the running binary — "is what's running what
// you think it is?", the observation doctor's liveness+file-state health model
// never made.
use check_binary_provenance;
// #4605: the reachability half for SKILLS — `check_skill_staleness` above
// compares against the deploy MANIFEST, so a bundled skill absent from that
// manifest is outside everything it can see and reports a clean `Ok` while the
// file on disk serves text the current binary removed.
use check_skill_unmanaged;
// Split out to keep this file under the 500-SLOC production cap (DOC-42,
// issue #2889 — the agent-bundled-skills dangling-reference / prose-mention
// probe).
use check_agent_skills;
// #5032: the `gh_account` probe, split out when its tri-state outcome
// (authenticated / unauthenticated / could-not-tell) pushed this file over cap.
use check_gh_account;
// Split out to keep this file under the 500-SLOC production cap (issue #2940
// — the tm hook contamination / foreign claude-mpm hook conflict probe).
// #5274/ADR-0037: `pub(crate)` so the launch path can reuse this probe's own
// file enumeration and parsing instead of growing a second detector.
pub
use check_hooks_hygiene;
// Split out to keep this file under the 500-SLOC production cap (issue #2997 —
// the macOS TCC responsibility-disclaim visibility probe).
use check_tcc_taint;
// Split out to keep this file under the 500-SLOC production cap (issue #3427 —
// the harness-scaffolding tracked-in-git-AND-regenerated-locally probe).
use check_scaffold_tracking;
// Split out to keep this file under the 500-SLOC production cap (issue #2867 —
// the cross-branch push-guard coverage probe, which is what makes an
// unprotected already-provisioned base clone discoverable at all).
use check_push_guard;
// Split out to keep this file under the 500-SLOC production cap (issue #2919 —
// the worktree disk-consumption / merged-PR reclaimability probe, the
// early-warning half of the 1.1 TiB leak post-mortem).
use check_worktree_disk;
// Split out to keep this file under the 500-SLOC production cap (issue #4286 —
// the retired `.trusty-mpm/` override-file probe, the on-demand half of the
// signal that stops the hard cut from silently dropping a project's rules).
use check_legacy_overrides;
// #5045: the resolution half of `check_search` below. That probe asks the
// daemon whether it is healthy and whether the DERIVED id appears in
// `/indexes`; this one resolves the id the session is actually PINNED to. The
// registration path is fail-open at every step, so the pin advances even when
// index creation failed — 4 of 75 live worktrees had an index while `search`
// reported fine.
use check_search_index_pin;
// Split out to keep this file under the 500-SLOC production cap (issue #5007 —
// the managed-session-store integrity probe; a corrupt `sessions.json` blocks
// every write and was previously invisible to every diagnostic).
use check_session_store;
// Claude Code finds `.mcp.json` by walking UP from a session's cwd, so one
// written above real projects configures every session beneath it with nothing
// in the project to point at. Read-only; the quarantine is opt-in.
use check_stray_mcp_json;
/// Per-probe network timeout.
///
/// Why: a sidecar that is down or wedged must not stall the whole diagnostic;
/// a bounded probe turns "hung" into a clean verdict.
///
/// Raised from 2 s to 10 s for issue #4005. The old bound produced
/// "trusty-memory unreachable at 127.0.0.1:7070" against a daemon whose MCP
/// surface was verifiably serving in the same minutes: trusty-memory's
/// `/health` samples process RSS/CPU behind a mutex and enumerates open file
/// descriptors, none of which the MCP request path touches, so under load
/// `/health` can exceed a budget that real traffic never approaches. The probe
/// was measuring its own impatience. Note that the timeout is only half the
/// fix — a timeout now resolves to [`CheckStatus::Unknown`] rather than a
/// false `Fail` (see [`probe_health`]).
pub const PROBE_TIMEOUT: Duration = from_secs;
/// Number of attempts a health probe makes before giving up.
///
/// Why (issue #4005): the probe was single-shot, so one unlucky sample — a GC
/// pause, a burst of concurrent MCP traffic, the warm-up window right after a
/// `tm` restart that the issue explicitly calls out — became a hard failure
/// verdict with no second opinion. Two retries cost nothing on the healthy
/// path (the first attempt succeeds and returns immediately) and remove the
/// single-sample fragility on the unhealthy one.
const PROBE_ATTEMPTS: usize = 3;
/// Delay between health-probe attempts.
///
/// Why: long enough to let a transient spike pass, short enough that three
/// attempts stay well inside an interactive `tm doctor` run.
const PROBE_RETRY_DELAY: Duration = from_millis;
// #4003: the search-index doctor probe used to hardcode a literal expected
// index id ("trusty-mpm" — the crate name, not this repo's registered index
// id "trusty-tools"), so it permanently warned "index missing" against a
// healthy, fully-indexed project. `expected_search_index_id` below resolves
// the id the same way every other launch path does, via
// `trusty_common::derive_index_id`.
/// Run every diagnostic probe and assemble the report.
///
/// Why: the single entry point behind `GET /api/v1/doctor` and `tm doctor` —
/// running all probes here keeps the check set identical across every UI.
/// What: runs the instruction / agent / skill / output-style filesystem
/// probes (the output-style probe closes DOC-28 F4 — the "did the
/// trusty-mpm instructions actually load" gap), the `deployment` probe (issue
/// #2158 — a full manifest-completeness diff against the canonical bundled
/// roster, layered on top of the narrower agent/skill/output-style probes),
/// then the memory and search HTTP probes (each bounded by [`PROBE_TIMEOUT`]),
/// a worktree-orphan scan (Fix 1b, #1840), the `skill_source` probe (A2,
/// tm-skills-portfolio epic), the `gh_account` probe
/// (#gh-account-awareness — surfaces the active github.com identity and warns
/// on the multi-account ambiguity), and the `oauth_token` probe (issue #2246 —
/// warns when a managed session risks the `CLAUDE_CONFIG_DIR`-keyed Keychain
/// login loop), the `skill_staleness` and `legacy_sources` probes (issue #2876
/// — warn when deployed skills differ from the bundled assets, or when legacy
/// global instruction sources linger), and the `agent_skills` /
/// `agent_skills_prose_hints` probe pair (DOC-42, issue #2889 — the former
/// flags dangling `skills:` frontmatter references and can `Warn`; the
/// latter is always `Ok` and carries undeclared prose skill mentions as
/// informational text, per issue #2906 review — folding both severities
/// into one check caused alert fatigue), and the `hooks_contamination` /
/// `hooks_foreign_conflict` probe pair (issue #2940 — the former warns when
/// a project-level `.claude/settings*.json` still carries tm hook entries
/// from a pre-fix `tm install`'s `$HOME`-wide write and points at `tm hooks
/// clean`; the latter warns, informationally only, when a project carries
/// its own claude-mpm hook entries that would fire inside a tm session), and
/// the `output_style_staleness` probe (issue #2333 — content-diffs each
/// deployed output-style file against the bundled catalog and flags orphaned
/// files under `output-styles/`, closing the gap where `check_output_style`
/// only validates that the configured id RESOLVES to a file, not that its
/// content is current), the `output_style_legacy_ids` probe (issue #3453 part
/// 2 — Warn-only scan of every settings LAYER, not just the effective one,
/// for a legacy/unresolvable `outputStyle` id sitting dormant in a currently
/// shadowed layer such as `settings.local.json`), the `tcc_taint` probe
/// (issue #2997 — surfaces whether managed panes spawn `claude` with macOS
/// TCC responsibility disclaimed, so the "would like to access data…" prompt
/// class is diagnosable rather than silent; synchronous and instantaneous, no
/// `log show` scan), and the `scaffold_tracking` probe (issue #3427 — warns
/// when a harness-owned path under `.claude/agents/`, `.claude/skills/`, or
/// `.claude/output-styles/` is BOTH tracked in `project_dir`'s git index AND
/// regenerated locally by tm, the precondition for a `git merge --ff-only`
/// "would be overwritten" collision; reports the exact true-intersection
/// paths plus a copy-pasteable `git rm -r --cached` remediation, never runs
/// it itself), and the `push_guard` probe (issue #2867 — warns when
/// `project_dir`'s clone has no trusty-mpm cross-branch `pre-push` guard, or
/// carries an older revision of it, naming the `tm repair push-guard`
/// retrofit; this is the only way a base clone provisioned BEFORE the guard
/// shipped is discoverable as unprotected), and the `search_index_pin` probe
/// (issue #5045 — resolves the index id the project's `.mcp.json` actually
/// PINS against `GET /indexes/{id}/status` and `Fail`s on a 404; index
/// registration is fail-open at every step, so the pin advances even when
/// creation failed and the `search` probe above stays green) — folding the
/// resulting [`DoctorCheck`]s into a [`DoctorReport`] whose `overall` status
/// is the worst of them.
///
/// Note (#1905): the mpm-*→tm-* stale-skill cleanup is intentionally NOT a
/// permanent probe here — it is a one-time migration
/// ([`crate::core::stale_skills::run_stale_mpm_skills_migration_once`]) run
/// from `tm`'s startup path instead, so it does real work at most once per
/// machine rather than nagging on every `tm doctor` invocation forever.
///
/// `project_dir` scopes the instruction and output-style probes; `repos_root`
/// (when `Some`) gives the managed workspace root for the worktree scan;
/// `active_workspace_paths` is the full set of workspace paths currently
/// registered to live sessions.
///
/// Issue #2149: `check_skills` is ALSO scoped by `project_dir` when it is
/// supplied. A managed session (#1931) deploys its SKILLS under
/// `<workspace>/.claude/skills`, not the operator's `$HOME/.claude` — probing
/// only the home-tier directory previously let a managed workspace with a
/// completely empty roster report a false `Ok` (because the operator's OWN
/// `$HOME/.claude/` was populated), silently missing the exact provisioning
/// gap that issue is about.
///
/// Issue #5867 narrows that scoping to a project that IS a managed workspace,
/// decided by [`is_managed_workspace`] against `active_workspace_paths`. An
/// unmanaged cwd now resolves the home layout, so the three skill deploy tiers
/// stay distinct and `~/.claude/skills` is audited again. Two probes keep the
/// old workspace-scoped layout on purpose — `deployment` and `agent_skills`,
/// whose subject is a workspace's own `.claude/` payload, not the tier model.
///
/// Issue #4409 removes AGENTS from that scoping: bundled agents deploy into
/// the one tm-managed `CLAUDE_CONFIG_DIR` tier and nowhere else, so
/// `check_agents`/`check_agent_skills` probe `paths.agent_deploy_dir()`, which
/// is the same directory whether or not a `project_dir` was supplied.
/// Test: `run_doctor_produces_thirty_two_checks`,
/// `agents_check_probes_the_managed_config_tier_not_the_workspace`.
pub async
/// Is `project_dir` a workspace some live session was provisioned into?
///
/// Why (#5867): [`FrameworkPaths::for_managed_workspace`] rewrites the SKILL
/// deploy destination to `<dir>/.claude/skills`, which is true only of a
/// managed session's own workspace. Every other production call site already
/// passes one; `run_doctor` is the only one handed an arbitrary process cwd,
/// and applying the workspace layout there collapsed the operator-home tier
/// onto the project tier. `active_workspace_paths` is exactly the set of
/// provisioned workspaces — `daemon::api::doctor` builds it from every session
/// record's `workspace_path` — so it is the only input that can answer this
/// without inventing a heuristic.
/// What: canonicalizes both sides (a workspace under `/tmp` resolves through a
/// symlink on macOS, so a raw `==` would miss the match) and reports whether
/// `project_dir` appears in the set. A path that cannot be canonicalized —
/// absent, dangling symlink, unreadable parent alike — falls back to its own
/// raw spelling, so it is then compared verbatim. That is the safe answer in
/// the sense that matters: it can still match a recorded workspace under the
/// exact name the session recorded, but it can never match an unregistered
/// directory, which is the promotion #5867 is about.
/// Test: `unmanaged_cwd_audits_the_operator_home_tier`,
/// `a_registered_workspace_still_gets_the_workspace_layout`,
/// `an_uncanonicalizable_path_is_not_a_managed_workspace`,
/// `an_unreadable_directory_is_not_a_managed_workspace`,
/// `an_absent_path_still_matches_the_recorded_spelling_of_itself`.
/// Probe for the `CLAUDE_CONFIG_DIR`-keyed OAuth login-loop risk (issue #2246).
///
/// Why: every managed spawn relocates `CLAUDE_CONFIG_DIR` to the tm-owned
/// config home; on macOS the Keychain credential Claude Code reads is keyed by
/// a hash of that path, so a `/login` run inside a managed session can
/// diverge from the login stored under the operator's default config dir and
/// loop between "login successful" and "not logged in". Storing a
/// `CLAUDE_CODE_OAUTH_TOKEN` (`tm auth set-token`) bypasses the Keychain
/// entirely — this probe warns BEFORE an operator hits the loop rather than
/// after.
/// What: `Warn` when the managed config dir resolves (true in virtually every
/// real environment — see [`crate::core::trusty_tools_config::managed_claude_config_dir`])
/// AND no OAuth token will resolve for a managed spawn — i.e. neither a stored
/// token file NOR the `CLAUDE_CODE_OAUTH_TOKEN` env var is set (the two sources
/// [`crate::core::oauth_token::resolve_oauth_token`] consults, via
/// [`crate::core::oauth_token::compute_status`]); `Ok` otherwise. Ambient
/// `ANTHROPIC_API_KEY` deliberately does NOT count — every managed spawn strips
/// it with `env -u ANTHROPIC_API_KEY` (see [`crate::runtime::claude_code`]), so
/// relying on it still triggers the #2246 login loop while doctor would
/// otherwise falsely report `Ok`. Never `Fail` — a first `/login` still
/// frequently succeeds even without a stored token, so this is advisory.
/// Test: `oauth_token_check_warns_when_relocated_with_no_token_or_key`,
/// `oauth_token_check_ok_when_token_stored`,
/// `oauth_token_check_warns_when_only_api_key_set`.
/// Pure verdict for [`check_oauth_token_config`], separated for hermetic
/// testing without mutating real process env / home state.
///
/// Why: keeping the branch logic pure makes both the warn and ok paths
/// unit-testable without redirecting `HOME` or the process env.
/// What: `token_available = stored_token_present || env_var_set` — exactly the
/// two sources a managed spawn's [`crate::core::oauth_token::resolve_oauth_token`]
/// consults. `Warn` when `relocated && !token_available`, else `Ok`. Ambient
/// `ANTHROPIC_API_KEY` is intentionally NOT an input: managed spawns scrub it,
/// so it can never satisfy the managed-session auth requirement (#2246 doctor
/// false-negative fix).
/// Test: `oauth_token_check_warns_when_relocated_with_no_token_or_key`,
/// `oauth_token_check_ok_when_token_stored`,
/// `oauth_token_check_ok_when_env_var_set`,
/// `oauth_token_check_warns_when_only_api_key_set`,
/// `oauth_token_check_ok_when_not_relocated`.
/// Probe the trusty-memory sidecar's `/health` endpoint.
///
/// Why: memory recall and store route through trusty-memory; if it is down the
/// PM silently loses its long-term memory, so the operator must know.
/// What: resolves the service address via [`discover_addr`], then delegates to
/// [`probe_health`], which retries, distinguishes a refusal from a timeout, and
/// reads the daemon's own worker-pool observation out of the response body.
/// Test: `memory_unreachable_is_fail`, `memory_timeout_is_unknown_not_fail`,
/// `memory_wedged_worker_pool_is_not_ok`, `memory_warming_is_warn_not_fail`.
async
/// Outcome of one `/health` request attempt.
///
/// Why (issue #4005): the old code collapsed every non-success into a single
/// `Err`, which is what made "timed out" and "connection refused" produce the
/// same "unreachable" verdict despite meaning opposite things operationally.
/// Naming the cases is what lets the caller be honest about which it saw.
/// What: a 2xx with its parsed body, a non-2xx, a timeout, or a refusal.
/// Issue one `/health` request and classify the outcome.
async
/// Probe a sidecar's `/health` and turn what was actually observed into a
/// [`DoctorCheck`] (issues #4005, #4001).
///
/// Why: this function encodes the principle both issues share. Doctor used to
/// infer health from a cheap proxy — "the socket answered" — instead of
/// observing the thing it claims to report. That produced a false NEGATIVE
/// when the proxy was merely slow (#4005) and a false POSITIVE when the
/// listener was fine but every worker was parked (#4001). So: retry before
/// concluding anything, separate "nothing is listening" from "nothing
/// answered in time", and prefer the daemon's own observation of its workers
/// over our inference from the status code.
/// What: up to [`PROBE_ATTEMPTS`] attempts. Returns `Ok` only when the daemon
/// positively reports a healthy worker pool; `Fail` on a refusal, a non-2xx,
/// or a reported wedge; `Warn` while warming or degraded; and `Unknown` when
/// every attempt timed out or the body carried no worker observation.
/// Test: `memory_unreachable_is_fail`, `memory_timeout_is_unknown_not_fail`,
/// `memory_wedged_worker_pool_is_not_ok`, `memory_warming_is_warn_not_fail`,
/// `memory_slow_but_serving_daemon_is_ok`.
async
/// Map a 2xx `/health` body to a status (issues #4001, #4005).
///
/// Why: a 2xx proves a listener accepted a socket, not that the daemon is
/// doing work — which is precisely how #3992 kept `tm doctor` green for the
/// duration of an incident in which six threads were parked and a
/// `memory_remember` had been hung for ~1800 s. When the daemon reports its
/// own worker occupancy, that observation wins over our inference.
/// What: `Fail` on a reported wedge, `Warn` while warming or degraded,
/// `Unknown` when no worker block is present (an older daemon, or an
/// unreadable body — we cannot claim health we did not observe), `Ok`
/// otherwise.
/// Test: `memory_wedged_worker_pool_is_not_ok`, `memory_warming_is_warn_not_fail`,
/// `health_body_without_worker_block_is_unknown`.
/// Probe the trusty-search sidecar's health and this project's index.
///
/// Why: code search backs the PM's "search before grep" rule; both the service
/// being up *and* this project's index existing are required for it to work.
/// What: resolves the service address, checks `GET /health` (a non-2xx or
/// transport error is `Fail`), then checks `GET /indexes` for the index id
/// [`expected_search_index_id`] resolves for `project_dir` — a healthy
/// service missing that index is `Warn`.
/// Test: `search_unreachable_is_fail`.
async
/// Resolve the trusty-search index id `tm doctor` should expect for
/// `project_dir` (#4003).
///
/// Why: the probe previously hardcoded a literal expected index name
/// (`"trusty-mpm"` — the crate name), which diverges from a repo's actual
/// registered index id (e.g. this repo registers as `"trusty-tools"`), so a
/// healthy, fully-indexed project permanently reported "index missing".
/// What: walks up from `project_dir` (falling back to the process cwd when
/// `None`, matching the daemon's own `run_doctor` default) to the nearest
/// git root via [`trusty_common::resolve_project_root`], then derives the id
/// via [`trusty_common::derive_index_id`] — the exact same rule
/// `core::session_launch` uses to register-and-pin a session's index and
/// trusty-search's own `detect_project` uses to resolve a bare `search`
/// call, so all three agree on one id per project (#1373).
/// Test: `expected_search_index_id_derives_from_project_dir_not_hardcoded`.
/// True when `body` mentions an index named `name`.
///
/// Why: the `/indexes` payload shape varies (a bare string array, or objects
/// with an `id`/`name` field); a tolerant scan avoids coupling the probe to one
/// exact wire form.
/// What: returns true when any array element equals `name` directly or carries
/// an `id`/`name`/`index_id` field equal to `name`.
/// Test: `index_present_matches_each_shape`.
/// Issue a timeout-bounded `GET` and report whether the status is 2xx.
///
/// Why: the memory and search health probes only need a yes/no liveness answer.
/// What: `GET url` with a [`PROBE_TIMEOUT`] client timeout; `Ok(true)` on a 2xx
/// response, `Ok(false)` on any other status, `Err` on a transport failure.
/// Test: covered by `memory_unreachable_is_fail`.
async
/// Issue a timeout-bounded `GET` and parse the response body as JSON.
///
/// Why: the search index check needs the `/indexes` payload, not just its
/// status code.
/// What: `GET url` with a [`PROBE_TIMEOUT`] client timeout; returns the parsed
/// [`serde_json::Value`], `Err` on a non-2xx status or a transport / parse
/// failure.
/// Test: covered by `search_unreachable_is_fail`.
async