diff --git a/distribution/agent-detection/codex.toml b/distribution/agent-detection/codex.toml index dd275758..c9d71135 100644 --- a/distribution/agent-detection/codex.toml +++ b/distribution/agent-detection/codex.toml @@ -1,7 +1,7 @@ id = "codex" -version = "2026.09.23.1" +version = "2026.10.01.1" min_engine_version = 3 -updated_at = "2026-09-23T22:12:00Z" +updated_at = "2026-10-01T00:00:00Z" [[rules]] id = "osc_title_blocked" @@ -38,8 +38,20 @@ priority = 950 region = "top_non_empty_lines(20)" visible_blocker = true all = [ - { regex = ['\A> You are in [^\r\n]+(?:\r?\n|$)'] }, - { regex = ['(?s)Do\s+you\s+trust\s+the\s+contents\s+of\s+this\s+directory\?'] }, + { any = [ + { regex = ['\A> You are in [^\r\n]+(?:\r?\n|$)'] }, + { contains = ["Folder access"] }, + ] }, + { any = [ + { regex = ['(?s)Do\s+you\s+trust\s+the\s+contents\s+of\s+this\s+directory\?'] }, + { all = [ + { contains = ["Trust this folder?", "Codex can read, edit, and run files here"] }, + { any = [ + { contains = ["Trust and continue"] }, + { contains = ["enter continue"] }, + ] }, + ] }, + ] }, ] [[rules]] @@ -92,3 +104,15 @@ any = [{ contains = [" to interrupt)"] }, { contains = ["s)"] }] regex = ['(?m)^(?:[•◦][ \t]+)?[^\s›•◦■✗✓─][^\r\n]* \((?:[0-9]+[hm] )*[0-9]+s(?: • [^\r\n]+? to interrupt)?\)(?: · [^\r\n]*)?(?:\r?\n(?:[^•◦›■✗✓─\r\n][^\r\n]*|•[ \t]+(?:Queued\s+follow-up\s+inputs|Messages\s+to\s+be\s+submitted\s+after\s+next\s+tool\s+call(?:\s+\(press\s+[^\r\n]+?\s+to\s+interrupt\s+and\s+send\s+immediately\))?|Messages\s+to\s+be\s+submitted\s+at\s+end\s+of\s+turn)|›[⠁⠂⠄⠈⠐⠠⡀⢀][^\r\n]*)?)*\s*\z'] # A failed reconnect keeps its final elapsed timer but is no longer working. not = [{ line_regex = ['^(?:[•◦][ \t]+)?Reconnect failed — check the endpoint, then relaunch \([0-9hms ]+\)$'] }] + +[[rules]] +id = "osc_title_idle" +state = "idle" +priority = 100 +region = "osc_title" +visible_idle = true +regex = ['\S'] +not = [ + { regex = ['(?:^| )[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏](?: |$)'] }, + { contains = ["Action Required"] }, +] diff --git a/distribution/agent-detection/pi.toml b/distribution/agent-detection/pi.toml index 77b2d632..5ae51a85 100644 --- a/distribution/agent-detection/pi.toml +++ b/distribution/agent-detection/pi.toml @@ -1,7 +1,7 @@ id = "pi" -version = "2026.09.14.1" +version = "2026.10.01.1" min_engine_version = 1 -updated_at = "2026-09-14T00:00:00Z" +updated_at = "2026-10-01T00:00:00Z" aliases = ["herdr:pi"] [[rules]] @@ -10,7 +10,10 @@ state = "working" priority = 100 region = "whole_recent" visible_working = true -contains = ["Working..."] +any = [ + { contains = ["Working..."] }, + { line_regex = ['^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working$'] }, +] [[rules]] id = "working_border" @@ -18,4 +21,7 @@ state = "working" priority = 100 region = "bottom_non_empty_lines(12)" visible_working = true -line_regex = ['^── [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working ─+$'] +any = [ + { line_regex = ['^── [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working ─+$'] }, + { line_regex = ['^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working$'] }, +] diff --git a/docs/next/website/src/content/docs/agents.mdx b/docs/next/website/src/content/docs/agents.mdx index 3efe3fd2..b2799312 100644 --- a/docs/next/website/src/content/docs/agents.mdx +++ b/docs/next/website/src/content/docs/agents.mdx @@ -73,11 +73,9 @@ Some restricted Linux runtimes do not expose a terminal foreground process group ## Blocked state -Blocked detection is deliberately strict for screen-manifest agents. Herdr only marks `blocked` when the live bottom-buffer snapshot matches known visible approval, question, or permission UI. If no manifest rule matches for a known agent other than Codex, Herdr falls back to `idle` and labels that fallback as `default_known_agent_idle_fallback` in explain output. Codex falls back to `unknown` because its title and composer can look the same during an active turn and after a response. +Blocked detection is deliberately strict for screen-manifest agents. Herdr only marks `blocked` when the live bottom-buffer snapshot matches known visible approval, question, or permission UI. If no manifest rule matches for a known agent, Herdr falls back to `idle` and labels that fallback as `default_known_agent_idle_fallback` in explain output. -For those other agents, unusual new prompts may initially show as `idle` instead of `blocked` until Herdr learns that screen shape. The misclassification affects only the visible status and waits. It should not make Herdr send input or take destructive action. - -For Codex, a visible spinner or live activity timer can establish `working`, and a visible approval prompt can establish `blocked`. An ordinary title, composer, or missing spinner cannot establish that a turn ended. Codex may therefore stay `unknown` after a response, and waits for `idle` or completion may time out. Managed startup uses the initial composer only to determine when it can accept a prompt; that observation does not change turn status. +This means unusual new agent prompts may initially show as `idle` instead of `blocked` until Herdr learns that screen shape. The misclassification affects only the visible status and waits. It should not make Herdr send input or take destructive action. ## Detection manifests diff --git a/src/detect/manifest.rs b/src/detect/manifest.rs index e224adfe..03a279cc 100644 --- a/src/detect/manifest.rs +++ b/src/detect/manifest.rs @@ -540,14 +540,14 @@ fn fallback_explain( ) }) .unwrap_or((None, Vec::new(), None, None, None, false)); - let assume_idle = agent.is_some_and(|agent| agent != Agent::Codex); + let known_agent = agent.is_some(); let remote_update_status = include_update_status .then(|| agent.and_then(remote_update_status)) .flatten(); DetectionExplain { agent: agent.map(|agent| agent_label(agent).to_string()), - state: if assume_idle { + state: if known_agent { AgentState::Idle } else { AgentState::Unknown @@ -560,11 +560,7 @@ fn fallback_explain( visible_working: false, skip_state_update: false, skipped_update_reason: None, - fallback_reason: match agent { - Some(Agent::Codex) => Some("codex_state_ambiguous".to_string()), - Some(_) => Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK.to_string()), - None => None, - }, + fallback_reason: known_agent.then(|| DEFAULT_KNOWN_AGENT_IDLE_FALLBACK.to_string()), evaluated_rules, warning, manifest_version, diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index 1e6b1a85..636b2927 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -89,21 +89,15 @@ fn write_local_codex(content: &str) { } #[test] -fn codex_no_match_is_unknown_without_changing_other_agents() { +fn known_agent_no_match_defaults_to_idle_fallback() { with_manifest_dirs("no-match", || { write_local_codex(&local_manifest("working", "active-marker")); let explain = explain(Agent::Codex, "unmatched-marker"); - assert_eq!(explain.state, AgentState::Unknown); + assert_eq!(explain.state, AgentState::Idle); assert!(!explain.visible_idle); assert_eq!( explain.fallback_reason.as_deref(), - Some("codex_state_ambiguous") - ); - let other = fallback_explain(Some(Agent::Pi), None, false); - assert_eq!(other.state, AgentState::Idle); - assert_eq!( - other.fallback_reason.as_deref(), Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK) ); }); @@ -190,10 +184,10 @@ fn fallback_explain_preserves_active_manifest_version() { let explain = explain(Agent::Codex, "ordinary prompt text"); - assert_eq!(explain.state, AgentState::Unknown); + assert_eq!(explain.state, AgentState::Idle); assert_eq!( explain.fallback_reason.as_deref(), - Some("codex_state_ambiguous") + Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK) ); assert_eq!(explain.manifest_version.as_deref(), Some("9999.01.01.1")); assert!(matches!( @@ -273,10 +267,10 @@ fn detection_uses_cached_manifest_until_explicit_reload() { write_remote_codex_without_reload(&remote_manifest("9999.01.01.2", "working", "new-ready")); let unchanged = explain(Agent::Codex, "new-ready"); - assert_eq!(unchanged.state, AgentState::Unknown); + assert_eq!(unchanged.state, AgentState::Idle); assert_eq!( unchanged.fallback_reason.as_deref(), - Some("codex_state_ambiguous") + Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK) ); assert_eq!( unchanged.cached_remote_version.as_deref(), diff --git a/src/detect/manifests/codex.toml b/src/detect/manifests/codex.toml index dd275758..c9d71135 100644 --- a/src/detect/manifests/codex.toml +++ b/src/detect/manifests/codex.toml @@ -1,7 +1,7 @@ id = "codex" -version = "2026.09.23.1" +version = "2026.10.01.1" min_engine_version = 3 -updated_at = "2026-09-23T22:12:00Z" +updated_at = "2026-10-01T00:00:00Z" [[rules]] id = "osc_title_blocked" @@ -38,8 +38,20 @@ priority = 950 region = "top_non_empty_lines(20)" visible_blocker = true all = [ - { regex = ['\A> You are in [^\r\n]+(?:\r?\n|$)'] }, - { regex = ['(?s)Do\s+you\s+trust\s+the\s+contents\s+of\s+this\s+directory\?'] }, + { any = [ + { regex = ['\A> You are in [^\r\n]+(?:\r?\n|$)'] }, + { contains = ["Folder access"] }, + ] }, + { any = [ + { regex = ['(?s)Do\s+you\s+trust\s+the\s+contents\s+of\s+this\s+directory\?'] }, + { all = [ + { contains = ["Trust this folder?", "Codex can read, edit, and run files here"] }, + { any = [ + { contains = ["Trust and continue"] }, + { contains = ["enter continue"] }, + ] }, + ] }, + ] }, ] [[rules]] @@ -92,3 +104,15 @@ any = [{ contains = [" to interrupt)"] }, { contains = ["s)"] }] regex = ['(?m)^(?:[•◦][ \t]+)?[^\s›•◦■✗✓─][^\r\n]* \((?:[0-9]+[hm] )*[0-9]+s(?: • [^\r\n]+? to interrupt)?\)(?: · [^\r\n]*)?(?:\r?\n(?:[^•◦›■✗✓─\r\n][^\r\n]*|•[ \t]+(?:Queued\s+follow-up\s+inputs|Messages\s+to\s+be\s+submitted\s+after\s+next\s+tool\s+call(?:\s+\(press\s+[^\r\n]+?\s+to\s+interrupt\s+and\s+send\s+immediately\))?|Messages\s+to\s+be\s+submitted\s+at\s+end\s+of\s+turn)|›[⠁⠂⠄⠈⠐⠠⡀⢀][^\r\n]*)?)*\s*\z'] # A failed reconnect keeps its final elapsed timer but is no longer working. not = [{ line_regex = ['^(?:[•◦][ \t]+)?Reconnect failed — check the endpoint, then relaunch \([0-9hms ]+\)$'] }] + +[[rules]] +id = "osc_title_idle" +state = "idle" +priority = 100 +region = "osc_title" +visible_idle = true +regex = ['\S'] +not = [ + { regex = ['(?:^| )[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏](?: |$)'] }, + { contains = ["Action Required"] }, +] diff --git a/src/detect/manifests/pi.toml b/src/detect/manifests/pi.toml index 77b2d632..5ae51a85 100644 --- a/src/detect/manifests/pi.toml +++ b/src/detect/manifests/pi.toml @@ -1,7 +1,7 @@ id = "pi" -version = "2026.09.14.1" +version = "2026.10.01.1" min_engine_version = 1 -updated_at = "2026-09-14T00:00:00Z" +updated_at = "2026-10-01T00:00:00Z" aliases = ["herdr:pi"] [[rules]] @@ -10,7 +10,10 @@ state = "working" priority = 100 region = "whole_recent" visible_working = true -contains = ["Working..."] +any = [ + { contains = ["Working..."] }, + { line_regex = ['^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working$'] }, +] [[rules]] id = "working_border" @@ -18,4 +21,7 @@ state = "working" priority = 100 region = "bottom_non_empty_lines(12)" visible_working = true -line_regex = ['^── [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working ─+$'] +any = [ + { line_regex = ['^── [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working ─+$'] }, + { line_regex = ['^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Working$'] }, +]