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
//! `confer arm` — the one paved command to (re-)arm your watcher correctly (design/49).
//!
//! Self-locates your role's clone (the current clone, or the single watch target this session
//! owns in the auto-heal registry), then runs the watch loop with `--replace` and
//! `--delivery monitor` so you can never forget either. It is a long-lived streamer, same as
//! `confer watch` — meant to be hosted under the Monitor tool via the `/confer-arm` skill, which
//! is Monitor-only by construction so the watch can't be backgrounded (the one mistake that sends
//! wakes nowhere). `confer watch` stays the low-level primitive; `arm` is the pit-of-success path.
use crate::{autoheal, config, watch};
use anyhow::{anyhow, Result};
use std::path::PathBuf;
/// Resolve the clone dir to arm. The current dir wins when it's inside a confer hub clone (the
/// unambiguous common case); otherwise fall back to a watch target this session (or the named
/// role) owns in the auto-heal registry. Refuses to guess across multiple owned targets — asks
/// for a `cd` or `--role` instead, so `arm` never arms the wrong role's watcher.
fn resolve_clone(role: &Option<String>) -> Result<PathBuf> {
// 1. CWD is itself a hub clone → unambiguous, use it (mirrors how `watch` resolves).
if let Ok(root) = config::repo_root() {
return Ok(root);
}
// 2. Fall back to the watch-registry targets this session/role owns (the post-compaction
// case, where the agent's cwd isn't its clone). `owned_by_session` never returns a
// co-resident peer's target, so this can't arm someone else's watcher.
let reg = autoheal::load();
let me_session = autoheal::current_session();
let mut owned: Vec<&autoheal::Target> = reg
.targets
.iter()
.filter(|t| autoheal::owned_by_session(t, &me_session, role))
.filter(|t| role.as_ref().is_none_or(|r| &t.role == r))
.collect();
owned.sort_by(|a, b| a.hub.cmp(&b.hub).then(a.role.cmp(&b.role)));
owned.dedup_by(|a, b| a.hub == b.hub && a.role == b.role);
match owned.as_slice() {
[] => Err(anyhow!(
"confer arm: not inside a confer clone, and no watch target owned by this session.\n\
cd into your role's clone and re-run (see `confer clones`), pass `--role <r>`, or \
`confer reconnect`."
)),
[t] => Ok(PathBuf::from(&t.hub)),
many => {
let list = many
.iter()
.map(|t| format!(" • {} @ {}", t.role, t.hub))
.collect::<Vec<_>>()
.join("\n");
Err(anyhow!(
"confer arm: several watch targets are owned here — cd into the one you mean, or \
pass `--role <r>`:\n{list}"
))
}
}
}
/// Arm (or re-arm) the watcher the one correct way. Locates the clone, enters it, and streams
/// wakes with `--replace` + `--delivery monitor` baked in. Long-lived: returns only when the
/// watch loop ends (killed / replaced).
///
/// `topic`/`all`/`min_priority`/`wake_on` resolve exactly like `confer watch`'s own flags — explicit
/// CLI > saved per-(hub,role) machine-config preference > built-in default — and an explicit flag here
/// saves the resolved bundle, so the NEXT bare `arm` (including the post-compaction auto-heal re-arm)
/// reloads it without re-deciding (design/51 §6/Phase B).
pub fn run(
role: Option<String>,
topic: Option<String>,
all: bool,
min_priority: Option<String>,
wake_on: Option<String>,
session: Option<String>,
) -> Result<()> {
let clone = resolve_clone(&role)?;
std::env::set_current_dir(&clone)
.map_err(|e| anyhow!("confer arm: cannot enter clone {}: {e}", clone.display()))?;
let hub_key = config::hub_key(&clone);
let resolved_role = config::resolve_role(role.clone(), &clone).unwrap_or_default();
let (wake_on, min_priority, topic, all) = watch::resolve_watch_prefs(
&hub_key,
&resolved_role,
wake_on.as_deref(),
min_priority.as_deref(),
topic.as_deref(),
all,
)?;
// The one right way, baked in so it can't be forgotten: take over any orphan (`--replace`),
// and stamp the delivery method so `watch-status` can affirm we actually deliver wakes. Every
// other option is the plain `watch` default.
watch::run(watch::WatchOpts {
topic,
role,
json: false,
poll_secs: 10,
advance: true,
replace: true,
all,
min_priority,
wake_on,
no_version_notice: false,
delivery: Some("monitor".to_string()),
session,
})
}