diff --git a/distribution/agent-detection/codex.toml b/distribution/agent-detection/codex.toml index c9d711359..d35820c92 100644 --- a/distribution/agent-detection/codex.toml +++ b/distribution/agent-detection/codex.toml @@ -1,7 +1,7 @@ id = "codex" -version = "2026.10.01.1" +version = "2026.10.11.1" min_engine_version = 3 -updated_at = "2026-10-01T00:00:00Z" +updated_at = "2026-10-11T00:00:00Z" [[rules]] id = "osc_title_blocked" @@ -60,8 +60,19 @@ state = "blocked" priority = 950 region = "bottom_non_empty_lines(20)" visible_blocker = true -contains = ["Update available!", "Update now"] -regex = ['Skip\s+until\s+next\s+version', 'Press enter to continue\s*\z'] +# Keep the legacy dialog; the shared picker (0.159+) has a different heading +# and footer. Choices can scroll out of a short viewport, but both gates must +# remain visible. The passive update history notice has neither picker footer. +any = [ + { all = [ + { contains = ["Update available!", "Update now"] }, + { regex = ['Skip\s+until\s+next\s+version', 'Press enter to continue\s*\z'] }, + ] }, + { all = [ + { regex = ['(?m)^\s*Update\s+available\s+·'] }, + { regex = ['enter\s+continue\s+·\s+esc\s+skip\s*\z'] }, + ] }, +] [[rules]] id = "live_strong_blocker" diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index 636b2927f..65c6a6845 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -157,6 +157,51 @@ line_regex = ["^exact line$"] }); } +#[test] +fn paired_alternatives_require_complete_gates_at_bottom_buffer() { + with_manifest_dirs("paired-alternatives", || { + write_local_codex(&rules_manifest( + r#" +[[rules]] +id = "paired" +state = "blocked" +region = "bottom_non_empty_lines(3)" +visible_blocker = true +any = [ + { all = [ + { contains = ["alpha heading"] }, + { regex = ['alpha\s+control\s*\z'] }, + ] }, + { all = [ + { contains = ["beta heading"] }, + { regex = ['beta\s+control\s*\z'] }, + ] }, +] +"#, + )); + for text in [ + "alpha heading\nalpha control", + "beta heading\nbeta\ncontrol", + ] { + let result = explain(Agent::Codex, text); + assert_eq!(result.state, AgentState::Blocked); + assert!(result.visible_blocker); + } + for text in [ + "alpha heading\nbeta control", + "beta heading\nalpha control", + "beta control", + "beta heading", + "beta heading\nbeta control\nnewer content", + "beta heading\nfiller one\nfiller two\nbeta control", + ] { + let result = explain(Agent::Codex, text); + assert!(result.matched_rule.is_none(), "{text}"); + assert!(!result.visible_blocker, "{text}"); + } + }); +} + #[test] fn remote_manifest_loads_between_local_override_and_bundled() { with_manifest_dirs("remote-source", || { diff --git a/src/detect/manifests/codex.toml b/src/detect/manifests/codex.toml index c9d711359..d35820c92 100644 --- a/src/detect/manifests/codex.toml +++ b/src/detect/manifests/codex.toml @@ -1,7 +1,7 @@ id = "codex" -version = "2026.10.01.1" +version = "2026.10.11.1" min_engine_version = 3 -updated_at = "2026-10-01T00:00:00Z" +updated_at = "2026-10-11T00:00:00Z" [[rules]] id = "osc_title_blocked" @@ -60,8 +60,19 @@ state = "blocked" priority = 950 region = "bottom_non_empty_lines(20)" visible_blocker = true -contains = ["Update available!", "Update now"] -regex = ['Skip\s+until\s+next\s+version', 'Press enter to continue\s*\z'] +# Keep the legacy dialog; the shared picker (0.159+) has a different heading +# and footer. Choices can scroll out of a short viewport, but both gates must +# remain visible. The passive update history notice has neither picker footer. +any = [ + { all = [ + { contains = ["Update available!", "Update now"] }, + { regex = ['Skip\s+until\s+next\s+version', 'Press enter to continue\s*\z'] }, + ] }, + { all = [ + { regex = ['(?m)^\s*Update\s+available\s+·'] }, + { regex = ['enter\s+continue\s+·\s+esc\s+skip\s*\z'] }, + ] }, +] [[rules]] id = "live_strong_blocker"