From 176efb7623adb5a0f28fd5b9f648b92af7f3347a Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 10 Apr 2026 10:24:33 -0700 Subject: [PATCH] feat: add ecc2 harness compatibility env --- ecc2/src/session/manager.rs | 122 ++++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 19 deletions(-) diff --git a/ecc2/src/session/manager.rs b/ecc2/src/session/manager.rs index 6f293a27..9f1bc164 100644 --- a/ecc2/src/session/manager.rs +++ b/ecc2/src/session/manager.rs @@ -3018,6 +3018,7 @@ fn build_agent_command( if let Some(runner) = cfg.harness_runner(&SessionHarnessInfo::runner_key(agent_type)) { return build_configured_harness_command( runner, + agent_type, agent_program, task, session_id, @@ -3028,7 +3029,7 @@ fn build_agent_command( let task = normalize_task_for_harness(harness, task, profile); let mut command = Command::new(agent_program); - command.env("ECC_SESSION_ID", session_id); + apply_shared_harness_runtime_env(&mut command, agent_type, session_id, working_dir, profile); match harness { HarnessKind::Claude => { command @@ -3125,6 +3126,7 @@ fn build_agent_command( fn build_configured_harness_command( runner: &crate::config::HarnessRunnerConfig, + agent_type: &str, agent_program: &Path, task: &str, session_id: &str, @@ -3132,7 +3134,7 @@ fn build_configured_harness_command( profile: Option<&SessionAgentProfile>, ) -> Command { let mut command = Command::new(agent_program); - command.env("ECC_SESSION_ID", session_id); + apply_shared_harness_runtime_env(&mut command, agent_type, session_id, working_dir, profile); for (key, value) in &runner.env { if !value.trim().is_empty() { command.env(key, value); @@ -3211,6 +3213,52 @@ fn build_configured_harness_command( command } +fn apply_shared_harness_runtime_env( + command: &mut Command, + agent_type: &str, + session_id: &str, + working_dir: &Path, + profile: Option<&SessionAgentProfile>, +) { + let harness_label = SessionHarnessInfo::runner_key(agent_type); + command.env("ECC_SESSION_ID", session_id); + command.env("ECC_HARNESS", &harness_label); + command.env("ECC_WORKING_DIR", working_dir); + command.env("ECC_PROJECT_DIR", working_dir); + command.env("CLAUDE_SESSION_ID", session_id); + command.env("CLAUDE_PROJECT_DIR", working_dir); + command.env("CLAUDE_CODE_ENTRYPOINT", "cli"); + if let Some(model) = profile.and_then(|profile| profile.model.as_ref()) { + command.env("CLAUDE_MODEL", model); + } + if let Some(plugin_root) = resolve_ecc_plugin_root() { + command.env("ECC_PLUGIN_ROOT", &plugin_root); + command.env("CLAUDE_PLUGIN_ROOT", &plugin_root); + } +} + +fn resolve_ecc_plugin_root() -> Option { + let mut seeds = Vec::new(); + if let Ok(current_exe) = std::env::current_exe() { + seeds.push(current_exe); + } + seeds.push(PathBuf::from(env!("CARGO_MANIFEST_DIR"))); + + for seed in seeds { + for candidate in seed.ancestors() { + if is_ecc_plugin_root(candidate) { + return Some(candidate.to_path_buf()); + } + } + } + + None +} + +fn is_ecc_plugin_root(candidate: &Path) -> bool { + candidate.join("scripts/lib/utils.js").is_file() && candidate.join("hooks/hooks.json").is_file() +} + fn normalize_task_for_harness( harness: HarnessKind, task: &str, @@ -4246,6 +4294,24 @@ mod tests { "System instructions:\nReview thoroughly.\n\nECC execution profile:\n- Allowed tools: Read\n- Disallowed tools: Bash\n- Permission mode: plan\n- Max budget USD: 1.25\n- Token budget: 750\n\nTask:\nreview this change", ] ); + + let envs = command_env_map(&command); + assert_eq!(envs.get("ECC_SESSION_ID"), Some(&"sess-1234".to_string())); + assert_eq!( + envs.get("CLAUDE_SESSION_ID"), + Some(&"sess-1234".to_string()) + ); + assert_eq!( + envs.get("CLAUDE_PROJECT_DIR"), + Some(&"/tmp/repo".to_string()) + ); + assert_eq!(envs.get("CLAUDE_CODE_ENTRYPOINT"), Some(&"cli".to_string())); + assert_eq!(envs.get("ECC_HARNESS"), Some(&"codex".to_string())); + assert_eq!(envs.get("CLAUDE_MODEL"), Some(&"gpt-5.4".to_string())); + assert!( + envs.contains_key("CLAUDE_PLUGIN_ROOT"), + "shared compatibility env should expose the ECC plugin root" + ); } #[test] @@ -4441,24 +4507,20 @@ mod tests { "System instructions:\nUse repo context carefully.\n\nTask:\nfix callback regression", ] ); - let mut envs = command - .as_std() - .get_envs() - .map(|(key, value)| { - ( - key.to_string_lossy().to_string(), - value.map(|value| value.to_string_lossy().to_string()), - ) - }) - .collect::>(); - envs.sort(); + let envs = command_env_map(&command); + assert_eq!(envs.get("ECC_SESSION_ID"), Some(&"sess-cur1".to_string())); assert_eq!( - envs, - vec![ - ("ECC_HARNESS".to_string(), Some("cursor".to_string())), - ("ECC_SESSION_ID".to_string(), Some("sess-cur1".to_string())), - ] + envs.get("CLAUDE_SESSION_ID"), + Some(&"sess-cur1".to_string()) ); + assert_eq!( + envs.get("CLAUDE_PROJECT_DIR"), + Some(&"/tmp/repo".to_string()) + ); + assert_eq!(envs.get("CLAUDE_CODE_ENTRYPOINT"), Some(&"cli".to_string())); + assert_eq!(envs.get("ECC_HARNESS"), Some(&"cursor".to_string())); + assert_eq!(envs.get("CLAUDE_MODEL"), Some(&"gpt-5.4".to_string())); + assert_eq!(envs.get("ECC_PLUGIN_ROOT"), envs.get("CLAUDE_PLUGIN_ROOT")); } #[test] @@ -4806,7 +4868,7 @@ mod tests { let script_path = root.join("fake-claude.sh"); let log_path = root.join("fake-claude.log"); let script = format!( - "#!/usr/bin/env python3\nimport os\nimport pathlib\nimport signal\nimport sys\nimport time\n\nlog_path = pathlib.Path(r\"{}\")\nlog_path.write_text(os.getcwd() + \"\\n\", encoding=\"utf-8\")\nwith log_path.open(\"a\", encoding=\"utf-8\") as handle:\n handle.write(\" \".join(sys.argv[1:]) + \"\\n\")\n handle.write(\"ECC_SESSION_ID=\" + os.environ.get(\"ECC_SESSION_ID\", \"\") + \"\\n\")\n\ndef handle_term(signum, frame):\n raise SystemExit(0)\n\nsignal.signal(signal.SIGTERM, handle_term)\nwhile True:\n time.sleep(0.1)\n", + "#!/usr/bin/env python3\nimport os\nimport pathlib\nimport signal\nimport sys\nimport time\n\nlog_path = pathlib.Path(r\"{}\")\nlog_path.write_text(os.getcwd() + \"\\n\", encoding=\"utf-8\")\nwith log_path.open(\"a\", encoding=\"utf-8\") as handle:\n handle.write(\" \".join(sys.argv[1:]) + \"\\n\")\n handle.write(\"ECC_SESSION_ID=\" + os.environ.get(\"ECC_SESSION_ID\", \"\") + \"\\n\")\n handle.write(\"CLAUDE_SESSION_ID=\" + os.environ.get(\"CLAUDE_SESSION_ID\", \"\") + \"\\n\")\n handle.write(\"CLAUDE_PROJECT_DIR=\" + os.environ.get(\"CLAUDE_PROJECT_DIR\", \"\") + \"\\n\")\n handle.write(\"CLAUDE_CODE_ENTRYPOINT=\" + os.environ.get(\"CLAUDE_CODE_ENTRYPOINT\", \"\") + \"\\n\")\n handle.write(\"CLAUDE_PLUGIN_ROOT=\" + os.environ.get(\"CLAUDE_PLUGIN_ROOT\", \"\") + \"\\n\")\n handle.write(\"ECC_HARNESS=\" + os.environ.get(\"ECC_HARNESS\", \"\") + \"\\n\")\n\ndef handle_term(signum, frame):\n raise SystemExit(0)\n\nsignal.signal(signal.SIGTERM, handle_term)\nwhile True:\n time.sleep(0.1)\n", log_path.display() ); @@ -4834,6 +4896,21 @@ mod tests { anyhow::bail!("timed out waiting for {}", path.display()); } + fn command_env_map(command: &Command) -> BTreeMap { + command + .as_std() + .get_envs() + .filter_map(|(key, value)| { + value.map(|value| { + ( + key.to_string_lossy().to_string(), + value.to_string_lossy().to_string(), + ) + }) + }) + .collect() + } + #[tokio::test(flavor = "current_thread")] async fn create_session_spawns_process_and_marks_session_running() -> Result<()> { let tempdir = TestDir::new("manager-create-session")?; @@ -4869,6 +4946,13 @@ mod tests { assert!(log.contains("--print")); assert!(log.contains("implement lifecycle")); assert!(log.contains(&format!("ECC_SESSION_ID={session_id}"))); + assert!(log.contains(&format!("CLAUDE_SESSION_ID={session_id}"))); + assert!(log.contains(&format!( + "CLAUDE_PROJECT_DIR={}", + repo_root.to_string_lossy() + ))); + assert!(log.contains("CLAUDE_CODE_ENTRYPOINT=cli")); + assert!(log.contains("ECC_HARNESS=claude")); stop_session_with_options(&db, &session_id, false).await?; Ok(())