trusty-mpm 1.3.4

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
---
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.)

**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` directly. A project
`.mcp.json` is **project scope**: visible only inside that one repo/worktree,
invisible to every other project and to the standalone `tm run` driver, and
(inside a fleet worktree) silently overwritten by `tm` on the next spawn
anyway. If a PM delegates "register the duetto-memory MCP" and an agent edits
`.mcp.json` in whatever worktree happens to be open, that is the wrong scope —
route it through `tm mcp add` instead.

**Where these servers actually reach (issue #2739, plus the custom-server follow-up):**

- 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 project,local`, which does NOT read the user tier.
  Since the #2739 follow-up, this is bridged in TWO ways:
  - **Native trusty MCP servers** — the allowlist `slack-mcp`, `telegram-mcp`,
    `gworkspace-mcp`, `trusty-analyze` (plus the always-provisioned
    `trusty-memory` / `trusty-search`, which have their own dedicated
    injectors) — are bridged with `env` secrets routed to a workspace
    `.env.local` (never the git-tracked `.mcp.json`).
  - **Every other (custom, non-native) server you add** — stdio or remote
    (http/sse) — is ALSO bridged now, with the same `env`-to-`.env.local`
    routing for stdio servers. The one exception: a **remote server that
    declares `headers`** (e.g. an `Authorization` bearer token) is **not**
    bridged — there is no established out-of-band delivery channel for an
    HTTP header secret into `.mcp.json` yet, so it fails closed rather than
    leak the header into a git-tracked file. A remote server with a clean URL
    and no `headers` bridges normally.
  - **Project scope**: a project's own `<project>/.trusty-mpm/manifest.toml`
    `[mcp.custom.<name>]` table (the same per-project override file
    `[agents]`/`[skills]` already use) declares servers scoped to THAT
    project's fleet sessions only, and — on a name collision — OVERRIDES the
    user-scope registry entry of the same name. A `trusty-memory`/
    `trusty-search`/native-allowlist name declared here is always REJECTED —
    a project manifest can never override a reserved name (issue #3033).
  - **Consent gate (issue #3033):** because a project-scope `[mcp.custom]`
    entry ships with the cloned repo itself, it is honored ONLY after the
    operator explicitly runs `tm project trust [--dir <path>]` for that
    project — an untrusted project's `[mcp.custom]` table is skipped entirely
    (a single warning names the project and hints the trust command). 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 an `[mcp-trusted]` marker / trust line for the current state.
    User-scope registry entries (`tm mcp add`) are unaffected — you already
    consented to those by registering them locally.

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

Every managed session always gets these three, whether or not they appear in the
`mcpServers` map:

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

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

### 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: the workspace `.mcp.json` (and its trust seed)
is only (re)built when a session **spawns or resumes**, never while the pane
is live. Concretely:

- **Standalone `tm run` driver** — sees the full user-scope map on its *next*
  launch.
- **Fleet sessions (`tm session new`)** — only the native-allowlist servers
  (`slack-mcp`, `telegram-mcp`, `gworkspace-mcp`, `trusty-analyze`, plus the
  always-provisioned `trusty-memory`/`trusty-search`) get bridged into the
  workspace `.mcp.json`, and only at spawn/resume. A non-native/custom server
  (e.g. an internal `duetto-memory`) reaches the standalone driver only — it
  is not yet bridged into fleet sessions at all (tracked by #2739).

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 both reintroduces the anti-pattern above and gets clobbered on the next
spawn anyway.

### 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
```

**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.
- 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.

### 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)
```

`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, and `prune-worktrees` only ever removes
directories that have no active session at all.

---

## 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 <alias> <url> [--force]# map a repo alias → URL (no clone)
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`.