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
//! Bare `tm` outside a git work tree — list managed sessions, then offer the
//! two ways to start one (#6666).
//!
//! Why: bare `tm` in a plain directory used to run `git init` there and carry
//! on as though the operator had asked for a project (#6274/#6276). The owner
//! ruling of 2026-09-02 supersedes that OUTSIDE a work tree: show the managed
//! session list — the thing an operator in an arbitrary directory almost always
//! wants — and make the two ways to create a session explicit choices rather
//! than a side effect. Inside a work tree nothing changes; the auto-`git init`
//! still runs there for the directories that are already repositories.
//!
//! What: [`route_bare_tm`] is the pure decision (work tree vs not, TTY vs not);
//! [`parse_outside_git_choice`] turns one line of input into an
//! [`OutsideGitChoice`]; [`choice_argv`] turns that choice into the `tm
//! sessions …` argv the options block advertises, so the displayed command and
//! the executed one cannot drift; [`run_outside_git_menu`] is the orchestrator
//! with every I/O edge injected. [`dispatch_session_argv`] is the production
//! runner: it parses the argv back through the real `Cli` and calls the real
//! `sessions` handler — no process spawn.
//!
//! Test: `guided_outside_git_tests.rs`.
use Future;
use Path;
/// The `tm sessions` verb that creates an UNTRACKED session in place.
///
/// Why: `tm sessions start` runs the harness in the directory it is given and
/// registers nothing in the managed fleet — the "non-tracked session" half of
/// the ruling. Named here so the options block and [`choice_argv`] read the
/// same constant.
/// Test: `outside_git_untracked_choice_maps_to_sessions_start_argv`.
const UNTRACKED_VERB: &str = "start";
/// The `tm sessions` verb that creates a MANAGED, tracked session.
///
/// Why: `tm sessions new <path>` creates a managed-fleet record that `tm ls`
/// then lists — the "managed tracked session" half of the ruling. For a path
/// with no readable GitHub remote the daemon's in-project detection returns
/// `Ok(None)` and the spawn falls through to its local-path form, so a plain
/// directory is a valid target and nothing is initialised here.
/// Test: `outside_git_managed_choice_maps_to_sessions_new_argv`.
const MANAGED_VERB: &str = "new";
/// What bare `tm` should do for the current directory.
///
/// Why: the interception has to answer two questions at once — is this a
/// directory the existing guided default owns, and may we block on stdin? —
/// and both must be decidable without touching a terminal or a daemon.
/// What: `GuidedProject` means run the pre-#6666 flow unchanged.
/// `OutsideGit { interactive }` means show the listing and the options;
/// `interactive` is `true` only when a keypress can actually be read.
/// Test: `route_bare_tm_*`.
pub
/// Which action the operator picked from the options block.
///
/// Test: `parse_outside_git_choice_*`.
pub
/// Decide what bare `tm` does, given the four facts that settle it.
///
/// Why: a pane that belongs to a managed session can sit in any directory,
/// including one with no git work tree, and `fallback_protected`'s #4061 retry
/// exists to give that pane's Active→Stopped settle race a second bounded
/// attempt. Routing such a pane into this menu would print a session list at
/// an operator who asked to get their agent back, so the managed-pane signal
/// keeps the pre-#6666 flow.
/// What: `inside_work_tree` or `managed_pane` → `GuidedProject`. Otherwise
/// `OutsideGit`, interactive only when BOTH stdin and stdout are terminals —
/// the same anti-hang pairing `session_ls_connector::should_show_picker` and
/// `commands::projects::should_launch_bare_tui` use, because stdin redirected
/// from `/dev/null` would EOF instantly and a piped stdout must stay clean.
/// Test: `route_bare_tm_git_tree_is_guided`, `route_bare_tm_managed_pane_is_guided`,
/// `route_bare_tm_outside_git_tty_is_interactive`,
/// `route_bare_tm_outside_git_piped_stdin_is_static`,
/// `route_bare_tm_outside_git_piped_stdout_is_static`.
pub
/// Read one line of operator input as a choice.
///
/// What: case-insensitive, whitespace-trimmed; `n` selects the untracked
/// session, `m` the managed one, and everything else — `q`, a bare Enter, an
/// EOF-empty string, a typo — quits without touching anything.
/// Test: `parse_outside_git_choice_n_is_untracked`,
/// `parse_outside_git_choice_m_is_managed`,
/// `parse_outside_git_choice_empty_is_quit`,
/// `parse_outside_git_choice_unknown_is_quit`.
pub
/// The `tm sessions …` argv a choice runs, without the leading `tm`.
///
/// Why: the options block advertises a command the operator could type; this
/// builds that same command as argv and [`dispatch_session_argv`] parses it
/// back through the real CLI. One source for both means the printed command
/// and the executed one cannot drift.
/// What: `Some(argv)` for the two session choices, `None` for `Quit`. The
/// managed form passes an empty `--task` because `tm sessions new` requires
/// the flag and an interactive session has no task to inject — the same empty
/// task `commands::session::start::start_session` builds for its own
/// protected-path dispatch.
/// Test: `outside_git_untracked_choice_maps_to_sessions_start_argv`,
/// `outside_git_managed_choice_maps_to_sessions_new_argv`,
/// `outside_git_quit_choice_maps_to_no_argv`.
pub
/// The options block printed under the session listing.
///
/// Why: an operator standing outside a git project needs to know both that
/// nothing was created and what the two real commands are — so the block names
/// each command in full rather than describing it.
/// What: a multi-line string naming `[n]`, `[m]`, and `[q]` with the exact
/// commands [`choice_argv`] builds. Printed to stderr by
/// [`run_outside_git_menu`] so the listing on stdout stays pipeable.
/// Test: `outside_git_options_block_names_both_real_commands`,
/// `outside_git_options_block_matches_choice_argv`.
pub
/// Read one line from stdin, treating EOF as an empty line.
///
/// Why: the menu's only blocking read. Kept separate from
/// [`run_outside_git_menu`] so every test drives the orchestrator with a
/// closure instead of a terminal.
/// Test: covered through `run_outside_git_menu`'s injected reader.
pub
/// List sessions, print the options, and run the operator's choice.
///
/// Why: every I/O edge is a parameter — the listing, the input read, and the
/// action — so the whole outside-git behaviour is testable with no daemon, no
/// terminal, and no spawned process.
/// What: runs `list_sessions` first (a failure is reported and the options are
/// still offered — a dead daemon is not a reason to hide the two commands),
/// prints [`options_block`], then returns `Ok(())` when not interactive. When
/// interactive it reads one line, maps it through [`parse_outside_git_choice`]
/// and [`choice_argv`], and awaits `run_argv` for the two action choices.
/// Test: `outside_git_menu_static_lists_then_prints_options`,
/// `outside_git_menu_static_never_reads_or_runs`,
/// `outside_git_menu_tty_choice_runs_the_advertised_argv`,
/// `outside_git_menu_quit_runs_nothing`,
/// `outside_git_menu_reports_a_listing_failure_and_still_offers_the_options`.
pub async
/// The whole #6666 interception, wired to production I/O.
///
/// Why: `run_guided_default` reads as a sequence of "does this case own the
/// invocation?" gates (`try_inplace_relaunch` is the same shape), and keeping
/// the wiring here rather than inline there is also what holds `guided.rs`
/// under the 500-SLOC production cap.
/// What: evaluates [`route_bare_tm`] against the real classifier, the
/// managed-session environment, and both terminals. `None` means the caller
/// carries on unchanged. `Some(result)` is the finished menu — the listing
/// comes from `commands::managed::session_ls`, the one `tm sessions ls`
/// implementation, so there is no second renderer to drift from it.
/// Test: the decision by `route_bare_tm_*`, the menu by
/// `outside_git_menu_*`; the wiring itself by the binary smoke run in #6666.
pub async
/// Run a `tm sessions …` argv through the real CLI parser and handler.
///
/// Why: the options block promises the operator a command. Parsing that same
/// argv back through [`crate::cli::Cli`] and dispatching the resulting
/// `SessionAction` means the promise is kept by construction — a wrong flag
/// would fail to parse rather than silently run something else — and nothing
/// is spawned as a subprocess.
/// What: prepends the `tm` `argv[0]` clap expects, parses, and calls
/// [`crate::commands::session::session`]. Any other parsed command is an
/// internal bug and returns `Err`.
/// Test: `outside_git_dispatch_rejects_a_non_session_argv`; the happy paths are
/// covered by the argv tests plus the `sessions` handler's own suite.
pub async