diff --git a/distribution/agent-detection/codex.toml b/distribution/agent-detection/codex.toml index 884492c4..7840ca6f 100644 --- a/distribution/agent-detection/codex.toml +++ b/distribution/agent-detection/codex.toml @@ -1,7 +1,7 @@ id = "codex" -version = "2026.09.05.1" +version = "2026.09.12.1" min_engine_version = 3 -updated_at = "2026-09-05T00:00:00Z" +updated_at = "2026-09-12T00:00:00Z" [[rules]] id = "osc_title_blocked" @@ -69,6 +69,8 @@ id = "weak_blocker" state = "blocked" priority = 600 region = "whole_recent_without_current_prompt_marker" +# Sparkles can replace the space after ›. A later response marker makes that prompt stale. +not = [{ regex = ['(?m)^›[⠁⠂⠄⠈⠐⠠⡀⢀][^\n]*(?:\n(?:[^•■✗✓\n][^\n]*)?)*\z'] }] any = [ { contains = ["[y/n]"] }, { contains = ["yes (y)"] }, diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index e1073f3c..c5522952 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -1194,6 +1194,46 @@ fn codex_weak_blocker_ignores_wrapped_current_prompt_text() { ); } +#[test] +fn codex_sparkle_prompt_preserves_live_states() { + for marker in ["› ", "›⠁", "›⠂", "›⠄", "›⠈", "›⠐", "›⠠", "›⡀", "›⢀"] + { + let screen = format!("Do you want to proceed? [y/n]\n{marker}unsent draft\n"); + let result = osc_explain(Agent::Codex, &screen, "project | Ready", ""); + assert_eq!(result.state, AgentState::Idle, "{marker}"); + + let working = format!( + "Do you want to proceed? [y/n]\n• Working (4s • esc to interrupt)\n{marker}draft\n" + ); + let result = osc_explain(Agent::Codex, &working, "project", ""); + assert_eq!(result.state, AgentState::Working, "{marker}"); + + let approval = format!("{screen}Press enter to confirm or esc to cancel\n"); + let result = osc_explain(Agent::Codex, &approval, "project", ""); + assert_eq!(result.state, AgentState::Blocked, "{marker}"); + assert!(result.visible_blocker); + + for response_marker in ['•', '■', '✗', '✓'] { + let response = format!("{screen}{response_marker} Do you want to proceed? [y/n]\n"); + let result = osc_explain(Agent::Codex, &response, "project", ""); + assert_eq!( + result.state, + AgentState::Blocked, + "{marker} {response_marker}" + ); + } + } +} + +#[test] +fn codex_weak_blocker_does_not_ignore_arbitrary_prompt_suffixes() { + for line in ["›text", "›⠋draft", "›⠀draft", " ›⠁draft", "quoted ›⠁draft"] { + let screen = format!("Do you want to proceed? [y/n]\n{line}\n"); + let result = osc_explain(Agent::Codex, &screen, "project", ""); + assert_eq!(result.state, AgentState::Blocked, "{line}"); + } +} + #[test] fn codex_transcript_viewer_outranks_working_fallback() { let screen = "• Working (4s • esc to interrupt)\n\ diff --git a/src/detect/manifests/codex.toml b/src/detect/manifests/codex.toml index 884492c4..7840ca6f 100644 --- a/src/detect/manifests/codex.toml +++ b/src/detect/manifests/codex.toml @@ -1,7 +1,7 @@ id = "codex" -version = "2026.09.05.1" +version = "2026.09.12.1" min_engine_version = 3 -updated_at = "2026-09-05T00:00:00Z" +updated_at = "2026-09-12T00:00:00Z" [[rules]] id = "osc_title_blocked" @@ -69,6 +69,8 @@ id = "weak_blocker" state = "blocked" priority = 600 region = "whole_recent_without_current_prompt_marker" +# Sparkles can replace the space after ›. A later response marker makes that prompt stale. +not = [{ regex = ['(?m)^›[⠁⠂⠄⠈⠐⠠⡀⢀][^\n]*(?:\n(?:[^•■✗✓\n][^\n]*)?)*\z'] }] any = [ { contains = ["[y/n]"] }, { contains = ["yes (y)"] },