use crate::templates::CONFER_SKILLS;
use crate::{autoheal, config, hooks::write_session_hook, BUILD_SHA};
use anyhow::{anyhow, Result};
use std::path::{Path, PathBuf};
pub(crate) const HARNESS_SKILL_HOMES: &[(&str, &str)] = &[("claude", ".claude"), ("grok", ".grok")];
fn harness_skill_dir(home: &Path, harness: &str) -> Option<PathBuf> {
HARNESS_SKILL_HOMES
.iter()
.find(|(h, _)| *h == harness)
.map(|(_, sub)| home.join(sub).join("skills"))
}
pub(crate) fn detect_harness() -> &'static str {
if std::env::var("GROK_AGENT").ok().filter(|s| !s.is_empty()).is_some() {
"grok"
} else {
"claude"
}
}
fn harness_rewrite(text: &str, harness: &str) -> String {
match harness {
"grok" => text
.replace("Monitor", "monitor")
.replace("Bash", "run_terminal_command")
.replace("AskUserQuestion", "ask_user_question")
.replace("/loop 45s", "/loop 60s"),
_ => text.to_string(), }
}
fn resync_dir(dir: &Path, bin: &str, harness: &str) -> bool {
if !dir.join("confer-watch").join("SKILL.md").is_file() {
return false; }
let marker = dir.join("confer-watch").join(".confer-build");
if std::fs::read_to_string(&marker).unwrap_or_default().trim() == BUILD_SHA {
return false; }
for (name, tmpl) in CONFER_SKILLS {
let filled = harness_rewrite(&tmpl.replace("{CONFER}", bin), harness);
if filled.contains("{ROLE}") || filled.contains("{HUB}") {
return false; }
let d = dir.join(name);
if std::fs::create_dir_all(&d).is_err() || std::fs::write(d.join("SKILL.md"), filled).is_err() {
return false;
}
}
let _ = std::fs::write(&marker, BUILD_SHA);
true
}
pub(crate) fn resync_skills_if_stale() -> Option<String> {
let home = config::home().ok()?;
let bin = std::env::current_exe().ok()?.to_string_lossy().to_string();
let mut acted = false;
for (harness, sub) in HARNESS_SKILL_HOMES {
acted |= resync_dir(&home.join(sub).join("skills"), &bin, harness);
}
acted.then(|| BUILD_SHA.to_string())
}
pub(crate) fn cmd_install_skill(
dir: Option<String>,
harness: Option<String>,
hub: Option<String>,
role: Option<String>,
no_autoheal: bool,
) -> Result<()> {
let bin = std::env::current_exe()?.to_string_lossy().to_string();
let hub_root = match hub {
Some(h) => std::fs::canonicalize(&h).unwrap_or_else(|_| std::path::PathBuf::from(h)),
None => config::repo_root()?,
};
let role = match role {
Some(r) => r,
None => config::resolve_role(None, &hub_root)?,
};
let home = config::home()?;
let targets: Vec<(&str, PathBuf)> = if let Some(d) = dir {
vec![(detect_harness(), PathBuf::from(d))]
} else {
match harness.as_deref().unwrap_or("auto") {
"all" => HARNESS_SKILL_HOMES.iter().map(|(h, s)| (*h, home.join(s).join("skills"))).collect(),
"auto" => {
let h = detect_harness();
vec![(h, harness_skill_dir(&home, h).expect("the detected harness is always known"))]
}
want => match HARNESS_SKILL_HOMES.iter().find(|(h, _)| *h == want) {
Some((h, s)) => vec![(*h, home.join(s).join("skills"))],
None => {
return Err(anyhow!("unknown --harness '{want}' — expected auto | claude | grok | all"))
}
},
}
};
let base_fill = |t: &str| {
t.replace("{CONFER}", &bin)
.replace("{HUB}", &hub_root.to_string_lossy())
.replace("{ROLE}", &role)
};
for (harness, dir) in &targets {
for (name, tmpl) in CONFER_SKILLS {
let d = dir.join(name);
std::fs::create_dir_all(&d)?;
std::fs::write(d.join("SKILL.md"), harness_rewrite(&base_fill(tmpl), harness))?;
}
let _ = std::fs::write(dir.join("confer-watch").join(".confer-build"), BUILD_SHA);
let names = CONFER_SKILLS.iter().map(|(n, _)| *n).collect::<Vec<_>>().join(",");
println!("wrote {}/{{{names}}}/SKILL.md", dir.display());
for legacy in ["watch", "check-blackboard", "confer-fleet-ops", "confer-fleetop", "confer-norms"] {
let sk = dir.join(legacy).join("SKILL.md");
if std::fs::read_to_string(&sk).map(|s| s.contains("confer")).unwrap_or(false) {
let _ = std::fs::remove_dir_all(dir.join(legacy));
println!(" migrated: removed legacy /{legacy}");
}
}
}
println!(" confer: {bin}");
println!(" hub: {}", hub_root.display());
println!(" role: {role}");
if !no_autoheal {
let settings = config::home()?.join(".claude").join("settings.json");
match write_session_hook(&settings, &format!("{bin} session-heal")) {
Ok(()) => {
let _ = autoheal::set_enabled(true);
println!(" auto-heal: installed SessionStart hook → {} and enabled (confer autoheal off to disable)", settings.display());
}
Err(e) => eprintln!(
" auto-heal: skipped (couldn't edit {}: {e})",
settings.display()
),
}
}
println!(
"use: /confer-watch (Monitor, reactive/dormant) or /loop 45s /confer-poll (poll fallback)."
);
Ok(())
}