From f7b26a911772dab16a49d982002d2bbe58e0956b Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Wed, 20 May 2026 20:42:14 +0300 Subject: fix: stop claude subagent completion reviving working refs #198 --- src/integration/assets/claude/herdr-agent-state.sh | 11 +++- src/integration/mod.rs | 65 +++++++++++++++------- 2 files changed, 55 insertions(+), 21 deletions(-) (limited to 'src/integration') diff --git a/src/integration/assets/claude/herdr-agent-state.sh b/src/integration/assets/claude/herdr-agent-state.sh index 035c3fe0..5042cad8 100644 --- a/src/integration/assets/claude/herdr-agent-state.sh +++ b/src/integration/assets/claude/herdr-agent-state.sh @@ -2,7 +2,7 @@ # installed by herdr # safe to edit. this hook only activates inside herdr-managed panes. # HERDR_INTEGRATION_ID=claude -# HERDR_INTEGRATION_VERSION=2 +# HERDR_INTEGRATION_VERSION=3 set -eu @@ -47,9 +47,16 @@ if hook_input_file: except Exception: hook_input = {} +hook_event_name = str(hook_input.get("hook_event_name") or "") is_subagent = bool(hook_input.get("agent_id")) +if hook_event_name == "SubagentStop": + # SubagentStop is a completion event. Older Herdr integrations mapped it + # to durable working, but Claude recap/away-summary can emit it after the + # main turn has already stopped. Never let it revive an idle pane. + raise SystemExit(0) if is_subagent and action in ("idle", "release"): - action = "working" + # Subagent completion must not make the parent pane look done early. + raise SystemExit(0) request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}" report_seq = time.time_ns() diff --git a/src/integration/mod.rs b/src/integration/mod.rs index ae3756b0..8b6b854c 100644 --- a/src/integration/mod.rs +++ b/src/integration/mod.rs @@ -16,7 +16,7 @@ const PI_INTEGRATION_VERSION: u32 = 1; const PI_CODING_AGENT_DIR_ENV_VAR: &str = "PI_CODING_AGENT_DIR"; const CLAUDE_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh"; const CLAUDE_HOOK_ASSET: &str = include_str!("assets/claude/herdr-agent-state.sh"); -const CLAUDE_INTEGRATION_VERSION: u32 = 2; +const CLAUDE_INTEGRATION_VERSION: u32 = 3; const CLAUDE_CONFIG_DIR_ENV_VAR: &str = "CLAUDE_CONFIG_DIR"; const CODEX_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh"; const CODEX_HOOK_ASSET: &str = include_str!("assets/codex/herdr-agent-state.sh"); @@ -591,6 +591,11 @@ pub(crate) fn install_claude() -> io::Result { "PostToolUseFailure", &format!("bash {quoted_hook_path} working"), )?; + remove_command_hook( + hooks, + "SubagentStop", + &format!("bash {quoted_hook_path} working"), + )?; ensure_command_hook( hooks, "UserPromptSubmit", @@ -612,13 +617,6 @@ pub(crate) fn install_claude() -> io::Result { 10, Some("*"), )?; - ensure_command_hook( - hooks, - "SubagentStop", - format!("bash {quoted_hook_path} working"), - 10, - Some("*"), - )?; ensure_command_hook( hooks, "Stop", @@ -1680,10 +1678,7 @@ mod tests { ); assert!(settings["hooks"].get("PostToolUse").is_none()); assert!(settings["hooks"].get("PostToolUseFailure").is_none()); - assert!(settings["hooks"]["SubagentStop"][0]["hooks"][0]["command"] - .as_str() - .unwrap() - .contains(" working")); + assert!(settings["hooks"].get("SubagentStop").is_none()); assert!(settings["hooks"]["Stop"][0]["hooks"][0]["command"] .as_str() .unwrap() @@ -1749,10 +1744,7 @@ mod tests { ); assert!(settings["hooks"].get("PostToolUse").is_none()); assert!(settings["hooks"].get("PostToolUseFailure").is_none()); - assert_eq!( - settings["hooks"]["SubagentStop"].as_array().unwrap().len(), - 1 - ); + assert!(settings["hooks"].get("SubagentStop").is_none()); assert_eq!(settings["hooks"]["Stop"].as_array().unwrap().len(), 1); assert_eq!(settings["hooks"]["SessionEnd"].as_array().unwrap().len(), 1); @@ -1761,7 +1753,7 @@ mod tests { } #[test] - fn install_claude_removes_deprecated_post_tool_hooks_and_preserves_user_hooks() { + fn install_claude_removes_deprecated_completion_hooks_and_preserves_user_hooks() { let _lock = integration_env_lock(); let base = unique_base(); let home = base.join("home"); @@ -1772,7 +1764,8 @@ mod tests { fs::write( claude_dir.join("settings.json"), format!( - r#"{{"hooks":{{"PostToolUse":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep-post","timeout":10}}]}}],"PostToolUseFailure":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep-failure","timeout":10}}]}}]}}}}"#, + r#"{{"hooks":{{"PostToolUse":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep-post","timeout":10}}]}}],"PostToolUseFailure":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep-failure","timeout":10}}]}}],"SubagentStop":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep-subagent","timeout":10}}]}}]}}}}"#, + hook_path.display(), hook_path.display(), hook_path.display(), ), @@ -1793,6 +1786,10 @@ mod tests { settings["hooks"]["PostToolUseFailure"][0]["hooks"][0]["command"], "echo keep-failure" ); + assert_eq!( + settings["hooks"]["SubagentStop"][0]["hooks"][0]["command"], + "echo keep-subagent" + ); assert_eq!( settings["hooks"]["UserPromptSubmit"] .as_array() @@ -1830,7 +1827,37 @@ mod tests { assert_eq!(claude.path, hook_path); assert_eq!(claude.installed_version, Some(1)); - assert_eq!(claude.expected_version, 2); + assert_eq!(claude.expected_version, 3); + assert_eq!(claude.state, IntegrationStatusKind::Outdated); + + std::env::remove_var("HOME"); + let _ = fs::remove_dir_all(base); + } + + #[test] + fn claude_v2_integration_status_is_outdated() { + let _lock = integration_env_lock(); + let base = unique_base(); + let home = base.join("home"); + let claude_hooks_dir = home.join(".claude").join("hooks"); + fs::create_dir_all(&claude_hooks_dir).unwrap(); + let hook_path = claude_hooks_dir.join(CLAUDE_HOOK_INSTALL_NAME); + fs::write( + &hook_path, + "#!/bin/sh\n# HERDR_INTEGRATION_ID=claude\n# HERDR_INTEGRATION_VERSION=2\n", + ) + .unwrap(); + std::env::set_var("HOME", &home); + + let statuses = installed_integration_statuses(); + let claude = statuses + .iter() + .find(|status| status.target == crate::api::schema::IntegrationTarget::Claude) + .unwrap(); + + assert_eq!(claude.path, hook_path); + assert_eq!(claude.installed_version, Some(2)); + assert_eq!(claude.expected_version, 3); assert_eq!(claude.state, IntegrationStatusKind::Outdated); std::env::remove_var("HOME"); -- cgit v1.2.3