trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
//! UI-agnostic command model.
//!
//! Why: the Telegram bot, the TUI, and the CLI all express the same operator
//! intents — list sessions, check status, approve a request, pair the bot, drive
//! the managed session-manager fleet. A single typed command keeps every UI's
//! dispatch exhaustive and lets the one [`super::executor::CommandExecutor`]
//! translate intent into daemon HTTP calls. The managed-verb variants
//! (`Managed*`) cover the full `/api/v1/sessions/managed/*` surface so any
//! adapter (CLI today; STUI #1272, TELUI #1433, Web/Slack next) can express
//! every operation through this one enum.
//! What: [`TrustyCommand`] enumerates every remote-management action. UI crates
//! convert their own native command representation into a `TrustyCommand` and
//! hand it to the executor. The command catalog ([`super::catalog`](mod@crate::client::catalog)) is the
//! single source of truth for the names, aliases, and help prose; this enum is
//! the typed target each catalog entry maps to.
//! Test: `cargo test -p trusty-mpm` covers the executor and the catalog parity;
//! conversions are tested in the UI crates and in `telegram::commands`.

use std::path::PathBuf;

/// A UI-agnostic operator command.
///
/// Why: a typed command is the shared contract between every UI and the
/// executor — UIs parse their input into one of these, the executor maps each
/// to daemon calls.
/// What: the full set of remote-management actions trusty-mpm supports — the
/// legacy project-session/pairing/overseer family AND the managed
/// session-manager verb set (`Managed*`).
/// Test: see the executor tests in `executor/tests.rs` and the catalog parity
/// test in `catalog/sm_tools.rs`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrustyCommand {
    /// List all managed sessions.
    Sessions,
    /// Show detailed status for one session.
    Status {
        /// Session id or friendly name.
        session_id: String,
    },
    /// Approve a session's pending permission request.
    Approve {
        /// Session id or friendly name.
        session_id: String,
    },
    /// Deny a session's pending permission request.
    Deny {
        /// Session id or friendly name.
        session_id: String,
    },
    /// Show overseer status (enabled, handler, recent decisions).
    Overseer,
    /// List all tmux sessions (trusty-mpm and external).
    Tmux,
    /// Discover projects from the Claude Code configuration.
    Projects,
    /// Auto-discover tmux sessions running Claude Code and adopt them.
    Discover,
    /// Adopt an external tmux session for oversight.
    Adopt {
        /// tmux session name to adopt.
        session: String,
    },
    /// Analyze Claude Code config for a project.
    Config {
        /// Project directory path.
        project: String,
    },
    /// Capture a tmux pane snapshot.
    Snapshot {
        /// tmux session name.
        session: String,
    },
    /// Kill a trusty-mpm session.
    Kill {
        /// Session id or friendly name.
        session_id: String,
    },
    /// Show the current alert subscription.
    Alerts,
    /// Pair the bot with the daemon.
    ///
    /// Why: `/pair <code>` confirms a code generated by `tm pair`; `/pair` with
    /// no code reports the current pairing status instead.
    /// What: `Some(code)` confirms that code, `None` queries status.
    Pair {
        /// The pairing code to confirm, or `None` to query status.
        code: Option<String>,
    },
    /// Send a prompt to a Claude Code session's tmux pane.
    ///
    /// Why: `/send <session> <prompt>` drives a running session remotely —
    /// type a prompt, let it run, read back the captured pane output.
    /// What: the session id/name/prefix to resolve and the prompt to send.
    Send {
        /// Session id, friendly name, or name prefix to resolve.
        session: String,
        /// The prompt line to type into the session's tmux pane.
        prompt: String,
    },
    /// Launch a Claude Code session, running the full framework-deployment
    /// sequence first.
    ///
    /// Why: `tm launch` is the full entry point — it deploys instructions,
    /// agents, and skills into the project (`prepare_session`) before starting
    /// or attaching to the tmux-hosted session.
    /// What: the project directory to launch in and an optional explicit tmux
    /// session name (the daemon derives one from the folder when `None`).
    Launch {
        /// Project directory to launch the session in.
        project: PathBuf,
        /// Optional explicit tmux session name.
        session_name: Option<String>,
    },
    /// Connect to — or start — a Claude Code session *without* deploying the
    /// framework.
    ///
    /// Why: `tm connect` is the lightweight sibling of `Launch`. It skips the
    /// deployment sequence entirely and only starts (or attaches to) the
    /// tmux-hosted session — idempotent: create when absent, attach when not.
    /// What: the project directory and an optional explicit tmux session name.
    Connect {
        /// Project directory to connect the session to.
        project: PathBuf,
        /// Optional explicit tmux session name.
        session_name: Option<String>,
    },
    /// First-contact command: report pairing status and onboarding guidance.
    Start,
    /// Run the full trusty-mpm system diagnostic.
    ///
    /// Why: `/doctor` verifies the whole stack — instructions, agents, skills,
    /// memory, and search — in one command so an operator can confirm a
    /// healthy install without inspecting each piece by hand.
    /// What: triggers `GET /api/v1/doctor` and renders the resulting report.
    Doctor,
    /// Send a free-text message to the cross-session coordinator.
    ///
    /// Why: `tm coordinator "<message>"` is the scripting/Telegram entry point
    /// for the coordinator — a message prefixed with `@session:` routes a
    /// command at that session, a plain message is answered by the LLM with
    /// full session context.
    /// What: the message text to send to `POST /api/v1/sessions/chat`.
    CoordinatorChat {
        /// The message to send to the coordinator.
        message: String,
    },

    // ── Managed session-manager verbs (`/api/v1/sessions/managed/*`) ─────────
    //
    // These mirror the typed `DaemonClient` managed methods one-to-one and the
    // `SessionControl` verb names (launch/list/get/send/stop/resume/kill) so the
    // intent layer, the prompt catalog, and the wire surface never diverge.
    /// Spawn a managed session from a repo + ref + task (`sessions.launch`).
    ///
    /// Why: the managed analogue of `Launch` — provision an isolated git
    /// workspace and start a harness in it. Distinct from the project-session
    /// `Launch`/`Connect` family, which operate on an existing directory.
    /// What: the repo URL, git ref, task description, and optional name/runtime
    /// hints POSTed to `POST /api/v1/sessions/managed`.
    ManagedNew {
        /// Absolute path to an EXISTING local directory the session runs in (ADR-0055).
        repo_url: String,
        /// Git branch or ref to check out.
        git_ref: String,
        /// Human-readable task description for the session.
        task: String,
        /// Optional name hint overriding the auto-generated tmux session name.
        name_hint: Option<String>,
        /// Optional runtime selector (`claude-code` | `tcode`).
        runtime: Option<String>,
        /// Turnkey-injection control (#1903/#1299): `Some(false)` → metadata-only
        /// (`--no-inject`); `None`/`Some(true)` → the daemon auto-injects the
        /// task once the runtime is ready (the turnkey default).
        inject_task: Option<bool>,
        /// Optional Deliverable id to bind this session to (`--deliverable`,
        /// DOC-35 §10.6, #2379). `None` → no link, the common case.
        deliverable_id: Option<String>,
        /// Force-new control (#2450): `true` → skip the daemon's in-project
        /// reconnect pre-flight and always spawn a FRESH session (the explicit
        /// `tm session new`/`session start` verbs); `false` → the reconnect
        /// default (#1707), so chat/TUI surfaces adopt an existing live session.
        force_new: bool,
    },
    /// List every managed session (`sessions.list`).
    ManagedList,
    /// List managed sessions grouped by registered project (`sessions.fleet`).
    ///
    /// Why: operators working across multiple repos need a per-project view of
    /// their live managed sessions; a flat list (`ManagedList`) makes it hard to
    /// see which sessions belong together. `ManagedFleet` groups by project via
    /// `GET /api/v1/sessions/managed/fleet` + the daemon's `fleet_by_project`.
    /// What: calls the fleet endpoint and returns a
    /// [`super::result::CommandResult::ManagedFleet`] with one
    /// [`super::result::ProjectFleetView`] per registered project.
    ManagedFleet,
    /// Fetch one managed session's record (`sessions.get`).
    ManagedGet {
        /// Managed session id or friendly name.
        target: String,
    },
    /// Inject text into a managed session's pane (`sessions.send`).
    ManagedSend {
        /// Managed session id or friendly name.
        target: String,
        /// Text to inject into the session's tmux pane.
        text: String,
    },
    /// Answer a managed session's pending decision (`sessions.send` decision).
    ///
    /// Why: the correctness-correct decision path — unblocks a harness waiting
    /// on an operator decision via the real `.../answer` endpoint, NOT a
    /// synthetic hook. `/approve`/`/deny` on a managed session resolve here.
    /// What: the target session and the answer text to inject.
    ManagedAnswer {
        /// Managed session id or friendly name.
        target: String,
        /// The answer text to inject for the pending decision.
        answer: String,
    },
    /// Inspect a managed session's activity (`sessions.get` observe).
    ManagedActivity {
        /// Managed session id or friendly name.
        target: String,
    },
    /// Fetch a managed session's tmux attach command.
    ManagedAttachCmd {
        /// Managed session id or friendly name.
        target: String,
    },
    /// Stop a managed session's runtime, preserving the workspace
    /// (`sessions.stop`).
    ManagedRuntimeStop {
        /// Managed session id or friendly name.
        target: String,
    },
    /// Resume a stopped managed session (`sessions.resume`).
    ManagedResume {
        /// Managed session id or friendly name.
        target: String,
    },
    /// Decommission a managed session — terminal, removes the workspace
    /// (`sessions.kill`).
    ManagedDecommission {
        /// Managed session id or friendly name.
        target: String,
    },
    /// Adopt an EXISTING unmanaged tmux session into the managed store
    /// (`sessions.adopt`, #1433).
    ///
    /// Why: connect the managed surface to a pane the operator already has (a
    /// hand-started session, an externally-created one, or one whose record was
    /// lost) so it can be driven through observe/send/stop/resume. Distinct from
    /// the project-session `Adopt` (oversight-only, stateless): this REGISTERS a
    /// durable managed record via `POST /api/v1/sessions/managed/adopt`.
    /// What: the live `tmux_name` to adopt, the REQUIRED `cwd` (provenance is
    /// unknown so the operator supplies it), an optional `task`, and an optional
    /// `runtime` selector (`claude-code` | `tcode`).
    ManagedAdopt {
        /// The live tmux session name to adopt (any name; need not be `tm-`/`tmpm-`).
        tmux_name: String,
        /// Working directory the adopted session runs in (required).
        cwd: String,
        /// Optional human-readable task description (empty allowed).
        task: Option<String>,
        /// Optional runtime selector (`claude-code` | `tcode`).
        runtime: Option<String>,
    },

    /// Report daemon health: reachability + catalog freshness + fleet summary.
    ///
    /// Why: operators (and the action-capable coordinator chat) routinely ask
    /// "run a health check" / "is the daemon up?". Before this verb there was no
    /// catalog entry for it, so the coordinator chat deflected. `Health` is the
    /// one dispatchable health probe every adapter shares.
    /// What: takes no arguments; the executor probes `GET /health` and the managed
    /// list, returning a [`super::result::CommandResult::Health`]. A dead daemon
    /// yields a `Health { reachable: false, .. }` result, never a panic.
    Health,

    /// Show the command list.
    Help,
}

/// The `/help` text listing every command.
///
/// Why: a single source of truth for the help body shared by every UI; it is
/// rendered from the [`super::catalog`](mod@crate::client::catalog) registry so it can never drift from the
/// typed command set.
/// What: returns the catalog-rendered multi-line help string.
/// Test: `super::catalog::tests` asserts the catalog covers every command.
pub fn help_text() -> &'static str {
    super::catalog::help_text()
}

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

    #[test]
    fn pair_command_distinguishes_code_and_status() {
        let with_code = TrustyCommand::Pair {
            code: Some("A4X9KZ".into()),
        };
        let status = TrustyCommand::Pair { code: None };
        assert_ne!(with_code, status);
    }

    #[test]
    fn managed_variants_round_trip_by_value() {
        // The managed verbs carry their fields by value; equality is structural
        // so adapters can build, compare, and pattern-match them losslessly.
        let spawn = TrustyCommand::ManagedNew {
            repo_url: "https://example/r.git".into(),
            git_ref: "main".into(),
            task: "do the thing".into(),
            name_hint: Some("api".into()),
            runtime: Some("tcode".into()),
            inject_task: None,
            deliverable_id: None,
            force_new: false,
        };
        assert_eq!(spawn.clone(), spawn);
        let answer = TrustyCommand::ManagedAnswer {
            target: "tmpm-red-owl".into(),
            answer: "yes".into(),
        };
        assert_eq!(
            answer,
            TrustyCommand::ManagedAnswer {
                target: "tmpm-red-owl".into(),
                answer: "yes".into(),
            }
        );
        assert_ne!(
            TrustyCommand::ManagedResume { target: "a".into() },
            TrustyCommand::ManagedRuntimeStop { target: "a".into() },
        );
    }

    #[test]
    fn help_text_is_catalog_rendered() {
        // The help body is the catalog rendering — it must list managed verbs.
        let text = help_text();
        assert!(text.contains("/managed-list"));
        assert!(text.contains("/managed-decommission"));
        assert!(text.contains("/sessions"));
    }
}