trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
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
//! The command catalog — single source of truth for the verb surface.
//!
//! Why: before this module, three things described the same verb set and could
//! silently drift: the `/help` body (hand-written in `command.rs`), the typed
//! [`TrustyCommand`](crate::client::TrustyCommand) enum, and the SM prompt's `SM_TOOLS.md` prose. The catalog
//! collapses them into one structured registry. Every adapter renders its help
//! from the same [`CommandSpec`] list, and the SM prompt's tool surface is
//! rendered from the same registry, so a new verb is described in exactly one
//! place. This is the "catalog as single source of truth" principle of the
//! chat-core nucleus (see `docs/specs/chat-core.md`, SPEC-CHAT-CORE-01).
//! What: [`CommandSpec`] is one entry per operator verb — its canonical name,
//! aliases, argument list, one-line summary, and the [`SpecKind`] that ties it
//! to a [`TrustyCommand`](crate::client::TrustyCommand) variant (or marks it SM-only). [`catalog`] returns the
//! full registry; [`help_text`] renders the `/help` body; [`render_sm_tools`]
//! renders the `SM_TOOLS.md` prompt section. A unit test asserts the committed
//! `SM_TOOLS.md` byte-matches [`render_sm_tools`] so the two can never diverge.
//! Test: `catalog_covers_every_command`, `help_text_lists_every_command`,
//! `sm_tools_md_matches_catalog_render` (the parity / single-source-of-truth
//! guard).

use std::sync::OnceLock;

mod sm_tools;

pub use sm_tools::{render_sm_tools, sm_ops_verbs, sm_session_verbs};

/// How a catalog entry relates to the typed [`TrustyCommand`](crate::client::TrustyCommand) surface.
///
/// Why: most catalog verbs map onto a `TrustyCommand` variant an adapter can
/// dispatch, but a few SM prompt verbs (memory, goals) are SM-only — the SM
/// calls them as namespaced JSON-RPC methods, not through the chat-core
/// executor. Modeling that distinction keeps both renderings (operator `/help`
/// vs. SM prompt) drawn from one list without pretending every prompt verb is a
/// dispatchable command.
/// What: `Command` carries the canonical slash name an adapter parses;
/// `SmOnly` marks a prompt-surface verb with no chat-core dispatch.
/// Test: covered by `catalog_covers_every_command` and the SM-tools parity test.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpecKind {
    /// A dispatchable operator command, keyed by its canonical slash name
    /// (without the leading `/`), e.g. `"sessions"`.
    Command,
    /// An SM-prompt-only verb (memory/goals) with no chat-core dispatch.
    SmOnly,
}

/// One catalog entry: a verb, its surface forms, and its one-line summary.
///
/// Why: a single structured description per verb lets every rendering (operator
/// `/help`, the SM prompt tool table) derive from the same data, so adding or
/// renaming a verb touches exactly one place.
/// What: the canonical `name`, any `aliases`, the human-readable `args` sketch,
/// a one-line `summary`, and the [`SpecKind`] that ties it to the typed command
/// surface. All fields are `'static` so the catalog is a compile-time constant
/// table with no allocation.
/// Test: `catalog_covers_every_command`, `sm_tools_md_matches_catalog_render`.
#[derive(Debug, Clone, Copy)]
pub struct CommandSpec {
    /// Canonical verb name. For [`SpecKind::Command`] this is the slash command
    /// (without `/`); for [`SpecKind::SmOnly`] it is the namespaced JSON-RPC
    /// method spelling (e.g. `memory.recall(query)`).
    pub name: &'static str,
    /// Alternate spellings the adapters accept for this verb (may be empty).
    pub aliases: &'static [&'static str],
    /// Human-readable argument sketch (e.g. `<id>`), empty when the verb takes
    /// no arguments.
    pub args: &'static str,
    /// One-line operator-facing summary.
    pub summary: &'static str,
    /// How the entry relates to the typed [`TrustyCommand`](crate::client::TrustyCommand) surface.
    pub kind: SpecKind,
}

impl CommandSpec {
    /// The callable signature for the SM-prompt tool table.
    ///
    /// Why: the SM prompt renders each verb as a callable `name(args)` form (e.g.
    /// `sessions.launch(workdir, model?, prompt?, goal_id?)`), matching the
    /// hand-written `SM_TOOLS.md`. Both SM-only and command entries carry their
    /// args in the dedicated `args` field, so the signature is computed uniformly.
    /// What: renders `name(args)`; an empty `args` yields `name()`. Operator-facing
    /// angle brackets (`<id>`) are stripped so the prompt reads as a method call.
    /// Test: `sm_tools_md_matches_catalog_render`, `prompt_signature_renders_callable_verbs`.
    pub fn prompt_signature(&self) -> String {
        if self.args.is_empty() {
            format!("{}()", self.name)
        } else {
            let inner = self.args.replace(['<', '>'], "");
            format!("{}({inner})", self.name)
        }
    }
}

/// The operator-facing chat-core commands (every dispatchable verb).
///
/// Why: this is the list every adapter renders its `/help` from and the typed
/// dispatch the executor exhausts; it covers the legacy project-session/pairing
/// family AND the managed session-manager verb set.
/// What: ordered for a readable `/help`; each entry's `name` is the canonical
/// slash command the adapters parse.
/// Test: `catalog_covers_every_command`, `help_text_lists_every_command`.
const COMMANDS: &[CommandSpec] = &[
    CommandSpec {
        name: "sessions",
        aliases: &[],
        args: "",
        summary: "list managed sessions",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "status",
        aliases: &[],
        args: "<id>",
        summary: "detailed session status",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "approve",
        aliases: &[],
        args: "<id>",
        summary: "approve a pending permission request",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "deny",
        aliases: &[],
        args: "<id>",
        summary: "deny a pending permission request",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "overseer",
        aliases: &[],
        args: "",
        summary: "show overseer status",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "tmux",
        aliases: &[],
        args: "",
        summary: "list all tmux sessions",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "projects",
        aliases: &[],
        args: "",
        summary: "discover projects from Claude Code config",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "discover",
        aliases: &[],
        args: "",
        summary: "auto-discover tmux sessions running Claude Code",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "adopt",
        aliases: &[],
        args: "<session>",
        summary: "adopt an external tmux session",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "config",
        aliases: &[],
        args: "<path>",
        summary: "analyze Claude Code config for a project",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "snapshot",
        aliases: &[],
        args: "<session>",
        summary: "capture a tmux pane",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "kill",
        aliases: &[],
        args: "<id>",
        summary: "kill a session",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "send",
        aliases: &[],
        args: "<session> <prompt>",
        summary: "send a prompt to a Claude Code session",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "connect",
        aliases: &[],
        args: "<path>",
        summary: "connect to or start a session (no deployment)",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "alerts",
        aliases: &[],
        args: "",
        summary: "show alert subscriptions",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "pair",
        aliases: &[],
        args: "<code>",
        summary: "pair this bot with your daemon",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "start",
        aliases: &[],
        args: "",
        summary: "pairing status and onboarding",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "doctor",
        aliases: &[],
        args: "",
        summary: "run full system diagnostic",
        kind: SpecKind::Command,
    },
    // ── Managed session-manager verbs ───────────────────────────────────────
    CommandSpec {
        name: "managed-new",
        aliases: &["spawn"],
        args: "<repo> <ref> <task>",
        summary: "spawn a managed session from a repo + ref + task",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-list",
        aliases: &[],
        args: "",
        summary: "list managed session-manager sessions",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-get",
        aliases: &[],
        args: "<id>",
        summary: "fetch one managed session's record",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-send",
        aliases: &[],
        args: "<id> <text>",
        summary: "inject text into a managed session's pane",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-answer",
        aliases: &[],
        args: "<id> <answer>",
        summary: "answer a managed session's pending decision",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-activity",
        aliases: &[],
        args: "<id>",
        summary: "inspect a managed session's activity",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-attach",
        aliases: &[],
        args: "<id>",
        summary: "show a managed session's tmux attach command",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-stop",
        aliases: &[],
        args: "<id>",
        summary: "stop a managed session's runtime (keeps the workspace)",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-resume",
        aliases: &[],
        args: "<id>",
        summary: "resume a stopped managed session",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-decommission",
        aliases: &[],
        args: "<id>",
        summary: "decommission a managed session (terminal; removes workspace)",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "managed-adopt",
        aliases: &[],
        args: "<tmux_name> <cwd>",
        summary: "adopt an existing tmux session into the managed store",
        kind: SpecKind::Command,
    },
    // ── Ops / diagnostics ───────────────────────────────────────────────────
    CommandSpec {
        name: "health",
        // `status` is intentionally NOT an alias — it is already the
        // project-session status verb, so reusing it would collide. `healthcheck`
        // and `ping` are the natural operator spellings.
        aliases: &["healthcheck", "ping"],
        args: "",
        summary: "report daemon health (reachability, catalog freshness, fleet)",
        kind: SpecKind::Command,
    },
    CommandSpec {
        name: "help",
        aliases: &[],
        args: "",
        summary: "this message",
        kind: SpecKind::Command,
    },
];

/// The full operator command catalog.
///
/// Why: adapters and tests need read access to the registry to render help,
/// build pickers, and assert coverage.
/// What: returns the static [`CommandSpec`] slice of every dispatchable command.
/// Test: `catalog_covers_every_command`.
pub fn catalog() -> &'static [CommandSpec] {
    COMMANDS
}

/// Render the `/help` body from the command catalog.
///
/// Why: the help text is derived from the same registry the executor dispatches,
/// so it can never list a command the executor cannot run, nor omit one.
/// What: one line per [`SpecKind::Command`] entry, `"/name <args> — summary"`,
/// computed once and cached for the `&'static str` callers expect.
/// Test: `help_text_lists_every_command`.
pub fn help_text() -> &'static str {
    static HELP: OnceLock<String> = OnceLock::new();
    HELP.get_or_init(|| {
        let mut out = String::from("trusty-mpm bot commands:\n");
        let lines: Vec<String> = COMMANDS
            .iter()
            .filter(|c| c.kind == SpecKind::Command)
            .map(|c| {
                if c.args.is_empty() {
                    format!("/{} — {}", c.name, c.summary)
                } else {
                    format!("/{} {} — {}", c.name, c.args, c.summary)
                }
            })
            .collect();
        out.push_str(&lines.join("\n"));
        out
    })
    .as_str()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn catalog_covers_every_command() {
        // The catalog must contain every operator-facing slash command, keyed by
        // its canonical name without the leading slash.
        let names: Vec<&str> = catalog()
            .iter()
            .filter(|c| c.kind == SpecKind::Command)
            .map(|c| c.name)
            .collect();
        for expected in [
            "sessions",
            "status",
            "approve",
            "deny",
            "overseer",
            "tmux",
            "projects",
            "discover",
            "adopt",
            "config",
            "snapshot",
            "kill",
            "send",
            "connect",
            "alerts",
            "pair",
            "start",
            "doctor",
            "managed-new",
            "managed-list",
            "managed-get",
            "managed-send",
            "managed-answer",
            "managed-activity",
            "managed-attach",
            "managed-stop",
            "managed-resume",
            "managed-decommission",
            "managed-adopt",
            "health",
            "help",
        ] {
            assert!(names.contains(&expected), "catalog missing `{expected}`");
        }
    }

    #[test]
    fn health_verb_has_expected_aliases_and_no_status_collision() {
        // `health` carries the operator-friendly spellings but NEVER `status`,
        // which is already the project-session status verb (a collision).
        let health = catalog()
            .iter()
            .find(|c| c.name == "health")
            .expect("catalog has a health verb");
        assert!(health.aliases.contains(&"healthcheck"));
        assert!(health.aliases.contains(&"ping"));
        assert!(
            !health.aliases.contains(&"status"),
            "`status` must not alias health — it is the session status verb"
        );
        assert_eq!(health.kind, SpecKind::Command);
    }

    #[test]
    fn catalog_has_no_duplicate_command_names_or_aliases() {
        // Every dispatchable verb's canonical name AND each alias must be unique
        // across the catalog, so a parsed token resolves to exactly one verb.
        use std::collections::HashSet;
        let mut seen: HashSet<&str> = HashSet::new();
        for spec in catalog().iter().filter(|c| c.kind == SpecKind::Command) {
            assert!(
                seen.insert(spec.name),
                "duplicate verb name `{}`",
                spec.name
            );
            for alias in spec.aliases {
                assert!(seen.insert(alias), "duplicate alias `{alias}`");
            }
        }
    }

    #[test]
    fn help_text_lists_every_command() {
        let text = help_text();
        for cmd in [
            "/sessions",
            "/status",
            "/approve",
            "/deny",
            "/overseer",
            "/tmux",
            "/projects",
            "/discover",
            "/adopt",
            "/config",
            "/snapshot",
            "/kill",
            "/send",
            "/connect",
            "/alerts",
            "/pair",
            "/start",
            "/doctor",
            "/managed-new",
            "/managed-list",
            "/managed-get",
            "/managed-send",
            "/managed-answer",
            "/managed-activity",
            "/managed-attach",
            "/managed-stop",
            "/managed-resume",
            "/managed-decommission",
            "/managed-adopt",
            "/health",
            "/help",
        ] {
            assert!(text.contains(cmd), "help text missing {cmd}");
        }
    }
}