trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
//! `tm repair` deploy-state recovery command group.
//!
//! Why: extracted from `cli.rs` (issue #2603) to keep the top-level file
//! under the 500-SLOC production cap.
//! What: [`RepairAction`] — `deploy` and `push-guard`.
//! Test: `cli_parses_repair_deploy`, `cli_parses_repair_push_guard` in `tests.rs`.

use clap::Subcommand;

/// Actions for the `repair` subcommand.
///
/// Why: scoped sub-actions keep `tm repair` extensible — future variants can
/// cover other deploy artefacts without changing the top-level command surface.
/// What: `Deploy` covers agent and skill manifests; `PushGuard` retrofits the
/// #2867 cross-branch push guard onto an already-provisioned clone.
/// Test: `cli_parses_repair_deploy`, `cli_parses_repair_push_guard`.
#[derive(Debug, Subcommand)]
pub(crate) enum RepairAction {
    /// Repair the agent/skill deploy state in `~/.claude/`.
    ///
    /// Why: a crash during `tm install` or `tm sessions start` may leave stale
    /// `.tmp` staging files in `~/.claude/agents/` or `~/.claude/skills/`, or
    /// leave either manifest corrupt. This command removes the orphans and
    /// validates both manifests.
    /// What: for each target directory (`~/.claude/agents/`, skill subdirs under
    /// `~/.claude/skills/`), removes `*.tmp` orphans and validates the manifest.
    /// With `--force`, resets a corrupt agent manifest to empty so the next
    /// `tm install` performs a fresh full deploy.
    /// Test: `cli_parses_repair_deploy`.
    Deploy {
        /// Reset a corrupt manifest to empty (triggers full re-deploy on next
        /// `tm install`). Without this flag, a corrupt manifest is reported
        /// but not modified.
        #[arg(long)]
        force: bool,
    },

    /// Retrofit the #2867 cross-branch `pre-push` guard onto an existing clone.
    ///
    /// Why: the guard is installed automatically only when trusty-mpm CLONES a
    /// repository, so every base clone that already existed when the guard
    /// shipped is unprotected — including long-lived ones shared by dozens of
    /// worktrees, which is exactly the population #2867 happened in. Without
    /// this verb there is no supported way to close that gap.
    /// What: installs the guard into the clone's SHARED hooks directory
    /// (`$GIT_COMMON_DIR/hooks`), which every linked and ad-hoc worktree of
    /// that clone consults — so one invocation from any worktree covers them
    /// all, with no git config write. Idempotent: re-running an up-to-date
    /// guard writes nothing. It never overwrites a `pre-push` it does not own
    /// (a foreign hook, a symlink, an unreadable file, or a `core.hooksPath`
    /// redirect all produce a refusal that names the reason). `--dry-run`
    /// reports what would happen and writes nothing at all.
    /// Test: `cli_parses_repair_push_guard`, `cli_parses_repair_push_guard_flags`.
    PushGuard {
        /// Repository (or any worktree of it) to protect. Defaults to the
        /// current directory.
        #[arg(long)]
        path: Option<String>,
        /// Report what would happen without writing anything.
        #[arg(long)]
        dry_run: bool,
    },

    /// Recover a corrupt managed-session store (`sessions.json`).
    ///
    /// Why (#5007): the store is read before every write, so a file the daemon
    /// cannot parse is a file it can never rewrite — the corruption is
    /// permanent until something outside the daemon repairs it. On 2026-08-06
    /// that something was a human hand-truncating JSON at 09:00 while every
    /// session mutation failed. This is that repair, automated.
    /// What: backs the file up to a timestamped sibling
    /// (`sessions.json.corrupt-backup-<YYYYMMDD-HHMMSS>`) and truncates it to
    /// the longest complete JSON document at its head. It REFUSES when the
    /// bytes it would discard name a session id the recovered document does not
    /// contain, unless `--force`. `--dry-run` reports the cut and writes
    /// nothing. A store that parses is left alone.
    /// Test: `cli_parses_repair_session_store`,
    /// `cli_parses_repair_session_store_flags`.
    SessionStore {
        /// Store file to repair. Defaults to
        /// `~/.trusty-mpm/session-manager/sessions.json`.
        #[arg(long)]
        path: Option<String>,
        /// Report the cut without writing anything.
        #[arg(long)]
        dry_run: bool,
        /// Discard the trailing bytes even when they name a session id the
        /// recovered document does not contain.
        #[arg(long)]
        force: bool,
    },

    /// Quarantine the savings-ledger rows unit tests wrote (#7569).
    ///
    /// Why: #7514 let the instruction-compression producer resolve a fixture
    /// path to a session id, so every test run appended a row to the
    /// OPERATOR's real ledger. Its fix stops new rows arriving; it cannot
    /// retract the 228 already there, and they claim 1,498,419 of the
    /// ledger's 1,501,558 instruction-compression tokens — which is what the
    /// `💸` statusline average is computed over.
    /// What: reports what it would move and writes NOTHING unless `--apply`.
    /// With `--apply` it moves the matched rows to
    /// `<ledger>.quarantine-<stamp>.jsonl` (never deleting one), rewrites the
    /// ledger atomically from the kept rows' original bytes, and repairs the
    /// #7579 lines that hold two rows with no separator by splitting them and
    /// judging each half. `--markers` additionally moves the
    /// `usage/no-fold-warned/` directory aside. Re-running finds nothing.
    /// Test: `cli_parses_repair_savings_ledger`,
    /// `cli_parses_repair_savings_ledger_flags`.
    SavingsLedger {
        /// Framework root holding `usage/savings.jsonl`. Defaults to
        /// `~/.trusty-mpm`.
        #[arg(long)]
        root: Option<String>,
        /// Move the matched rows. Without it the command only reports.
        #[arg(long)]
        apply: bool,
        /// Report without writing anything — the default, and the way to say
        /// so explicitly.
        #[arg(long, conflicts_with = "apply")]
        dry_run: bool,
        /// Also move the `usage/no-fold-warned/` marker directory aside.
        #[arg(long)]
        markers: bool,
    },

    /// End a delegation record stuck non-terminal (#7602).
    ///
    /// Why: a dispatched agent's record leaves `Running` only on a
    /// `SubagentStop` the daemon receives. When the parent session dies without
    /// one, the record stays live forever, and every worktree gate — the
    /// merged-PR reclaim and `tm session adopt-worktree` both — refuses the
    /// agent's tree with "a delegation naming it has not ended". No verb ended
    /// such a record before this one.
    /// What: asks the daemon to terminalize every non-terminal record naming
    /// `<agent-id>`. The daemon REFUSES while the dispatching session is still
    /// Active, whatever flags are passed; it also refuses when it holds no
    /// record of that session, since the delegation map is rebuilt empty at
    /// every daemon start and silence is undeterminable rather than absent —
    /// `--force` is the operator asserting the owner is gone. The status
    /// written is `cancelled`, never `completed`.
    /// #8257: `--delegation-id` reaches a record with no agent id, and
    /// `--list [DIR]` prints the live records for a directory without writing.
    /// Test: `cli_parses_repair_delegation`, `cli_parses_repair_delegation_force`,
    /// `cli_parses_repair_delegation_by_id_and_list_8257`.
    Delegation {
        /// The agent id from the worktree's ownership sentinel, e.g.
        /// `af20cc838b2b30a55`.
        #[arg(required_unless_present_any = ["delegation_id", "list"],
              conflicts_with_all = ["delegation_id", "list"])]
        agent_id: Option<String>,
        /// Address the record by its delegation id instead — the only address
        /// a record whose stop was matched by agent type has (#8257).
        #[arg(long, conflicts_with = "list")]
        delegation_id: Option<String>,
        /// List the live delegation records for DIR (default: the current
        /// directory), read-only (#8257).
        #[arg(long, value_name = "DIR", num_args = 0..=1, default_missing_value = ".",
              conflicts_with = "force")]
        list: Option<std::path::PathBuf>,
        /// End the record even though the daemon cannot confirm the dispatching
        /// session is gone.
        #[arg(long)]
        force: bool,
    },
}