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
//! `tm launch --twin`: arm the `claude` a launch starts for supervisor-twin
//! mode (#8878, ruling D1).
//!
//! Why: twin mode needs an arming the session cannot give itself. `tm launch
//! --twin` is that arming: it records the one `claude` process it started, by
//! PID and start time, in `~/.trusty-mpm/twin/armed/`.
//! What: [`preflight`] refuses `--twin --worktree` before `tm launch` does
//! anything; [`precheck`] refuses `--twin` before any clone or worktree is
//! provisioned unless the user-level grant holds for the managed checkout, and
//! [`confirm_placement`] refuses a session placed anywhere else;
//! [`refuse_reattach`] and [`refuse_live_checkout`] refuse `--twin` where this
//! command starts no `claude` of its own; [`arm`] writes the record once the
//! `claude` PID is known. Arming failure never stops the launch: the session
//! runs as a plain supervisor, which is what the hook concludes without a record.
//! Test: `cli_parses_launch_with_twin`,
//! `a_twin_worktree_launch_is_refused_before_anything_is_touched`,
//! `twin_preflight_refuses_only_twin_with_worktree`,
//! `twin_placement_must_be_the_checked_directory`,
//! `twin_refusals_apply_only_to_twin_launches`; the record and grant logic in
//! `core/twin_identity_tests.rs` (`an_arming_record_round_trips`,
//! `a_supervisor_without_the_twin_grant_is_not_twin`).
use std::path::{Path, PathBuf};
use trusty_mpm::core::twin_arming;
use trusty_mpm::core::twin_identity;
/// Refuse `--twin --worktree` before `tm launch` touches anything.
///
/// Why: a per-session worktree is never the granted managed checkout, so the
/// grant check would always refuse it — after the clone, worktree and branch
/// were already made and left behind (#8878 review).
/// What: `Err` exactly when both flags are set.
/// Test: `twin_preflight_refuses_only_twin_with_worktree`,
/// `a_twin_worktree_launch_is_refused_before_anything_is_touched`.
pub(crate) fn preflight(twin: bool, worktree: bool) -> anyhow::Result<()> {
if twin && worktree {
anyhow::bail!(
"`--twin` refused with `--worktree`: twin mode is granted to the managed \
checkout itself, never to a per-session worktree. Launch without `--worktree`."
);
}
Ok(())
}
/// Refuse a twin session placed anywhere but the directory [`precheck`] passed.
///
/// Why: the grant is checked before provisioning, against the managed checkout
/// the placement rule should select; this confirms provisioning selected it.
/// What: `Ok` when both paths canonicalize to one directory; `Err` when they
/// differ or either cannot be canonicalized.
/// Test: `twin_placement_must_be_the_checked_directory`.
pub(crate) fn confirm_placement(checked: &Path, placed: &Path) -> anyhow::Result<()> {
let canon = |p: &Path| {
std::fs::canonicalize(p)
.map_err(|e| anyhow::anyhow!("`--twin` refused: {}: {e}", p.display()))
};
let (checked_c, placed_c) = (canon(checked)?, canon(placed)?);
if checked_c != placed_c {
anyhow::bail!(
"`--twin` refused: the session was placed in {}, not the granted {}",
placed_c.display(),
checked_c.display()
);
}
Ok(())
}
/// Refuse `--twin` unless `project_dir` holds the user-level twin grant.
///
/// Why: a launch that asked for twin mode and cannot get it must say so
/// before the operator starts working, not leave them in a plain supervisor.
/// What: reads `~/.trusty-mpm/config.toml` strictly and runs
/// [`twin_identity::check_grant`] — the check the hook runs. Returns the
/// `~/.trusty-mpm` root the record will be written under. `tm launch` calls it
/// on the managed checkout before provisioning anything (#8878 review).
/// Test: the grant rule is `a_supervisor_without_the_twin_grant_is_not_twin`.
pub(crate) fn precheck(project_dir: &Path) -> anyhow::Result<PathBuf> {
let root = dirs::home_dir()
.map(|home| home.join(".trusty-mpm"))
.ok_or_else(|| anyhow::anyhow!("`--twin` refused: the home directory is unknown"))?;
let config = twin_arming::load_user_config_strict(&root)
.map_err(|e| anyhow::anyhow!("`--twin` refused: the user config could not be read: {e}"))?;
twin_identity::check_grant(project_dir, &config).map_err(|refusal| {
anyhow::anyhow!(
"`--twin` refused for {}: {refusal}. Twin mode needs the project in both \
`[supervisor] projects` and `[supervisor.twin] projects` in \
~/.trusty-mpm/config.toml, and `profile = \"supervisor\"` in its \
.trusty-mpm.toml (#8878).",
project_dir.display()
)
})?;
Ok(root)
}
/// Refuse `--twin` for a checkout with no origin remote, which `tm launch`
/// hands to `tm connect`; that path does not arm.
/// Test: `twin_refusals_apply_only_to_twin_launches`.
pub(crate) fn refuse_live_checkout(twin: bool) -> anyhow::Result<()> {
if twin {
anyhow::bail!(
"`--twin` refused: this checkout has no origin remote, and its session starts \
through `tm connect`, which does not arm twin mode."
);
}
Ok(())
}
/// Refuse `--twin` when the launch would reattach to a running session.
/// Test: `twin_refusals_apply_only_to_twin_launches`.
pub(crate) fn refuse_reattach(twin: bool, session: &str) -> anyhow::Result<()> {
if twin {
anyhow::bail!(
"`--twin` refused: session {session} is already running, and twin mode arms only \
a claude this command starts. Stop that session, or reattach without `--twin`."
);
}
Ok(())
}
/// Record the started `claude` as armed; report the outcome on stderr.
///
/// What: `claude_pid` `None` (not found in the pane) or an
/// [`twin_arming::arm_claude`] error prints a warning and leaves the session
/// unarmed.
pub(crate) fn arm(root: &Path, claude_pid: Option<u32>, project_dir: &Path) {
let Some(pid) = claude_pid else {
eprintln!("warning: twin mode NOT armed: the claude process could not be found");
return;
};
match twin_arming::arm_claude(root, pid, project_dir) {
Ok(record) => eprintln!(
"twin mode armed for claude pid {} ({})",
record.pid,
twin_arming::record_path(root, pid).display()
),
Err(e) => eprintln!("warning: twin mode NOT armed: {e}"),
}
}