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
//! trusty-search MCP stub + project-index registration/reindex helpers.
//!
//! Why: split out of `settings.rs` (issue #610 SLOC cap) — the trusty-search
//! index lifecycle (build the pinned MCP stub, find-or-create the index, then
//! best-effort trigger a reindex so it is actually populated) is a cohesive
//! concern distinct from the output-style/hook/trust-preseed helpers that
//! remain in `settings.rs`.
//! What: [`trusty_search_mcp_value`] / [`inject_trusty_search_mcp`] build and
//! write the `.mcp.json` stub (issue #1373); [`register_project_index`] +
//! [`best_effort_create_index`] + [`best_effort_trigger_reindex`] +
//! [`index_is_fresh`] find-or-create the daemon-side index and ensure it is
//! populated (issue #1908).
//! Test: each `pub(super)` function has a dedicated test in `tests.rs`.
use Path;
use PrepError;
use inject_mcp_server;
/// Build the `trusty-search` MCP server definition injected into a project's
/// `.mcp.json`, optionally pinned to a project index (issue #1373).
///
/// Why (issue #1270 / step 4): trusty-mpm-spawned sessions need the code-search
/// tools (`search`, `grep`, `get_call_chain`, …). Issue #1373: without pinning,
/// the contextless `trusty-search serve` stub left index selection to the LLM,
/// which routinely queried the WRONG index (usually the persistent `claude-mpm`
/// one) instead of the session's own project. Passing `--index <derived-id>`
/// pins the session to its project index so a bare `search` always resolves
/// correctly and fan-out never sweeps every index. The canonical stdio MCP
/// invocation is bare `serve` (stdio is the default transport; HTTP is off
/// unless `--with-http`).
/// What: returns the JSON `Value` for a stdio MCP server running
/// `trusty-search serve` — with `["serve", "--index", "<id>"]` when `index_id`
/// is `Some` (non-empty), else the unpinned `["serve"]`. Index *creation* is
/// handled separately by [`register_project_index`].
/// Test: `trusty_search_mcp_value_pins_index`, `trusty_search_mcp_value_unpinned`.
pub
/// Inject the `trusty-search` MCP server into the project's `.mcp.json`,
/// pinned to the project's index when one is known (issue #1373).
///
/// Why (issue #1270 / step 4): spawned sessions must reach the code-search
/// tools, but `trusty-search` was never registered alongside `trusty-memory`.
/// Issue #1373: the stub must additionally PIN the session to the project's own
/// index (`--index <id>`) so queries never resolve to the wrong index. When
/// `index_id` is `None` (derivation failed) the unpinned stub is written so the
/// session still gets the tools — backward-compatible with the pre-#1373 stub.
/// What: builds the (optionally pinned) server value via
/// [`trusty_search_mcp_value`] and registers it under the key `trusty-search`.
/// Test: `inject_trusty_search_mcp_adds_server`,
/// `inject_trusty_search_mcp_preserves_existing`,
/// `inject_trusty_search_mcp_is_idempotent`,
/// `inject_trusty_search_mcp_pins_index`,
/// `inject_both_mcp_servers_coexist`.
pub
/// Find-or-create the trusty-search index for `project_root`, best-effort
/// trigger a reindex so it is actually populated, and return its id (issues
/// #1373, #1908).
///
/// Why: pinning the serve stub to an index id is only useful if that index
/// actually exists in the daemon — otherwise a query against it returns nothing
/// and the LLM falls back to guessing (the very bug #1373 fixes). At session
/// launch we therefore derive the project's canonical index id (the same rule
/// trusty-search's `detect_project` uses, via the shared
/// `trusty_common::derive_index_id`) and best-effort register it with the
/// running daemon. The daemon's `POST /indexes` is idempotent (returns
/// `created: false` for an existing id), so a re-register is safe and cheap.
/// Issue #1908: `POST /indexes` alone only registers an EMPTY index and starts
/// a future-changes file watcher — it never walks the existing tree (see
/// `trusty-search/src/service/server/indexes.rs::create_index_handler`). Without
/// an explicit reindex trigger, a freshly pinned session index stays empty
/// until *something* changes on disk, so the very first `search` query in a
/// spawned session silently returns nothing. [`best_effort_trigger_reindex`] is
/// therefore called right after registration, in the same reachable-daemon
/// branch, so both calls share one "is the daemon up" check.
/// What: resolves the git-root for `project_root`, derives the index id, and —
/// when the id is non-empty AND the trusty-search daemon address is discoverable
/// — POSTs `{id, root_path}` to `/indexes` then best-effort triggers a reindex
/// (skipping it when the index is already fresh; see
/// [`best_effort_trigger_reindex`]). ALWAYS returns the derived id (`None` only
/// when derivation yields an empty string) so the caller can pin the stub even
/// if the daemon is unreachable; every failed/skipped step is logged at
/// warn/debug and never propagates (the session must still launch).
/// Test: `register_project_index_returns_derived_id` (derivation + daemon-down
/// graceful path) in `tests.rs`.
pub
/// POST `/indexes` to find-or-create `index_id`; failures are logged, never
/// propagated (issue #1373).
///
/// Why: registration is best-effort — a daemon that is briefly unreachable, or
/// an HTTP hiccup, must NOT abort session launch. Isolating the blocking HTTP
/// call here keeps [`register_project_index`] readable and the error handling
/// in one place.
/// What: issues a short-timeout blocking `POST {base}/indexes` with body
/// `{id, root_path}` ON A DEDICATED OS THREAD. `prepare_session` is synchronous
/// but is frequently invoked from inside a tokio runtime (the daemon/TUI launch
/// paths); creating `reqwest::blocking`'s internal runtime directly there panics
/// with "Cannot drop a runtime in a context where blocking is not allowed".
/// Running the blocking client on a freshly-spawned `std::thread` (joined here)
/// keeps that nested runtime entirely off the async worker, so the call is safe
/// from both sync and async callers. A non-2xx response or transport error is
/// logged at warn/debug and swallowed; the daemon endpoint is idempotent so
/// re-creates are harmless. The client uses a tight ~1s overall timeout
/// (750 ms connect) so the joined thread returns quickly: this call sits on the
/// `prepare_session` hot path and must NOT stall a session launch when the
/// daemon is slow or unreachable. Registration is purely best-effort — the
/// index is also created on the first reindex — so a short cap is acceptable.
/// Test: exercised via `register_project_index_returns_derived_id` (daemon-down
/// path) and `launch_session_errors_when_daemon_unreachable` (async-context
/// safety); the live HTTP path is covered by integration use.
/// Best-effort, non-blocking trigger of a trusty-search reindex for `index_id`
/// (issue #1908).
///
/// Why: [`best_effort_create_index`] only find-or-creates an EMPTY index — the
/// daemon's `POST /indexes` handler registers the id and starts a
/// future-changes file watcher but never walks the existing tree. Without an
/// explicit reindex trigger, a freshly pinned session index stays empty until
/// *something* changes on disk, so the very first `search`/`grep` query in a
/// spawned session silently returns nothing. `POST /indexes/{id}/reindex` is
/// fire-and-forget server-side — it `tokio::spawn`s the walk and returns
/// `{"queued": true}` almost instantly — so triggering it here does not risk a
/// long stall; the short dedicated-thread timeout below guards against the
/// (much rarer) case where even the initial HTTP round trip itself is slow.
/// What: on a dedicated OS thread (mirroring [`best_effort_create_index`]) with
/// a ~2s overall / 750ms connect timeout: first does a cheap `GET
/// {base}/indexes/{id}/status` freshness probe (see [`index_is_fresh`]) and
/// skips the reindex entirely when the index already has chunks and was
/// indexed within the last hour; otherwise POSTs `{base}/indexes/{id}/reindex`.
/// A failed status probe is treated as "not fresh" (fail-open toward
/// reindexing). Every outcome — skipped, triggered, non-2xx, transport error,
/// panicked thread — is logged at warn/debug and swallowed; the daemon-side
/// reindex is itself idempotent, so calling it redundantly is harmless, and
/// session launch must never block or fail because trusty-search is
/// unreachable or slow.
/// Test: `index_is_fresh_true_when_recently_indexed_with_chunks`,
/// `index_is_fresh_false_when_no_chunks`, `index_is_fresh_false_when_stale`,
/// `index_is_fresh_false_when_last_indexed_missing_or_malformed` in `tests.rs`;
/// the live-HTTP trigger path is exercised the same way
/// `best_effort_create_index` is (daemon-down graceful path via
/// `register_project_index_returns_derived_id`).
/// Whether a `GET /indexes/{id}/status` response body represents an index
/// fresh enough that [`best_effort_trigger_reindex`] should skip reindexing
/// (issue #1908).
///
/// Why: pure predicate over the JSON body so the freshness rule is unit
/// testable without a live daemon — [`best_effort_trigger_reindex`] is
/// otherwise pure I/O. Skipping redundant reindexes avoids reindex spam on
/// every session launch of an already-fresh workspace.
/// What: returns `true` when `chunk_count` is a positive integer AND
/// `last_indexed` parses as an RFC3339 timestamp no more than one hour in the
/// past (clock skew that makes it appear in the future is also treated as not
/// fresh, out of caution). Any missing/malformed/zero field returns `false`
/// (fail-open toward reindexing, never toward skipping).
/// Test: `index_is_fresh_true_when_recently_indexed_with_chunks`,
/// `index_is_fresh_false_when_no_chunks`, `index_is_fresh_false_when_stale`,
/// `index_is_fresh_false_when_last_indexed_missing_or_malformed`.
pub