trusty-mpm 1.7.0

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
Documentation
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
---
name: tm-cli-operations
description: Operate the tm / trusty-mpm CLI — set up and manage MCP servers, drive session lifecycle, and run health diagnostics
user-invocable: true
version: "1.0.0"
category: pm-reference
tags: [cli, mcp, sessions, services, doctor, auth, operations, pm-recommended]
effort: medium
---

# tm CLI Operations

How to drive the `tm` (trusty-mpm) binary itself: register and verify **MCP
servers**, manage the **session lifecycle**, and run **health diagnostics**.

This is distinct from `tm-tool-usage-guide`, which covers the PM's in-session
`mcp__trusty-mpm__* / mcp__trusty-memory__* / mcp__trusty-search__*` **tool**
families. This skill is about the `tm` **command-line binary** and the config it
owns.

**PM-delegation framing.** A PM under circuit-breaker discipline does not shell
out to run process-spawning or health commands (`tm mcp test`, `tm services`,
`tm doctor`) — it delegates execution to **local-ops** and reads back the
result. This skill tells you *what* to run and how to read it; local-ops (or you
at your own shell, outside a managed session) actually runs it. One-time config
edits (`tm mcp add/remove`, `tm auth set-token`) are setup, run once at your
shell.

---

## 1. MCP Server Management (headline)

### Where the config lives — the tm-owned config dir

Managed sessions launch `claude` under a **relocated** `CLAUDE_CONFIG_DIR`, not
your real `~/.claude`. `tm mcp` edits the top-level `mcpServers` map of that
dir's `.claude.json`:

- Default (daemon-managed): `~/.trusty-tools/trusty-mpm/claude-config/.claude.json`
- Standalone root: `<root>/claude-config/.claude.json` when you pass `--root`
  (same precedence chain as `tm register`/`tm ls`:
  `--root` > `TRUSTY_MPM_ROOT` env > `[standalone] root` in config > `~/.trusty-mpm`).

This map is **user scope**. There is deliberately **no `--scope` flag**:
`tm mcp` is inherently user scope. (Stock `claude mcp add` cannot target this
relocated dir, which is why `tm mcp` exists.)

🔴 **Since #7892, user scope is standard Claude Code user scope.** Every server
in that map loads in EVERY tm session, in every project, with no grant — no
`tm project trust`, no `tm mcp share`, no `[session] mcp_servers` entry. tm adds
its four trusty-* builtins on top through the session file it passes with
`--mcp-config`; it does not pass `--strict-mcp-config`, which is what used to
suppress the user scope. A server added with `claude mcp add --scope user` under
that `CLAUDE_CONFIG_DIR` appears in the next tm session with no further step.

A project's own `.mcp.json` follows **Claude Code's** approval, not tm's:
`enableAllProjectMcpServers` / `enabledMcpjsonServers` in settings, or the
prompt. A non-interactive pane cannot prompt, so there it connects unasked —
the same posture as `claude -p`.

`tm mcp list` lists all three sources, with each `.mcp.json` entry's approval
state, and `tm doctor`'s `session_scope` check reports the same inventory.
`[session] plugins` is the one thing still gated on `tm project trust`, because
Claude Code has no per-project plugin approval:

```toml
[session]
plugins = ["aws-core"]
```

Full rules and failure arms:
[session-mcp-plugin-scope.md](../../../../../docs/reference/session-mcp-plugin-scope.md).

**ANTI-PATTERN — do not hand-edit a project's `.mcp.json`.** When asked to
"register the X MCP server" with no scope qualifier, that almost always means
**user-wide** (available in every project/session), and the answer is always
`tm mcp add` — never editing a project's `.mcp.json` by hand. A project
`.mcp.json` is **project scope**: visible only inside that one repo/worktree
and invisible to every other project. When a server genuinely belongs to ONE
repository, `tm mcp add <name> --project -- <command>` writes it there for you
— that is the supported project-scope path. Whether the session connects it is
Claude Code's approval decision, not tm's (#7892).

**Where these servers actually reach (issue #2739, inverted by #4181):**

- The **standalone `tm run` driver** reads this user-scope map directly, so it
  sees **every** server you add here.
- **Daemon-managed / fleet sessions** (`tm session new`) launch
  `claude --setting-sources user,project,local`, so they read that same
  user-scope map directly. Since ADR-0042 (#4181) nothing is injected into a
  workspace `.mcp.json`, and there is no native-server allowlist. Since #7892 a
  server you `tm mcp add` reaches EVERY fleet session, in every project. Proof:
  `prepare_session_reaches_an_operator_registered_server_through_user_scope`,
  `resolve_scope_never_filters_the_user_scope`.
- **Project scope: there is none (#7894).** A project manifest's
  `[mcp.custom.<name>]` table was designed as the per-project half of this,
  but its only consumer — the injector that wrote the workspace `.mcp.json` —
  was deleted by ADR-0042, so the table resolved through three merge layers and
  was read by nothing. #7894 removed it. A manifest still carrying one parses
  and has no effect. Declare a custom server once, in user scope, with
  `tm mcp add`.
- **Consent gate (issue #3033, narrowed by #7892):** `tm project trust
  [--dir <path>]` now grants exactly one thing — the project's
  `[session] plugins` opt-ins. A plugin ships its own skills, commands and hooks
  into every session in the project and Claude Code has no per-project approval
  for one, so the list is ignored until the operator grants it. Trust state
  lives in USER-scope config (`~/.trusty-tools/trusty-mpm/project-trust.json`),
  never inside the repo, so a cloned repo can never self-trust. Revoke with
  `tm project trust --revoke [--dir <path>]`; `tm project list`/`tm project
  info` show a `[plugin-trusted]` marker / trust line. **It no longer gates MCP
  servers**, and `tm mcp share` / `tm mcp unshare` are retired to notices.

### The 4 auto-provisioned framework built-ins

Every managed session always gets these four, whether or not they appear in the
`mcpServers` map. The list is `mcp_config::BUILTIN_MANAGED_MCP_SERVERS` and the
commands come from `mcp_config::builtin_server_entry`:

| Server | Launch command |
|---|---|
| `trusty-memory` | `trusty-memory serve --stdio` |
| `trusty-mpm` | `trusty-mpm serve --stdio` |
| `trusty-review` | `trusty-review serve --stdio` |
| `trusty-search` | `trusty-search serve` |

You never register these — they are built in, and their names are reserved.
`tm mcp add` is for **additional** servers on top of them.

### The opt-in native connectors — Google Workspace and Slack

These two ship as crates in this workspace but are NOT auto-provisioned. A
session sees neither until an operator registers it, so absence is the default
state and not a fault to diagnose.

| Connector | Crate | Binary | Install |
|---|---|---|---|
| Google Workspace | `crates/trusty-gworkspace` | `trusty-gworkspace-mcp` | `cargo install --path crates/trusty-gworkspace --locked` |
| Slack | `crates/trusty-channels` | `slack-mcp` | `cargo install --path crates/trusty-channels --locked` |

```bash
tm mcp add trusty-gworkspace -- trusty-gworkspace-mcp
tm mcp add slack-mcp -- slack-mcp
```

**The name you pass is the tool prefix.** Registering Google Workspace as
`trusty-gworkspace` gives the session `mcp__trusty-gworkspace__*`; registering
it as `gworkspace` gives `mcp__gworkspace__*`. The crate name, the binary name,
and the server's own `serverInfo.name` (`gworkspace-mcp`) do not decide it. Run
`tm mcp list` to see what is actually registered rather than inferring a prefix.

**Do not install a third-party lookalike.** A PyPI `gworkspace-mcp` exists and
is the predecessor of this crate; the Rust binary was renamed to
`trusty-gworkspace-mcp` in #2644 precisely because both claimed the same `$PATH`
name. If the native tools are missing, install and register the crate above —
never search the machine for a package with a similar name.

**Registered is not the same as working.** `trusty-gworkspace-mcp` needs a
Google OAuth client and an authorized account (`trusty-gworkspace-mcp setup`);
run `trusty-gworkspace-mcp doctor` and it names which of the two is missing.
`slack-mcp` needs `SLACK_BOT_TOKEN`, plus `SLACK_USER_TOKEN` for
`slack_search_messages`. Each crate's `README.md` carries its auth setup and
full tool inventory.

`crates/trusty-channels` also builds a `telegram-mcp` binary. It is a scaffold —
its tool calls return a `not-yet-implemented` MCP error (#2641) — so do not
register it expecting a working connector.

### Adding a server — `tm mcp add`

stdio (default transport — a local subprocess speaking MCP over stdio):

```bash
# Options (-t/-e/--root) MUST precede the command token.
# A leading `--` stops flag parsing so hyphen-led args pass through.
tm mcp add my-server -e API_KEY=xxx -- npx -y @scope/some-mcp-server
tm mcp add pyserver -e LOG=debug -- python -m my_mcp_pkg --flag
```

- `-e KEY=VALUE` (repeatable) — environment for the subprocess. Splits on the
  **first** `=` (so `-e B=x=y` → `B` = `x=y`).
- The first positional token after the options is the **command**; the rest are
  subprocess args. `add` only **warns** (never blocks) when the command is not
  on `PATH` — it may resolve later at session launch.

http / sse (a remote endpoint):

```bash
tm mcp add remote-api -t http -H "Authorization: Bearer $TOKEN" https://host/mcp
tm mcp add events -t sse -H "X-Api-Key: $KEY" https://host/sse
```

- `-H "Name: Value"` (repeatable) — HTTP headers. Splits on the **first** `:`,
  value trimmed. Only valid for http/sse.
- `-e` and subprocess args are rejected for http/sse; `-H` is rejected for stdio.

### Propagation caveat — an already-running session needs a relaunch

`tm mcp add` writes the config immediately, but a **currently running** session
does not hot-reload it. Claude Code reads the `mcpServers` map once, at launch,
so both the standalone `tm run` driver and a fleet session pick up a new entry
on their *next* spawn or resume — never while the pane is live.

So if a newly `tm mcp add`-ed server isn't showing up in `/mcp` inside a
session that was already running when you added it, the fix is to **relaunch
the session** (`tm session resume <id>`, or the in-pane `tm` relaunch hint) —
not to hand-edit that session's project `.mcp.json` as a workaround. That edit
reintroduces the anti-pattern above, and it puts a workspace-scope declaration
back where ADR-0042 removed it — which is what makes Claude Code raise its
approval gate in the first place.

### How added servers flow into the trust-seed

You do **not** manage trust separately. On the next managed-session start, tm
computes the trust list as `BUILTIN_MANAGED_MCP_SERVERS ∪ (your mcpServers keys)`
and pre-seeds each workspace's `enabledMcpjsonServers` (via
`preseed_managed_trust`). Result: a `tm mcp add`-ed server is **auto-trusted** —
the session never shows the "New MCP servers found" approval dialog. No extra
bookkeeping.

### Inspecting — `tm mcp list` / `tm mcp get`

```bash
tm mcp list                 # table: NAME  TYPE  TARGET
tm mcp list --json          # raw mcpServers map (automation)
tm mcp get my-server        # one server: Type / Target / Scope
tm mcp get my-server --json # the raw entry
```

### Verifying — `tm mcp test` (the real handshake)

Registration only proves the config is well-formed. `tm mcp test` **spawns each
server and runs a real MCP handshake** so you (and CI) get a definitive
pass/fail. This is the MCP-layer check.

```bash
tm mcp test                 # bare sweep: every configured server ∪ the 3 built-ins
tm mcp test my-server       # just one (config entry, else built-in fallback, else error)
tm mcp test --json          # machine-readable results array (automation)
```

- **stdio** servers: full `initialize` → `tools/list` handshake; reports the
  tool count on success. Bounded by a 5s probe timeout.
- **http/sse** servers: an HTTP reachability check.
- **Exit code: non-zero (1) if ANY tested server fails** — slots straight into
  a CI gate, matching `tm services`' convention. A named server that isn't found
  is a hard error.

**Interpreting failures** (the RESULT/DETAIL column, or `error` in `--json`):

| Failure | Meaning | Typical fix |
|---|---|---|
| `spawn error: …` | The command couldn't be launched | Command not on `PATH`; fix the `-- <command>` or install it |
| `timeout` | No handshake within 5s | Server hung on startup, or waits on missing env/creds |
| `protocol error: …` | Launched but spoke bad/no MCP | Wrong command, non-MCP binary, or a version mismatch |
| `unreachable: …` | http/sse endpoint not reachable | Bad URL, server down, or auth header rejected |

### Migrating servers from a stock claude-code config

To bring servers you already use in stock Claude Code into the managed fleet,
read them from your existing config and re-register with `tm mcp add`:

- User-level source: `~/.claude.json` → its `mcpServers` map.
- Project-level source: a repo's `.mcp.json` → its `mcpServers` map.

For each entry, translate to a `tm mcp add`: `command` + `args` → the stdio form
(`-- <command> <args...>`), `env` → repeated `-e KEY=VALUE`, a `url` → the
`-t http|sse` form with `-H` headers. **Never echo secret env/header values into
logs or transcripts** — reference them via `$ENV_VAR`, and confirm the config
dir before writing. Verify the migration with `tm mcp test <name>`.

### Removing — `tm mcp remove`

```bash
tm mcp remove my-server     # drops it from the map; no-op (not an error) if absent
```

---

## 2. Session Lifecycle

`tm session <verb>` has **two families** under one command group. A verb from one
family is not a valid target for the other's IDs.

**Local project sessions** (bound to the cwd, daemon-backed):

```bash
tm session start [--dir <path>]        # new session in the project
tm session list [--dir <path>]         # sessions for this project
tm session info <id|name>              # detailed info
tm session pause <id|name> [--note …]  # save state for later resume
tm session run <id|name> "<cmd>"       # send a command to the pane
tm session output <id|name> [--lines N]# capture recent pane output
tm session clean [--dir <path>]        # reap dead sessions
```

**Managed fleet sessions** (provisioned in isolated worktrees):

```bash
tm session new <repo> --task "<desc>" [--git-ref main] [--name-hint …] [--runtime claude-code|tcode]
tm session ls [--current | --source-id owner/repo] [--all] [--json]
tm session activity <id>               # what a session is doing (no attach)
tm session send <id> "<text>"          # inject text into the pane
tm session answer <id> "<answer>"      # resolve a pending decision
tm session attach <id>                 # print the tmux attach command
tm session rename <id|name> <new-name> # rename from the master list
tm session rename <new-name>           # rename the CURRENT session (in-session form)
```

`rename` is the only lifecycle verb with a one-arg in-session form: with no
target it resolves the current session from `$TM_MANAGED_SESSION_ID`. A managed
session's name IS its tmux name, so a rename also renames the live tmux session
and `tmux attach` / `tmux ls` reflect it immediately. Collisions and invalid
names are rejected with an actionable error.

**Shared verbs** (managed-aware — auto-detect managed vs local by id/name):

```bash
tm session stop <id|name>              # non-destructive; workspace preserved, resumable
                                       #   (alias: `tm session kill`)
tm session resume <id|name>            # re-spawn runtime / restore paused session
```

### The `tm ls` / bare `tm` picker (incl. `d<N>` delete)

On a real terminal, bare `tm ls` (and bare `tm`) opens the interactive
session picker — a numbered menu of live managed sessions. Actions:

- `<N>` — connect to session N.
- **`d<N>`** (or `d <N>`) — **delete** the session at slot N (#2304). A
  confirm prompt guards it, and the delete is routed correctly for managed vs
  local sessions. The only form that can delete a RUNNING session.
- **`d <glob>`** — **bulk delete by tmux name** (#5539), e.g. `d tm-test-*`.
  Glob only (`*`, `?`, `[…]`); a plain word is not a pattern, so `d delete`
  stays unrecognised. Deletes only `stopped`/`errored` matches — running ones
  are listed as `keep` and need `d<N>`. Excludes your own session, skips
  already-deleted rows, and never matches a session with an empty tmux name.
  Prints every match marked `DELETE`/`keep`, then asks for the number of
  `DELETE` rows typed back (the prompt does not print it, so a bulk delete
  cannot be confirmed unread). No match = says so, deletes nothing.
- **`d <glob> --dry-run`** (flag on either side) — preview, delete nothing.
  ⚠️ After a `tmux kill-server`/reboot every session reads `stopped`, so `d *`
  proposes the whole fleet. Dry-run any broad pattern first.
- Piped / `--json` / non-TTY invocations degrade to a static, pipeable list
  (never block on stdin). `tm ls --projects` (`-p`) shows the repo-alias /
  project registry instead of sessions.

**`tm ls -a` / `--attached` — only what you're actually looking at.** Lists the
sessions with a tmux client attached RIGHT NOW, and nothing else. A session can
be live and reported `active` with nobody connected to it; `-a` drops those.
The signal is tmux's own `#{session_attached}`, not the lifecycle state word.

- Forces the static table (like `--all`) — a session you already have a client
  on is the one thing the picker cannot usefully connect you to.
- Nothing attached prints `no attached sessions` and exits 0.
- No effect on `--json` (raw daemon response is always complete, matching
  `--all` and the filter terms) or on `--projects`.
- ⚠️ `-a` here NARROWS; it does not mean "all" as in `ls -a` / `docker ps -a`.
  `--all` (tombstones included) stays long-only.

### Delete / teardown semantics — pick the right verb

| Verb | Runtime | Workspace on disk | Record | Resumable? |
|---|---|---|---|---|
| `stop` / `kill` | stopped | **preserved** | kept | **yes** |
| `decommission` | stopped | **removed** | tombstone kept | no (terminal) |
| `delete` | (guarded) | **never touched** | **hard-deleted** | no |

- `tm session decommission <id>` — the only op that **removes the workspace
  directory**. Terminal. Keeps a tombstone for `ls` history.
- `tm session delete <id> [--force]` — hard-deletes the store **record** only
  (never the workspace). **Fail-closed**: refuses a RUNNING session unless
  `--force`. Use `decommission` first if you also want the disk reclaimed.

### Bulk cleanup

```bash
tm session prune-idle [--dry-run] [--json]      # idle→stop, done→decommission (locked policy)
tm session prune --state ephemeral|stopped|decommissioned|all [--dry-run] [--include-active]
tm session decommission-ephemeral               # tear down all test/throwaway sessions
tm session prune-worktrees [--force]            # remove orphaned .worktrees/ dirs (default dry-run)
```

To clear a batch by NAME rather than state, use the picker's `d <glob>` above.

The `.worktrees/` path above is what `tm`'s own session provisioning writes
today; it is NOT the canonical worktree home. Hand-provisioned and
harness-provisioned worktrees live under `.claude/worktrees/` (see
`tm-workflow`). Migrating tm's provisioning path is pending — until it lands,
`prune-worktrees` only sees `.worktrees/`.

`prune-worktrees` defaults to a safe preview (pass `--force` to act);
`prune`/`prune-idle` act immediately unless you pass `--dry-run` yourself.
`prune --include-active` is the only explicit override to also touch RUNNING
sessions; `prune-idle`'s idle/done verdict policy inherently leaves
working/blocked sessions alone.

**`--force` against the liveness gate (#6806).** `--force` selects the
destructive mode. It overrides no gate, and specifically it never overrides
another live session's claim on a worktree — a worktree some OTHER session
claims stays spared however you invoke the command; stop that session to
reclaim its trees. What `--force` does not have to fight is the CALLER's own
claim: the command sends `$TM_MANAGED_SESSION_ID`, so the nested
`.worktrees/<name>` trees a session created inside its own workspace are
reclaimable from inside that session. The caller's own workspace DIRECTORY is
still refused — a session may prune worktrees inside its workspace, never the
workspace it is sitting in. Every liveness refusal names the session holding
the claim and says whether that session is the caller.

---

## 3. Health & Diagnostics

Three complementary layers — use the right one:

| Command | Layer it checks |
|---|---|
| `tm doctor` | Full trusty-mpm stack (instructions, agents, skills, memory, search) — needs a reachable daemon |
| `tm validate [--path <dir>] [--repair]` | A workspace's deployed `.claude/{agents,skills}` + `settings.json` vs the bundled roster — **standalone, no daemon** |
| `tm services status <name> [--json]` | A service daemon's **HTTP health** (is trusty-search up, on what port, healthy) |
| `tm mcp test [<name>]` | The **MCP protocol layer** — does each server actually spawn and speak MCP |

Key distinction: **`tm services` checks HTTP liveness; `tm mcp test` checks the
MCP handshake.** A daemon can be HTTP-healthy yet fail its MCP handshake (or vice
versa) — test the layer you actually care about.

```bash
tm doctor                          # full diagnostic report
tm validate --repair               # re-run the deploy pipeline to close gaps; non-zero exit if gaps remain
tm services list [--json]          # every declared service + status (exit 0)
tm services status trusty-search   # 0=running, 1=down, 2=unknown service
tm services health trusty-search   # probes /health: prints OK / FAIL
tm services port|url|log <name>    # scriptable single-value lookups
tm mcp test                        # MCP-layer sweep (see §1)
```

`tm services` exit codes are stable and scriptable: `0` ok/running, `1`
down/unhealthy, `2` unknown service.

---

## 4. Managed Auth (`CLAUDE_CODE_OAUTH_TOKEN`)

Because managed sessions run under a relocated `CLAUDE_CONFIG_DIR`, a `/login`
inside a managed session can diverge from your default-dir login (the macOS
Keychain entry is keyed by a hash of the config dir) — producing a "login
successful" → "not logged in" loop (#2246). `tm auth` manages a tm-owned token
that bypasses the Keychain entirely.

```bash
claude setup-token | tm auth set-token   # store from stdin (safer — no shell history)
tm auth set-token --token <val>          # explicit (lands in history — avoid in shared shells)
tm auth status                           # presence of stored token + env vars + on-disk creds — NEVER the value
tm auth clear-token                       # remove it (roll back to ambient auth)
```

The token is written 0600 to `~/.trusty-tools/trusty-mpm/claude-code-oauth.token`
and injected by the daemon into managed sessions. `tm auth status` never prints
the secret — safe to include in a `tm doctor`-style report.

---

## 5. Other Operator Commands

```bash
tm install [--force]               # deploy bundled framework artifacts to ~/.trusty-mpm/framework/
tm register <owner/repo> [alias]   # map a repo alias → URL (no clone; GitHub assumed; alias defaults to owner-repo)
tm load <alias>                    # clone/refresh + deploy managed config
tm run <alias> [--task …]          # launch an interactive claude session for an alias
tm path <alias>                    # print the stable repo path (IDE-attach / cd)
tm update [<alias>]                # git pull --ff-only + re-deploy config (all loaded aliases if omitted)
tm rm <alias>                      # deregister + delete the alias's project dir (shared config untouched)
tm start | stop | restart | status # daemon lifecycle (see note below)
tm serve --stdio                   # the MCP stdio bridge wired into .mcp.json
```

🔴 **`tm start` and `tm restart` REFUSE when launchd owns the daemon** (issue
#4230). On a host with a registered daemon launchd unit they exit with an error
naming the right command instead of spawning a second, unsupervised daemon that
would seize the port and keep serving a stale binary. Use launchd there:

```bash
tm doctor            # read the exact recipe from `daemon_orphan` / the refusal message
tm doctor            # after restarting: `daemon_orphan` must be OK
```

Take the label from `tm doctor`'s own output rather than assuming
`com.trusty.mpm` — the refusal message and every `daemon_orphan` remediation name
the unit that is actually registered on this host, which is not always the
canonical label. The recipe has the shape
`launchctl kickstart -k gui/$(id -u)/<label>`.

`tm stop` still works, but launchd will NOT bring the daemon back on its own
(`KeepAlive.SuccessfulExit=false`) — use the `kickstart` above. To run an
unsupervised daemon deliberately, `tm daemon --force`.

`--root <path>` on the alias commands (and `tm mcp`) switches to a standalone
managed root via the precedence chain in §1.

---

## When to load this skill

Load `/tm-cli-operations` when the task is to **set up or verify an MCP server**
for the fleet, **drive session lifecycle** beyond the MCP session tools, or
**diagnose the tm stack** at the CLI. For the PM's in-session MCP *tools*, see
`tm-tool-usage-guide`; for the delegation model, `tm-delegation-patterns`.