From bbed46d3eb2ba16bd2c79fb74de954d6cef23d79 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 10 Apr 2026 09:08:06 -0700 Subject: [PATCH] feat: detect custom ecc2 harness markers --- ecc2/src/config/mod.rs | 7 +++ ecc2/src/main.rs | 10 +++- ecc2/src/session/manager.rs | 37 ++++++++++-- ecc2/src/session/mod.rs | 109 ++++++++++++++++++++++++++++++++++-- ecc2/src/tui/dashboard.rs | 36 +++++++++--- 5 files changed, 176 insertions(+), 23 deletions(-) diff --git a/ecc2/src/config/mod.rs b/ecc2/src/config/mod.rs index 159d78f0..0f083e39 100644 --- a/ecc2/src/config/mod.rs +++ b/ecc2/src/config/mod.rs @@ -84,6 +84,7 @@ pub struct ResolvedAgentProfile { pub struct HarnessRunnerConfig { pub program: String, pub base_args: Vec, + pub project_markers: Vec, pub cwd_flag: Option, pub session_name_flag: Option, pub task_flag: Option, @@ -752,6 +753,7 @@ impl Default for HarnessRunnerConfig { Self { program: String::new(), base_args: Vec::new(), + project_markers: Vec::new(), cwd_flag: None, session_name_flag: None, task_flag: None, @@ -1266,6 +1268,7 @@ inherits = "a" [harness_runners.cursor] program = "cursor-agent" base_args = ["run"] +project_markers = [".cursor", ".cursor/rules"] cwd_flag = "--cwd" session_name_flag = "--name" task_flag = "--task" @@ -1282,6 +1285,10 @@ ECC_HARNESS = "cursor" let runner = config.harness_runner("cursor").expect("cursor runner"); assert_eq!(runner.program, "cursor-agent"); assert_eq!(runner.base_args, vec!["run"]); + assert_eq!( + runner.project_markers, + vec![PathBuf::from(".cursor"), PathBuf::from(".cursor/rules")] + ); assert_eq!(runner.cwd_flag.as_deref(), Some("--cwd")); assert_eq!(runner.session_name_flag.as_deref(), Some("--name")); assert_eq!(runner.task_flag.as_deref(), Some("--task")); diff --git a/ecc2/src/main.rs b/ecc2/src/main.rs index decbe825..355a279c 100644 --- a/ecc2/src/main.rs +++ b/ecc2/src/main.rs @@ -1230,15 +1230,19 @@ async fn main() -> Result<()> { for s in sessions { let harness = harnesses .get(&s.id) - .map(|info| info.primary_label.clone()) - .unwrap_or_else(|| session::SessionHarnessInfo::runner_key(&s.agent_type)); + .cloned() + .unwrap_or_else(|| { + session::SessionHarnessInfo::detect(&s.agent_type, &s.working_dir) + }) + .with_config_detection(&cfg, &s.working_dir) + .primary_label; println!("{} [{}] [{}] {}", s.id, s.state, harness, s.task); } } Some(Commands::Status { session_id }) => { sync_runtime_session_metrics(&db, &cfg)?; let id = session_id.unwrap_or_else(|| "latest".to_string()); - let status = session::manager::get_status(&db, &id)?; + let status = session::manager::get_status(&db, &cfg, &id)?; println!("{status}"); } Some(Commands::Team { session_id, depth }) => { diff --git a/ecc2/src/session/manager.rs b/ecc2/src/session/manager.rs index 807605a3..d5996dc7 100644 --- a/ecc2/src/session/manager.rs +++ b/ecc2/src/session/manager.rs @@ -158,7 +158,7 @@ pub fn list_sessions(db: &StateStore) -> Result> { db.list_sessions() } -pub fn get_status(db: &StateStore, id: &str) -> Result { +pub fn get_status(db: &StateStore, cfg: &Config, id: &str) -> Result { let session = resolve_session(db, id)?; let session_id = session.id.clone(); Ok(SessionStatus { @@ -166,7 +166,8 @@ pub fn get_status(db: &StateStore, id: &str) -> Result { .get_session_harness_info(&session_id)? .unwrap_or_else(|| { SessionHarnessInfo::detect(&session.agent_type, &session.working_dir) - }), + }) + .with_config_detection(cfg, &session.working_dir), profile: db.get_session_profile(&session_id)?, session, parent_session: db.latest_task_handoff_source(&session_id)?, @@ -5500,12 +5501,38 @@ mod tests { db.insert_session(&build_session("older", SessionState::Running, older))?; db.insert_session(&build_session("newer", SessionState::Idle, newer))?; - let status = get_status(&db, "latest")?; + let status = get_status(&db, &cfg, "latest")?; assert_eq!(status.session.id, "newer"); Ok(()) } + #[test] + fn get_status_uses_configured_custom_harness_markers() -> Result<()> { + let tempdir = TestDir::new("manager-custom-harness-status")?; + fs::create_dir_all(tempdir.path().join(".acme"))?; + let mut cfg = build_config(tempdir.path()); + cfg.harness_runners.insert( + "acme-runner".to_string(), + crate::config::HarnessRunnerConfig { + project_markers: vec![PathBuf::from(".acme")], + ..Default::default() + }, + ); + let db = StateStore::open(&cfg.db_path)?; + let mut session = build_session("custom", SessionState::Pending, Utc::now()); + session.agent_type = "".to_string(); + session.working_dir = tempdir.path().to_path_buf(); + db.insert_session(&session)?; + + let status = get_status(&db, &cfg, "custom")?; + assert_eq!(status.harness.primary, HarnessKind::Unknown); + assert_eq!(status.harness.primary_label, "acme-runner"); + assert_eq!(status.harness.detected_summary(), "acme-runner"); + + Ok(()) + } + #[test] fn get_status_surfaces_handoff_lineage() -> Result<()> { let tempdir = TestDir::new("manager-status-lineage")?; @@ -5538,14 +5565,14 @@ mod tests { "task_handoff", )?; - let status = get_status(&db, "parent")?; + let status = get_status(&db, &cfg, "parent")?; let rendered = status.to_string(); assert!(rendered.contains("Children:")); assert!(rendered.contains("child")); assert!(rendered.contains("sibling")); - let child_status = get_status(&db, "child")?; + let child_status = get_status(&db, &cfg, "child")?; assert_eq!(child_status.parent_session.as_deref(), Some("parent")); Ok(()) diff --git a/ecc2/src/session/mod.rs b/ecc2/src/session/mod.rs index 2c1ba242..3b774a70 100644 --- a/ecc2/src/session/mod.rs +++ b/ecc2/src/session/mod.rs @@ -111,9 +111,34 @@ pub struct SessionHarnessInfo { pub primary: HarnessKind, pub primary_label: String, pub detected: Vec, + pub detected_labels: Vec, } impl SessionHarnessInfo { + fn detected_labels_for(detected: &[HarnessKind]) -> Vec { + detected.iter().map(|harness| harness.to_string()).collect() + } + + fn configured_detected_labels(cfg: &crate::config::Config, working_dir: &Path) -> Vec { + let mut labels = Vec::new(); + for (name, runner) in &cfg.harness_runners { + if runner.project_markers.is_empty() { + continue; + } + if runner + .project_markers + .iter() + .any(|marker| working_dir.join(marker).exists()) + { + let label = Self::runner_key(name); + if !label.is_empty() && !labels.contains(&label) { + labels.push(label); + } + } + } + labels + } + pub fn runner_key(agent_type: &str) -> String { let canonical = HarnessKind::canonical_agent_type(agent_type); match HarnessKind::from_agent_type(&canonical) { @@ -167,10 +192,12 @@ impl SessionHarnessInfo { harness => harness, }; + let detected_labels = Self::detected_labels_for(&detected); Self { primary, primary_label: Self::primary_label_for(agent_type, primary), detected, + detected_labels, } } @@ -187,6 +214,7 @@ impl SessionHarnessInfo { } let normalized_label = harness_label.trim().to_ascii_lowercase(); + let detected_labels = Self::detected_labels_for(&detected); Self { primary, primary_label: if normalized_label.is_empty() { @@ -195,18 +223,36 @@ impl SessionHarnessInfo { normalized_label }, detected, + detected_labels, } } + pub fn with_config_detection( + mut self, + cfg: &crate::config::Config, + working_dir: &Path, + ) -> Self { + for label in Self::configured_detected_labels(cfg, working_dir) { + if !self.detected_labels.contains(&label) { + self.detected_labels.push(label); + } + } + + if self.primary == HarnessKind::Unknown + && self.primary_label == HarnessKind::Unknown.as_str() + && !self.detected_labels.is_empty() + { + self.primary_label = self.detected_labels[0].clone(); + } + + self + } + pub fn detected_summary(&self) -> String { - if self.detected.is_empty() { + if self.detected_labels.is_empty() { "none detected".to_string() } else { - self.detected - .iter() - .map(|harness| harness.to_string()) - .collect::>() - .join(", ") + self.detected_labels.join(", ") } } } @@ -573,6 +619,7 @@ mod tests { harness.detected, vec![HarnessKind::Claude, HarnessKind::Codex] ); + assert_eq!(harness.detected_labels, vec!["claude", "codex"]); assert_eq!(harness.detected_summary(), "claude, codex"); Ok(()) } @@ -587,6 +634,7 @@ mod tests { assert_eq!(harness.primary, HarnessKind::Gemini); assert_eq!(harness.primary_label, "gemini"); assert_eq!(harness.detected, vec![HarnessKind::Gemini]); + assert_eq!(harness.detected_labels, vec!["gemini"]); Ok(()) } @@ -610,6 +658,7 @@ mod tests { assert_eq!(harness.primary, HarnessKind::Unknown); assert_eq!(harness.primary_label, "custom-runner"); assert!(harness.detected.is_empty()); + assert!(harness.detected_labels.is_empty()); } #[test] @@ -626,6 +675,54 @@ mod tests { harness.detected, vec![HarnessKind::Claude, HarnessKind::Codex] ); + assert_eq!(harness.detected_labels, vec!["claude", "codex"]); + Ok(()) + } + + #[test] + fn config_detection_adds_custom_markers_to_detected_summary( + ) -> Result<(), Box> { + let repo = TestDir::new("session-harness-custom-config")?; + fs::create_dir_all(repo.path().join(".acme"))?; + let mut cfg = crate::config::Config::default(); + cfg.harness_runners.insert( + "acme-runner".to_string(), + crate::config::HarnessRunnerConfig { + project_markers: vec![PathBuf::from(".acme")], + ..Default::default() + }, + ); + + let harness = + SessionHarnessInfo::detect("", repo.path()).with_config_detection(&cfg, repo.path()); + assert_eq!(harness.primary, HarnessKind::Unknown); + assert_eq!(harness.primary_label, "acme-runner"); + assert_eq!(harness.detected_labels, vec!["acme-runner"]); + assert_eq!(harness.detected_summary(), "acme-runner"); + Ok(()) + } + + #[test] + fn config_detection_preserves_custom_primary_label_and_appends_marker_matches( + ) -> Result<(), Box> { + let repo = TestDir::new("session-harness-config-append")?; + fs::create_dir_all(repo.path().join(".acme"))?; + fs::create_dir_all(repo.path().join(".codex"))?; + let mut cfg = crate::config::Config::default(); + cfg.harness_runners.insert( + "acme-runner".to_string(), + crate::config::HarnessRunnerConfig { + project_markers: vec![PathBuf::from(".acme")], + ..Default::default() + }, + ); + + let harness = SessionHarnessInfo::detect("acme-runner", repo.path()) + .with_config_detection(&cfg, repo.path()); + assert_eq!(harness.primary, HarnessKind::Unknown); + assert_eq!(harness.primary_label, "acme-runner"); + assert_eq!(harness.detected_labels, vec!["codex", "acme-runner"]); + assert_eq!(harness.detected_summary(), "codex, acme-runner"); Ok(()) } diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index a73be631..c2f94056 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -476,6 +476,29 @@ impl SessionCompletionSummary { } } +fn load_session_harnesses( + db: &StateStore, + cfg: &Config, + sessions: &[Session], +) -> HashMap { + let working_dirs = sessions + .iter() + .map(|session| (session.id.as_str(), session.working_dir.as_path())) + .collect::>(); + db.list_session_harnesses() + .unwrap_or_default() + .into_iter() + .map(|(session_id, info)| { + let info = if let Some(working_dir) = working_dirs.get(session_id.as_str()) { + info.with_config_detection(cfg, working_dir) + } else { + info + }; + (session_id, info) + }) + .collect() +} + impl Dashboard { pub fn new(db: StateStore, cfg: Config) -> Self { Self::with_output_store(db, cfg, SessionOutputStore::default()) @@ -498,7 +521,7 @@ impl Dashboard { let _ = db.sync_tool_activity_metrics(&cfg.tool_activity_metrics_path()); } let sessions = db.list_sessions().unwrap_or_default(); - let session_harnesses = db.list_session_harnesses().unwrap_or_default(); + let session_harnesses = load_session_harnesses(&db, &cfg, &sessions); let initial_session_states = sessions .iter() .map(|session| (session.id.clone(), session.state.clone())) @@ -4040,13 +4063,7 @@ impl Dashboard { Vec::new() } }; - self.session_harnesses = match self.db.list_session_harnesses() { - Ok(harnesses) => harnesses, - Err(error) => { - tracing::warn!("Failed to refresh session harnesses: {error}"); - HashMap::new() - } - }; + self.session_harnesses = load_session_harnesses(&self.db, &self.cfg, &self.sessions); self.unread_message_counts = match self.db.unread_message_counts() { Ok(counts) => counts, Err(error) => { @@ -14488,7 +14505,8 @@ diff --git a/src/lib.rs b/src/lib.rs .map(|session| { ( session.id.clone(), - SessionHarnessInfo::detect(&session.agent_type, &session.working_dir), + SessionHarnessInfo::detect(&session.agent_type, &session.working_dir) + .with_config_detection(&cfg, &session.working_dir), ) }) .collect();