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
//! CLI-side managed-workspace provisioning that honours the #3455 worktree
//! opt-out (#4300).
//!
//! Why: two `tm` entry points create base clones and per-session worktrees
//! entirely IN THE CLI PROCESS — `tm launch` (which never asks the daemon)
//! and the daemon-unreachable guided fallback (which runs precisely because
//! the daemon is down). Both predate #3455's per-project `worktree: false`
//! opt-out and neither consulted it, so the setting was silently conditional
//! on the daemon being up: a project registered with `worktree: false` still
//! got a clone and a worktree from either path. Concentrating the decision
//! and the provisioning here means the opt-out is read once per launch, in
//! the SAME order the daemon uses (`managed_routes::lifecycle`, ahead of
//! `try_inproject_spawn`) — BEFORE `ensure_base_clone`, so an opted-out
//! project creates neither a worktree nor a base clone.
//! What: [`ManagedWorkspace`] names the two outcomes; [`provision`] is the
//! shared dispatcher that acts on an already-resolved decision;
//! [`provision_for_launch`] serves `tm launch` (which has already resolved
//! the base-clone path) and [`provision_for_fallback`] serves the guided
//! fallback (which parses `owner/repo` itself and reports daemon-specific
//! remediation). Each entry point resolves the decision EXACTLY once and passes
//! the answer down — so the operator-facing notice and the filesystem action can
//! never disagree.
//!
//! The two entry points answer DIFFERENT questions, and #5274 separated them.
//! [`provision_for_launch`] decides SESSION PLACEMENT, which since #5274 is the
//! main checkout unless the operator passed `tm launch --worktree`; it takes
//! that request as a parameter and consults no registry at all. Only
//! [`provision_for_fallback`] still reads
//! [`trusty_mpm::project::worktree_enabled_for_origin_at`], because it is
//! answering #1724's question instead — may bare `tm`, with the daemon down,
//! deploy `CLAUDE.md` / `.mcp.json` / `.claude/` into whatever checkout the
//! operator happens to be standing in? — where the registry's `worktree: false`
//! is the project TELLING it yes. The two paths still compose: the fallback ends
//! by calling `launch()` with its already-provisioned worktree as the cwd and
//! [`LaunchDir::CallerResolved`], which is what stops `provision_for_launch` from
//! resolving that placement a second time and overwriting it.
//! Scope note: this deliberately does NOT change #3455's concurrency
//! behaviour. `spawn_managed_on_main` WARNS (never refuses) on a second
//! session against one main checkout, and that stays the rule here — nothing
//! in this module refuses a launch.
//! Test: `managed_workspace_tests.rs`.
use ;
use DiskGate;
use ;
use inproject;
use ManagedSessionId;
/// Where a CLI-provisioned managed session will actually run.
///
/// Why: the two outcomes have materially different lifecycles — a worktree is
/// framework-owned and disposable, the operator's main checkout is neither —
/// so callers are forced to distinguish them rather than receiving a bare
/// `PathBuf` that hides which one they got.
/// What: `Worktree` carries `<base>/.worktrees/<session-id>`; `MainCheckout`
/// carries the checkout root the session will run in — since #5274 the default
/// outcome for `tm launch`, and still the #4300 outcome for a project that
/// registered `worktree: false` on the daemon-unreachable fallback.
/// Test: `managed_workspace_tests.rs`.
pub
/// Who resolved the directory [`provision_for_launch`] was handed.
///
/// Why: `launch()` serves two kinds of caller and they need opposite treatment.
/// `tm launch` passes the operator's cwd, which is not a placement yet —
/// resolving it against the managed checkout is the whole job. The guided
/// daemon-unreachable fallback and `tm run` pass a directory they ALREADY
/// resolved (a per-session worktree, a managed checkout they just established),
/// and re-resolving it discards their answer. Without this distinction the
/// redirect fired on every caller: the fallback's fresh worktree was created,
/// abandoned, and every daemon-unreachable session collapsed onto the one shared
/// base clone.
/// What: `OperatorCwd` runs ADR-0037's placement rule via
/// `managed_checkout::resolve_placement_at`. `CallerResolved` takes the
/// directory as given and prints no placement notice — every such caller prints
/// its own, naming the workspace it chose.
/// Test: `provision_for_launch_keeps_a_caller_resolved_placement`,
/// `guided_fallback_prepares_the_session_in_the_worktree_not_the_base_clone`.
pub
/// Act on an already-resolved isolation decision.
///
/// Why: the opt-out check MUST run before [`inproject::ensure_base_clone`].
/// Checking afterwards would still leave a full base clone on disk for a
/// project that asked for no isolation at all — the wasted-disk complaint
/// #3455 was filed about; the daemon's `spawn_managed_routed` documents the
/// same ordering for the same reason. Taking `isolate` as a parameter (rather
/// than re-reading the registry here) guarantees the notice each caller
/// printed describes the action actually taken.
/// What: `isolate == false` returns [`ManagedWorkspace::MainCheckout`] having
/// touched nothing on disk. `main_checkout` is a placement its caller has fully
/// resolved by the time it gets here — [`provision_for_launch`] routes an
/// operator cwd through `managed_checkout::resolve_placement_at` first
/// (ADR-0037's 2026-08-17 terminology clarification), while
/// [`provision_for_fallback`] answers #1724's separate question and resolves its
/// own. With `isolate` it ensures the base clone and adds a UUID-named
/// per-session worktree. Errors are the raw `inproject` strings so each caller
/// can wrap them with its own remediation text.
/// Test: `managed_workspace_tests.rs`.
async
/// Provision the workspace for `tm launch` (#4300 call site 1, #5274).
///
/// Why: `tm launch` runs the whole provisioning flow in-process and never asks
/// the daemon, so it decides placement itself and must reach the SAME answer
/// the daemon's `spawn_managed_routed` would. Since #5274 that answer is the
/// project's main checkout unless the operator asked otherwise, so this takes
/// the request as a parameter (`tm launch --worktree`) rather than reading the
/// registry: the project's `worktree` flag governs AGENT isolation, and letting
/// it decide session placement here would put the CLI and the daemon back on
/// two different rules.
/// What: resolves the REPO ROOT of `cwd`, prints the notice for whichever branch
/// applies, then delegates to [`provision`] with the caller-resolved `base_path`
/// (`inproject::base_clone_path(owner, repo)`). Errors are mapped to `tm
/// launch`'s wording.
///
/// The root resolution matters and is not incidental: `get_origin_url` succeeds
/// at ANY depth, so `cd repo/src && tm launch` would otherwise deploy `.claude`,
/// the project hooks and the tmux cwd into `repo/src` rather than `repo` —
/// landing tm's furniture exactly where the operator did not ask for it.
/// [`super::guided::find_git_root`] is the SAME helper the daemon-unreachable
/// fallback's classifier uses, so both CLI entry points cannot disagree about
/// which directory the project is. A `cwd` that is not inside a working tree (or
/// a machine with no `git` on PATH) falls back to `cwd` itself — the pre-#4300
/// behaviour, never worse.
///
/// The placement rule applies to an operator's cwd and to nothing else, which is
/// what `launch_dir` selects. The guided daemon-unreachable fallback composes
/// onto this function: it provisions a protected worktree via
/// [`provision_for_fallback`] and calls `launch()` with that worktree as `cwd`,
/// already resolved. Running the rule over it again found `find_git_root` ==
/// the worktree, `worktree != base_path`, and redirected the session into the
/// shared base clone — discarding the worktree and its branch, and putting two
/// concurrent fallback sessions in one tree. Such a caller passes
/// [`LaunchDir::CallerResolved`] and the rule is skipped.
/// Test: `provision_for_launch_without_a_request_uses_the_main_checkout`,
/// `provision_for_launch_from_subdirectory_targets_repo_root`,
/// `provision_for_launch_explicit_request_creates_worktree`,
/// `provision_for_launch_ignores_a_registered_worktree_true_project`,
/// `provision_for_launch_keeps_a_caller_resolved_placement`.
pub async
/// [`provision_for_launch`] with the disk gate's measurement source named
/// explicitly (#7603).
///
/// Why: an in-process test of this function otherwise decides its verdict from
/// how full the developer's volume is, against the threshold in the operator's
/// own `~/.trusty-tools/trusty-mpm/config.yaml` — the six-test red the issue
/// reports. `$HOME` cannot be redirected from a `tm`-bin test, so the
/// measurement arrives as an argument instead.
/// What: identical to [`provision_for_launch`] except that `gate` decides where
/// the measurement comes from; the threshold and the decision are unchanged.
/// Test: `provision_for_launch_explicit_request_creates_worktree`,
/// `a_pinned_over_threshold_gate_refuses_a_launch_worktree`,
/// `a_pinned_gate_keeps_a_launch_worktree_off_the_hosts_disk`,
/// `a_launch_worktree_is_refused_when_the_ledger_cannot_record`.
// #8994: `home` (the ledger's) is the eighth.
pub async
/// Provision the workspace for the daemon-unreachable guided fallback
/// (#4300 call site 2).
///
/// Why: bare `tm` with the daemon down must still never write framework files
/// into a live checkout it was not told it could (#1724) — but a project
/// registered with `worktree: false` HAS told us exactly that, and the daemon
/// would have honoured it via `spawn_managed_on_main`. Consulting the same
/// registry here is what makes the setting independent of daemon liveness.
/// What: parses `owner/repo` from `origin_url`, reads the opt-out, and
/// delegates to [`provision`] with `git_root` (the repo ROOT, not the
/// possibly-nested cwd) as the opt-out target; errors carry the fallback's
/// "start the daemon" remediation.
///
/// This function's answer is final because [`super::guided`] hands it to
/// `launch()` as [`LaunchDir::CallerResolved`]. The three tests below stop one
/// call short of that composition, so they cannot see it reversed — the two
/// named last cover the composed path instead.
/// Test: `provision_for_fallback_opted_out_creates_no_clone_and_no_worktree`,
/// `provision_for_fallback_unset_creates_worktree_not_live_checkout`,
/// `provision_for_fallback_other_projects_optout_does_not_leak`,
/// `provision_for_launch_keeps_a_caller_resolved_placement`,
/// `guided_fallback_prepares_the_session_in_the_worktree_not_the_base_clone`.
///
/// `gate` is the disk gate's measurement source (#7603). This path takes no
/// ungated wrapper, because its only production caller —
/// `guided_protected::launch_protected_workspace` — already receives the gate
/// from `guided::fallback_protected`, which is where the `MeasureTarget` default
/// is applied once for the whole fallback.
pub async