feat: auto-resolve ecc2 harnesses from repo markers

This commit is contained in:
Affaan Mustafa
2026-04-10 10:12:35 -07:00
parent 050d9a9707
commit b51792fe0e
2 changed files with 202 additions and 21 deletions

View File

@@ -1067,7 +1067,7 @@ pub async fn rebalance_team_backlog(
return Ok(outcomes);
}
let delegates = direct_delegate_sessions(db, &lead.id, agent_type)?;
let delegates = direct_delegate_sessions(db, cfg, &lead, agent_type)?;
let unread_counts = db.unread_message_counts()?;
let team_has_capacity = delegates.len() < cfg.max_parallel_sessions;
@@ -1099,7 +1099,7 @@ pub async fn rebalance_team_backlog(
break;
}
let current_delegates = direct_delegate_sessions(db, &lead.id, agent_type)?;
let current_delegates = direct_delegate_sessions(db, cfg, &lead, agent_type)?;
let current_unread_counts = db.unread_message_counts()?;
let current_team_has_capacity = current_delegates.len() < cfg.max_parallel_sessions;
let current_has_clear_idle_elsewhere = current_delegates.iter().any(|candidate| {
@@ -1567,7 +1567,7 @@ async fn assign_session_in_dir_with_runner_program(
.task_group
.or_else(|| normalize_group_label(&lead.task_group)),
};
let delegates = direct_delegate_sessions(db, &lead.id, agent_type)?;
let delegates = direct_delegate_sessions(db, cfg, &lead, agent_type)?;
let delegate_handoff_backlog = delegates
.iter()
.map(|session| {
@@ -2601,7 +2601,6 @@ async fn queue_session_with_resolved_profile_and_runner_program(
.as_ref()
.and_then(|profile| profile.agent.as_deref())
.unwrap_or(agent_type);
let effective_agent_type = HarnessKind::canonical_agent_type(effective_agent_type);
let session = build_session_record(
db,
task,
@@ -2658,7 +2657,8 @@ fn build_session_record(
repo_root: &Path,
grouping: SessionGrouping,
) -> Result<Session> {
let canonical_agent_type = HarnessKind::canonical_agent_type(agent_type);
let canonical_agent_type =
SessionHarnessInfo::resolve_requested_agent_type(cfg, agent_type, repo_root);
let id = uuid::Uuid::new_v4().to_string()[..8].to_string();
let now = chrono::Utc::now();
@@ -2809,12 +2809,15 @@ async fn spawn_session_runner(
fn direct_delegate_sessions(
db: &StateStore,
lead_id: &str,
cfg: &Config,
lead: &Session,
agent_type: &str,
) -> Result<Vec<Session>> {
let target_harness = HarnessKind::from_agent_type(agent_type);
let resolved_agent_type =
SessionHarnessInfo::resolve_requested_agent_type(cfg, agent_type, &lead.working_dir);
let target_harness = HarnessKind::from_agent_type(&resolved_agent_type);
let mut sessions = Vec::new();
for child_id in db.delegated_children(lead_id, 50)? {
for child_id in db.delegated_children(&lead.id, 50)? {
let Some(session) = db.get_session(&child_id)? else {
continue;
};
@@ -2823,7 +2826,7 @@ fn direct_delegate_sessions(
if HarnessKind::from_agent_type(&session.agent_type) != target_harness {
continue;
}
} else if session.agent_type != HarnessKind::canonical_agent_type(agent_type) {
} else if session.agent_type != resolved_agent_type {
continue;
}
@@ -2904,7 +2907,8 @@ fn summarize_backlog_pressure(
let mut summary = BacklogPressureSummary::default();
for (session_id, _) in targets {
let delegates = direct_delegate_sessions(db, session_id, agent_type)?;
let lead = resolve_session(db, session_id)?;
let delegates = direct_delegate_sessions(db, cfg, &lead, agent_type)?;
let has_clear_idle_delegate = delegates.iter().any(|delegate| {
delegate.state == SessionState::Idle
&& db.unread_task_handoff_count(&delegate.id).unwrap_or(0) == 0
@@ -3615,7 +3619,7 @@ pub fn preview_assignment_for_task(
agent_type: &str,
) -> Result<AssignmentPreview> {
let lead = resolve_session(db, lead_id)?;
let delegates = direct_delegate_sessions(db, &lead.id, agent_type)?;
let delegates = direct_delegate_sessions(db, cfg, &lead, agent_type)?;
let delegate_handoff_backlog = delegates
.iter()
.map(|session| {
@@ -4579,12 +4583,96 @@ mod tests {
"task_handoff",
)?;
let delegates = direct_delegate_sessions(&db, "lead", "claude")?;
let lead = resolve_session(&db, "lead")?;
let delegates = direct_delegate_sessions(&db, &cfg, &lead, "claude")?;
assert_eq!(delegates.len(), 1);
assert_eq!(delegates[0].id, "child");
Ok(())
}
#[test]
fn direct_delegate_sessions_resolves_auto_to_configured_harness() -> Result<()> {
let tempdir = TestDir::new("manager-delegate-auto-custom-harness")?;
let repo_root = tempdir.path().join("repo");
init_git_repo(&repo_root)?;
fs::create_dir_all(repo_root.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 now = Utc::now();
db.insert_session(&Session {
id: "lead".to_string(),
task: "Lead task".to_string(),
project: "workspace".to_string(),
task_group: "general".to_string(),
agent_type: "acme-runner".to_string(),
working_dir: repo_root.clone(),
state: SessionState::Running,
pid: Some(42),
worktree: None,
created_at: now,
updated_at: now,
last_heartbeat_at: now,
metrics: SessionMetrics::default(),
})?;
db.insert_session(&Session {
id: "custom-child".to_string(),
task: "Delegate task".to_string(),
project: "workspace".to_string(),
task_group: "general".to_string(),
agent_type: "acme-runner".to_string(),
working_dir: repo_root.clone(),
state: SessionState::Idle,
pid: Some(7),
worktree: None,
created_at: now,
updated_at: now,
last_heartbeat_at: now,
metrics: SessionMetrics::default(),
})?;
db.insert_session(&Session {
id: "claude-child".to_string(),
task: "Other delegate task".to_string(),
project: "workspace".to_string(),
task_group: "general".to_string(),
agent_type: "claude".to_string(),
working_dir: repo_root.clone(),
state: SessionState::Idle,
pid: Some(8),
worktree: None,
created_at: now,
updated_at: now,
last_heartbeat_at: now,
metrics: SessionMetrics::default(),
})?;
db.send_message(
"lead",
"custom-child",
"{\"task\":\"Delegate task\",\"context\":\"Delegated from lead\"}",
"task_handoff",
)?;
db.send_message(
"lead",
"claude-child",
"{\"task\":\"Other delegate task\",\"context\":\"Delegated from lead\"}",
"task_handoff",
)?;
let lead = resolve_session(&db, "lead")?;
let delegates = direct_delegate_sessions(&db, &cfg, &lead, "auto")?;
assert_eq!(delegates.len(), 1);
assert_eq!(delegates[0].id, "custom-child");
Ok(())
}
#[test]
fn enforce_session_heartbeats_marks_overdue_running_sessions_stale() -> Result<()> {
let tempdir = TestDir::new("manager-heartbeat-stale")?;
@@ -4786,6 +4874,37 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn create_session_resolves_auto_agent_from_repo_markers() -> Result<()> {
let tempdir = TestDir::new("manager-create-session-auto-agent")?;
let repo_root = tempdir.path().join("repo");
init_git_repo(&repo_root)?;
fs::create_dir_all(repo_root.join(".codex"))?;
let cfg = build_config(tempdir.path());
let db = StateStore::open(&cfg.db_path)?;
let (fake_runner, _log_path) = write_fake_claude(tempdir.path())?;
let session_id = create_session_in_dir(
&db,
&cfg,
"implement lifecycle",
"auto",
false,
&repo_root,
&fake_runner,
)
.await?;
let session = db
.get_session(&session_id)?
.context("session should exist")?;
assert_eq!(session.agent_type, "codex");
stop_session_with_options(&db, &session_id, false).await?;
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn create_session_derives_project_and_task_group_defaults() -> Result<()> {
let tempdir = TestDir::new("manager-create-session-grouping-defaults")?;
@@ -7229,7 +7348,7 @@ mod tests {
let now = Utc::now();
db.insert_session(&Session {
id: "worker".to_string(),
id: "lead".to_string(),
task: "worker task".to_string(),
project: "workspace".to_string(),
task_group: "general".to_string(),
@@ -7245,7 +7364,7 @@ mod tests {
})?;
db.insert_session(&Session {
id: "worker-child".to_string(),
id: "delegate".to_string(),
task: "delegate task".to_string(),
project: "workspace".to_string(),
task_group: "general".to_string(),
@@ -7261,31 +7380,31 @@ mod tests {
})?;
db.send_message(
"worker",
"worker-child",
"lead",
"delegate",
"{\"task\":\"seed delegate\",\"context\":\"Delegated from worker\"}",
"task_handoff",
)?;
let _ = db.mark_messages_read("worker-child")?;
let _ = db.mark_messages_read("delegate")?;
db.send_message(
"planner",
"worker",
"lead",
"{\"task\":\"task-a\",\"context\":\"Inbound\"}",
"task_handoff",
)?;
db.send_message(
"planner",
"worker",
"lead",
"{\"task\":\"task-b\",\"context\":\"Inbound\"}",
"task_handoff",
)?;
let outcome = coordinate_backlog(&db, &cfg, "claude", true, 10).await?;
assert_eq!(outcome.remaining_backlog_sessions, 1);
assert_eq!(outcome.remaining_backlog_sessions, 2);
assert_eq!(outcome.remaining_backlog_messages, 2);
assert_eq!(outcome.remaining_absorbable_sessions, 0);
assert_eq!(outcome.remaining_absorbable_sessions, 1);
assert_eq!(outcome.remaining_saturated_sessions, 1);
Ok(())

View File

@@ -248,6 +248,24 @@ impl SessionHarnessInfo {
self
}
pub fn resolve_requested_agent_type(
cfg: &crate::config::Config,
requested_agent_type: &str,
working_dir: &Path,
) -> String {
let canonical = HarnessKind::canonical_agent_type(requested_agent_type);
if !canonical.is_empty() && canonical != "auto" {
return canonical;
}
let detected = Self::detect("", working_dir).with_config_detection(cfg, working_dir);
if detected.primary_label != HarnessKind::Unknown.as_str() {
return Self::runner_key(&detected.primary_label);
}
HarnessKind::Claude.as_str().to_string()
}
pub fn detected_summary(&self) -> String {
if self.detected_labels.is_empty() {
"none detected".to_string()
@@ -812,4 +830,48 @@ mod tests {
);
assert_eq!(SessionHarnessInfo::runner_key("claude-code"), "claude");
}
#[test]
fn resolve_requested_agent_type_uses_detected_builtin_marker_for_auto(
) -> Result<(), Box<dyn std::error::Error>> {
let repo = TestDir::new("session-harness-resolve-auto-built-in")?;
fs::create_dir_all(repo.path().join(".codex"))?;
let resolved = SessionHarnessInfo::resolve_requested_agent_type(
&crate::config::Config::default(),
"auto",
repo.path(),
);
assert_eq!(resolved, "codex");
Ok(())
}
#[test]
fn resolve_requested_agent_type_uses_configured_marker_for_auto(
) -> Result<(), Box<dyn std::error::Error>> {
let repo = TestDir::new("session-harness-resolve-auto-custom")?;
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 resolved = SessionHarnessInfo::resolve_requested_agent_type(&cfg, "auto", repo.path());
assert_eq!(resolved, "acme-runner");
Ok(())
}
#[test]
fn resolve_requested_agent_type_falls_back_to_claude_without_markers() {
let resolved = SessionHarnessInfo::resolve_requested_agent_type(
&crate::config::Config::default(),
"auto",
Path::new("."),
);
assert_eq!(resolved, "claude");
}
}