parent
19be0a6ca7
commit
0ef94cdbc1
|
|
@ -91,14 +91,17 @@ current hook mapping:
|
|||
- `UserPromptSubmit` → `working`
|
||||
- `PreToolUse` → `working`
|
||||
- `PermissionRequest` → `blocked`
|
||||
- `PostToolUse` → `working`
|
||||
- `PostToolUseFailure` → `working`
|
||||
- `SubagentStop` → `working`
|
||||
- `Stop` → `idle`
|
||||
- `SessionEnd` → `release`
|
||||
|
||||
notes:
|
||||
|
||||
- claude's current hook surface improves state reporting, but it is not a perfect permission lifecycle.
|
||||
- when a permission prompt is canceled, claude does not currently give herdr a clean hook event that always resolves the pane out of `blocked` immediately.
|
||||
- that is acceptable in herdr's model: process detection still owns liveness, and heuristics remain the fallback for unresolved edges.
|
||||
- claude code hooks also run inside subagents. herdr treats subagent `working` and `blocked` reports as real pane state.
|
||||
- subagent stop/release events are converted to `working` by the bundled hook script so a completed subagent does not make the parent claude pane look idle.
|
||||
- `PostToolUse` and `PostToolUseFailure` move the pane back to `working` after a permissioned tool call resolves.
|
||||
|
||||
uninstall:
|
||||
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ if hook_input_file:
|
|||
hook_input = {}
|
||||
|
||||
is_subagent = bool(hook_input.get("agent_id"))
|
||||
if is_subagent:
|
||||
raise SystemExit(0)
|
||||
if is_subagent and action in ("idle", "release"):
|
||||
action = "working"
|
||||
|
||||
request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}"
|
||||
if action == "release":
|
||||
|
|
|
|||
|
|
@ -139,6 +139,27 @@ pub(crate) fn install_claude() -> io::Result<ClaudeInstallPaths> {
|
|||
10,
|
||||
Some("*"),
|
||||
)?;
|
||||
ensure_command_hook(
|
||||
hooks,
|
||||
"PostToolUse",
|
||||
format!("bash {quoted_hook_path} working"),
|
||||
10,
|
||||
Some("*"),
|
||||
)?;
|
||||
ensure_command_hook(
|
||||
hooks,
|
||||
"PostToolUseFailure",
|
||||
format!("bash {quoted_hook_path} working"),
|
||||
10,
|
||||
Some("*"),
|
||||
)?;
|
||||
ensure_command_hook(
|
||||
hooks,
|
||||
"SubagentStop",
|
||||
format!("bash {quoted_hook_path} working"),
|
||||
10,
|
||||
Some("*"),
|
||||
)?;
|
||||
ensure_command_hook(
|
||||
hooks,
|
||||
"Stop",
|
||||
|
|
@ -304,6 +325,21 @@ pub(crate) fn uninstall_claude() -> io::Result<ClaudeUninstallResult> {
|
|||
"PermissionRequest",
|
||||
&format!("bash {quoted_hook_path} blocked"),
|
||||
)?;
|
||||
updated_settings |= remove_command_hook(
|
||||
hooks,
|
||||
"PostToolUse",
|
||||
&format!("bash {quoted_hook_path} working"),
|
||||
)?;
|
||||
updated_settings |= remove_command_hook(
|
||||
hooks,
|
||||
"PostToolUseFailure",
|
||||
&format!("bash {quoted_hook_path} working"),
|
||||
)?;
|
||||
updated_settings |= remove_command_hook(
|
||||
hooks,
|
||||
"SubagentStop",
|
||||
&format!("bash {quoted_hook_path} working"),
|
||||
)?;
|
||||
updated_settings |=
|
||||
remove_command_hook(hooks, "Stop", &format!("bash {quoted_hook_path} idle"))?;
|
||||
updated_settings |= remove_command_hook(
|
||||
|
|
@ -747,6 +783,20 @@ mod tests {
|
|||
.unwrap()
|
||||
.contains(" blocked")
|
||||
);
|
||||
assert!(settings["hooks"]["PostToolUse"][0]["hooks"][0]["command"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains(" working"));
|
||||
assert!(
|
||||
settings["hooks"]["PostToolUseFailure"][0]["hooks"][0]["command"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains(" working")
|
||||
);
|
||||
assert!(settings["hooks"]["SubagentStop"][0]["hooks"][0]["command"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains(" working"));
|
||||
assert!(settings["hooks"]["Stop"][0]["hooks"][0]["command"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
|
|
@ -790,6 +840,21 @@ mod tests {
|
|||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
settings["hooks"]["PostToolUse"].as_array().unwrap().len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
settings["hooks"]["PostToolUseFailure"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
settings["hooks"]["SubagentStop"].as_array().unwrap().len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(settings["hooks"]["Stop"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(settings["hooks"]["SessionEnd"].as_array().unwrap().len(), 1);
|
||||
|
||||
|
|
@ -810,7 +875,11 @@ mod tests {
|
|||
fs::write(
|
||||
claude_dir.join("settings.json"),
|
||||
format!(
|
||||
r#"{{"hooks":{{"UserPromptSubmit":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep","timeout":10}}]}}],"Stop":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}],"SessionEnd":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' release","timeout":10}}]}}]}}}}"#,
|
||||
r#"{{"hooks":{{"UserPromptSubmit":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}},{{"type":"command","command":"echo keep","timeout":10}}]}}],"PermissionRequest":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' blocked","timeout":10}}]}}],"PostToolUse":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}}]}}],"PostToolUseFailure":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}}]}}],"SubagentStop":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' working","timeout":10}}]}}],"Stop":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' idle","timeout":10}}]}}],"SessionEnd":[{{"matcher":"*","hooks":[{{"type":"command","command":"bash '{}' release","timeout":10}}]}}]}}}}"#,
|
||||
hook_path.display(),
|
||||
hook_path.display(),
|
||||
hook_path.display(),
|
||||
hook_path.display(),
|
||||
hook_path.display(),
|
||||
hook_path.display(),
|
||||
hook_path.display(),
|
||||
|
|
@ -838,6 +907,10 @@ mod tests {
|
|||
settings["hooks"]["UserPromptSubmit"][0]["hooks"][0]["command"],
|
||||
"echo keep"
|
||||
);
|
||||
assert!(settings["hooks"].get("PermissionRequest").is_none());
|
||||
assert!(settings["hooks"].get("PostToolUse").is_none());
|
||||
assert!(settings["hooks"].get("PostToolUseFailure").is_none());
|
||||
assert!(settings["hooks"].get("SubagentStop").is_none());
|
||||
assert!(settings["hooks"].get("Stop").is_none());
|
||||
assert!(settings["hooks"].get("SessionEnd").is_none());
|
||||
|
||||
|
|
|
|||
|
|
@ -349,13 +349,34 @@ fn run_claude_hook(action: &str, hook_input: &str) -> Option<serde_json::Value>
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn claude_hook_suppresses_subagent_reports() {
|
||||
let subagent_input = r#"{"hook_event_name":"PermissionRequest","agent_id":"agent-abc123","agent_type":"Explore"}"#;
|
||||
fn claude_hook_reports_subagent_working_and_blocked() {
|
||||
let subagent_input = r#"{"hook_event_name":"Notification","agent_id":"agent-abc123","agent_type":"Explore","notification_type":"permission_prompt"}"#;
|
||||
|
||||
assert!(run_claude_hook("working", subagent_input).is_none());
|
||||
assert!(run_claude_hook("blocked", subagent_input).is_none());
|
||||
assert!(run_claude_hook("idle", subagent_input).is_none());
|
||||
assert!(run_claude_hook("release", subagent_input).is_none());
|
||||
let working =
|
||||
run_claude_hook("working", subagent_input).expect("subagent working should report working");
|
||||
assert_eq!(working["method"], "pane.report_agent");
|
||||
assert_eq!(working["params"]["state"], "working");
|
||||
|
||||
let blocked =
|
||||
run_claude_hook("blocked", subagent_input).expect("subagent blocked should report blocked");
|
||||
assert_eq!(blocked["method"], "pane.report_agent");
|
||||
assert_eq!(blocked["params"]["state"], "blocked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_hook_converts_subagent_idle_and_release_to_working() {
|
||||
let subagent_input =
|
||||
r#"{"hook_event_name":"SubagentStop","agent_id":"agent-abc123","agent_type":"Explore"}"#;
|
||||
|
||||
let idle = run_claude_hook("idle", subagent_input)
|
||||
.expect("subagent idle should keep parent pane working");
|
||||
assert_eq!(idle["method"], "pane.report_agent");
|
||||
assert_eq!(idle["params"]["state"], "working");
|
||||
|
||||
let release = run_claude_hook("release", subagent_input)
|
||||
.expect("subagent release should keep parent pane working");
|
||||
assert_eq!(release["method"], "pane.report_agent");
|
||||
assert_eq!(release["params"]["state"], "working");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue