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
//! `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,
},
}