mirror of
https://github.com/herdrdev/herdr.git
synced 2026-11-06 00:06:18 +00:00
fix: detect agy dialogs and mid-turn work, treat background work as idle for agy and grok (#4959)
refs #3871 refs #3530 refs #4735 refs #3419
This commit is contained in:
@@ -1,13 +1,60 @@
|
||||
id = "agy"
|
||||
version = "2026.06.24.1"
|
||||
version = "2026.10.05.1"
|
||||
min_engine_version = 1
|
||||
updated_at = "2026-06-24T00:00:00Z"
|
||||
updated_at = "2026-10-05T00:00:00Z"
|
||||
aliases = ["antigravity", "antigravity-cli"]
|
||||
|
||||
# Evidence: Antigravity CLI 1.2.17 live pane reads, plus reporter captures
|
||||
# from 1.1.28 through 1.2.16.
|
||||
#
|
||||
# Dialogs replace the prompt box and end with a key-hint row directly above
|
||||
# the "esc to cancel" footer:
|
||||
# permission: "↑/↓ Navigate · tab Amend · ctrl+g edit/expand command"
|
||||
# "↑/↓ Navigate · tab Amend · f full diff"
|
||||
# question: "↑/↓ Navigate · enter Select · esc Skip"
|
||||
# trust: "↑/↓ Navigate · enter Confirm"
|
||||
# slash menu: "↑/↓ Navigate · enter Select · tab Complete" (idle)
|
||||
# Some captures hold only the two footer rows, so dialog rules must not
|
||||
# require the question text.
|
||||
#
|
||||
# Working turns draw a braille spinner followed by either an -ing word or a
|
||||
# thought summary above the prompt box, and the footer reads "esc to cancel"
|
||||
# directly under the prompt box's bottom border. The idle footer reads
|
||||
# "? for shortcuts".
|
||||
#
|
||||
# Background tasks and subagents show "· N task(s)" in the footer and a task
|
||||
# panel under the prompt box. Once the agent is back at its prompt they do
|
||||
# not count as working.
|
||||
|
||||
[[rules]]
|
||||
id = "permission_prompt"
|
||||
state = "blocked"
|
||||
priority = 300
|
||||
region = "bottom_non_empty_lines(2)"
|
||||
visible_blocker = true
|
||||
contains = ["navigate", "tab amend"]
|
||||
|
||||
[[rules]]
|
||||
id = "question_prompt"
|
||||
state = "blocked"
|
||||
priority = 290
|
||||
region = "bottom_non_empty_lines(2)"
|
||||
visible_blocker = true
|
||||
contains = ["navigate", "esc skip"]
|
||||
|
||||
[[rules]]
|
||||
id = "trust_prompt"
|
||||
state = "blocked"
|
||||
priority = 280
|
||||
region = "bottom_non_empty_lines(8)"
|
||||
visible_blocker = true
|
||||
contains = ["do you trust the contents of this project?", "yes, i trust this folder", "enter confirm"]
|
||||
|
||||
# Pre-1.1.28 permission dialog.
|
||||
[[rules]]
|
||||
id = "legacy_permission_prompt"
|
||||
state = "blocked"
|
||||
priority = 270
|
||||
region = "whole_recent"
|
||||
visible_blocker = true
|
||||
contains = ["requesting permission for:"]
|
||||
@@ -16,18 +63,18 @@ any = [
|
||||
{ contains = ["tab amend", "edit command"] },
|
||||
]
|
||||
|
||||
[[rules]]
|
||||
id = "esc_cancel_footer_working"
|
||||
state = "working"
|
||||
priority = 110
|
||||
region = "bottom_non_empty_lines(2)"
|
||||
visible_working = true
|
||||
regex = ['(?m)^\s*─{3,}\s*esc to cancel\b']
|
||||
|
||||
[[rules]]
|
||||
id = "spinner_working"
|
||||
state = "working"
|
||||
priority = 100
|
||||
region = "whole_recent"
|
||||
region = "bottom_non_empty_lines(12)"
|
||||
visible_working = true
|
||||
line_regex = ['^\s*[\u2800-\u28FF]+\s+\p{Alphabetic}+\w*ing\b']
|
||||
|
||||
[[rules]]
|
||||
id = "background_tasks_working"
|
||||
state = "working"
|
||||
priority = 90
|
||||
region = "bottom_non_empty_lines(5)"
|
||||
visible_working = true
|
||||
line_regex = ['(?i)·\s*[1-9][0-9]*\s+task']
|
||||
line_regex = ['^\s*[\u2801-\u28FF]+\s+\S']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "grok"
|
||||
version = "2026.09.18.1"
|
||||
min_engine_version = 2
|
||||
updated_at = "2026-09-18T00:00:00Z"
|
||||
version = "2026.10.05.1"
|
||||
min_engine_version = 3
|
||||
updated_at = "2026-10-05T00:00:00Z"
|
||||
aliases = ["grok-build"]
|
||||
|
||||
# Evidence: Grok Build 0.2.101 source and 1.0.34 live pane reads.
|
||||
@@ -74,15 +74,8 @@ any = [
|
||||
{ contains = ["←/→:scope"] },
|
||||
]
|
||||
|
||||
# Grok 1.0.34 moves background counts above the composer and clears OSC
|
||||
# progress between turns even while these commands are still running.
|
||||
[[rules]]
|
||||
id = "background_status_working"
|
||||
state = "working"
|
||||
priority = 1165
|
||||
region = "bottom_non_empty_lines(12)"
|
||||
visible_working = true
|
||||
line_regex = ['^\s*[○◎◉]\s+[1-9][0-9]*\s+(?:commands?|monitors?|loops?|subagents?)(?:\s+·\s+[1-9][0-9]*\s+(?:commands?|monitors?|loops?|subagents?))*\s+still running(?:\s+·\s+send a message to interrupt)?\s*$']
|
||||
# Background commands, monitors, and subagents left running after Grok returns
|
||||
# to its prompt do not count as working, matching Claude and agy (#1217, #4735).
|
||||
|
||||
[[rules]]
|
||||
id = "osc_progress_working"
|
||||
|
||||
@@ -53,16 +53,10 @@ MAX_MATCHERS_PER_GATE = 32
|
||||
MAX_TOTAL_MATCHERS = 1024
|
||||
MAX_MATCHER_CHARS = 512
|
||||
|
||||
# Keep engine-2 clients on the OSC-capable manifest until an engine-3 release
|
||||
# can consume top_non_empty_lines. Remove this entry when the distribution
|
||||
# publishes the bundled Grok manifest.
|
||||
STAGED_PUBLISHED_MANIFESTS = {
|
||||
"grok": (
|
||||
"2026.09.18.2",
|
||||
"2026.09.18.1",
|
||||
"0f31b111144900b02f303577d27587f72d58d8c505185a682bd7887f822316ee",
|
||||
),
|
||||
}
|
||||
# Keep older-engine clients on a published manifest until a release with the
|
||||
# bundled manifest's engine ships. Maps agent id to (bundled version, published
|
||||
# version, published sha256). Remove an entry once the bundled manifest ships.
|
||||
STAGED_PUBLISHED_MANIFESTS: dict[str, tuple[str, str, str]] = {}
|
||||
|
||||
UNPUBLISHED_BUNDLED_MANIFESTS: dict[str, tuple[str, str]] = {}
|
||||
|
||||
|
||||
@@ -29,18 +29,27 @@ path = "{path}"
|
||||
'''
|
||||
|
||||
|
||||
def staged_grok_dirs(root: Path) -> tuple[Path, Path]:
|
||||
STAGED_BUNDLED_MANIFEST = manifest("codex", "2026.06.10.2").replace(
|
||||
"min_engine_version = 1", "min_engine_version = 2"
|
||||
)
|
||||
STAGED_PUBLISHED_MANIFEST = manifest("codex", "2026.06.10.1")
|
||||
STAGED_TEST_EXCEPTION = {
|
||||
"codex": (
|
||||
"2026.06.10.2",
|
||||
"2026.06.10.1",
|
||||
hashlib.sha256(STAGED_PUBLISHED_MANIFEST.encode()).hexdigest(),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def staged_manifest_dirs(root: Path) -> tuple[Path, Path]:
|
||||
bundled = root / "bundled"
|
||||
published = root / "published"
|
||||
bundled.mkdir()
|
||||
published.mkdir()
|
||||
(bundled / "grok.toml").write_bytes(
|
||||
(check.DEFAULT_BUNDLED_DIR / "grok.toml").read_bytes()
|
||||
)
|
||||
(published / "grok.toml").write_bytes(
|
||||
(check.DEFAULT_PUBLISHED_DIR / "grok.toml").read_bytes()
|
||||
)
|
||||
(published / "index.toml").write_text(catalog("grok", "grok.toml"))
|
||||
(bundled / "codex.toml").write_text(STAGED_BUNDLED_MANIFEST, encoding="utf-8", newline="\n")
|
||||
(published / "codex.toml").write_text(STAGED_PUBLISHED_MANIFEST, encoding="utf-8", newline="\n")
|
||||
(published / "index.toml").write_text(catalog())
|
||||
return bundled, published
|
||||
|
||||
|
||||
@@ -94,22 +103,24 @@ class AgentDetectionManifestCheckTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(check.CheckError, "lower than bundled"):
|
||||
check.validate_catalog(website, bundled_manifests, engine_version=1)
|
||||
|
||||
@patch.dict(check.STAGED_PUBLISHED_MANIFESTS, STAGED_TEST_EXCEPTION, clear=True)
|
||||
def test_allows_explicitly_staged_published_manifest(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
bundled, website = staged_grok_dirs(Path(tmp))
|
||||
bundled, website = staged_manifest_dirs(Path(tmp))
|
||||
|
||||
bundled_manifests = check.load_manifest_dir(bundled, engine_version=3)
|
||||
check.validate_catalog(website, bundled_manifests, engine_version=3)
|
||||
bundled_manifests = check.load_manifest_dir(bundled, engine_version=2)
|
||||
check.validate_catalog(website, bundled_manifests, engine_version=2)
|
||||
|
||||
@patch.dict(check.STAGED_PUBLISHED_MANIFESTS, STAGED_TEST_EXCEPTION, clear=True)
|
||||
def test_rejects_mutated_staged_published_manifest(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
bundled, website = staged_grok_dirs(Path(tmp))
|
||||
with (website / "grok.toml").open("a") as manifest_file:
|
||||
bundled, website = staged_manifest_dirs(Path(tmp))
|
||||
with (website / "codex.toml").open("a") as manifest_file:
|
||||
manifest_file.write("\n# unexpected mutation\n")
|
||||
|
||||
bundled_manifests = check.load_manifest_dir(bundled, engine_version=3)
|
||||
bundled_manifests = check.load_manifest_dir(bundled, engine_version=2)
|
||||
with self.assertRaisesRegex(check.CheckError, "lower than bundled"):
|
||||
check.validate_catalog(website, bundled_manifests, engine_version=3)
|
||||
check.validate_catalog(website, bundled_manifests, engine_version=2)
|
||||
|
||||
def test_rejects_unlisted_published_manifest_lag_for_new_engine(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
||||
@@ -1,13 +1,60 @@
|
||||
id = "agy"
|
||||
version = "2026.06.24.1"
|
||||
version = "2026.10.05.1"
|
||||
min_engine_version = 1
|
||||
updated_at = "2026-06-24T00:00:00Z"
|
||||
updated_at = "2026-10-05T00:00:00Z"
|
||||
aliases = ["antigravity", "antigravity-cli"]
|
||||
|
||||
# Evidence: Antigravity CLI 1.2.17 live pane reads, plus reporter captures
|
||||
# from 1.1.28 through 1.2.16.
|
||||
#
|
||||
# Dialogs replace the prompt box and end with a key-hint row directly above
|
||||
# the "esc to cancel" footer:
|
||||
# permission: "↑/↓ Navigate · tab Amend · ctrl+g edit/expand command"
|
||||
# "↑/↓ Navigate · tab Amend · f full diff"
|
||||
# question: "↑/↓ Navigate · enter Select · esc Skip"
|
||||
# trust: "↑/↓ Navigate · enter Confirm"
|
||||
# slash menu: "↑/↓ Navigate · enter Select · tab Complete" (idle)
|
||||
# Some captures hold only the two footer rows, so dialog rules must not
|
||||
# require the question text.
|
||||
#
|
||||
# Working turns draw a braille spinner followed by either an -ing word or a
|
||||
# thought summary above the prompt box, and the footer reads "esc to cancel"
|
||||
# directly under the prompt box's bottom border. The idle footer reads
|
||||
# "? for shortcuts".
|
||||
#
|
||||
# Background tasks and subagents show "· N task(s)" in the footer and a task
|
||||
# panel under the prompt box. Once the agent is back at its prompt they do
|
||||
# not count as working.
|
||||
|
||||
[[rules]]
|
||||
id = "permission_prompt"
|
||||
state = "blocked"
|
||||
priority = 300
|
||||
region = "bottom_non_empty_lines(2)"
|
||||
visible_blocker = true
|
||||
contains = ["navigate", "tab amend"]
|
||||
|
||||
[[rules]]
|
||||
id = "question_prompt"
|
||||
state = "blocked"
|
||||
priority = 290
|
||||
region = "bottom_non_empty_lines(2)"
|
||||
visible_blocker = true
|
||||
contains = ["navigate", "esc skip"]
|
||||
|
||||
[[rules]]
|
||||
id = "trust_prompt"
|
||||
state = "blocked"
|
||||
priority = 280
|
||||
region = "bottom_non_empty_lines(8)"
|
||||
visible_blocker = true
|
||||
contains = ["do you trust the contents of this project?", "yes, i trust this folder", "enter confirm"]
|
||||
|
||||
# Pre-1.1.28 permission dialog.
|
||||
[[rules]]
|
||||
id = "legacy_permission_prompt"
|
||||
state = "blocked"
|
||||
priority = 270
|
||||
region = "whole_recent"
|
||||
visible_blocker = true
|
||||
contains = ["requesting permission for:"]
|
||||
@@ -16,18 +63,18 @@ any = [
|
||||
{ contains = ["tab amend", "edit command"] },
|
||||
]
|
||||
|
||||
[[rules]]
|
||||
id = "esc_cancel_footer_working"
|
||||
state = "working"
|
||||
priority = 110
|
||||
region = "bottom_non_empty_lines(2)"
|
||||
visible_working = true
|
||||
regex = ['(?m)^\s*─{3,}\s*esc to cancel\b']
|
||||
|
||||
[[rules]]
|
||||
id = "spinner_working"
|
||||
state = "working"
|
||||
priority = 100
|
||||
region = "whole_recent"
|
||||
region = "bottom_non_empty_lines(12)"
|
||||
visible_working = true
|
||||
line_regex = ['^\s*[\u2800-\u28FF]+\s+\p{Alphabetic}+\w*ing\b']
|
||||
|
||||
[[rules]]
|
||||
id = "background_tasks_working"
|
||||
state = "working"
|
||||
priority = 90
|
||||
region = "bottom_non_empty_lines(5)"
|
||||
visible_working = true
|
||||
line_regex = ['(?i)·\s*[1-9][0-9]*\s+task']
|
||||
line_regex = ['^\s*[\u2801-\u28FF]+\s+\S']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "grok"
|
||||
version = "2026.09.18.2"
|
||||
version = "2026.10.05.1"
|
||||
min_engine_version = 3
|
||||
updated_at = "2026-09-18T00:00:00Z"
|
||||
updated_at = "2026-10-05T00:00:00Z"
|
||||
aliases = ["grok-build"]
|
||||
|
||||
# Evidence: Grok Build 0.2.101 source and 1.0.34 live pane reads.
|
||||
@@ -74,26 +74,8 @@ any = [
|
||||
{ contains = ["←/→:scope"] },
|
||||
]
|
||||
|
||||
# Grok clears its OSC busy signals while background work runs. The first
|
||||
# non-empty row is pinned application chrome, where this animated chip shows
|
||||
# the number of running background tasks and disappears when the count is zero.
|
||||
[[rules]]
|
||||
id = "background_work_chip_working"
|
||||
state = "working"
|
||||
priority = 1170
|
||||
region = "top_non_empty_lines(1)"
|
||||
visible_working = true
|
||||
line_regex = ['[⋅:⸬⁙.·]\s+[1-9][0-9]*\s+│']
|
||||
|
||||
# Grok 1.0.34 moves background counts above the composer and clears OSC
|
||||
# progress between turns even while these commands are still running.
|
||||
[[rules]]
|
||||
id = "background_status_working"
|
||||
state = "working"
|
||||
priority = 1165
|
||||
region = "bottom_non_empty_lines(12)"
|
||||
visible_working = true
|
||||
line_regex = ['^\s*[○◎◉]\s+[1-9][0-9]*\s+(?:commands?|monitors?|loops?|subagents?)(?:\s+·\s+[1-9][0-9]*\s+(?:commands?|monitors?|loops?|subagents?))*\s+still running(?:\s+·\s+send a message to interrupt)?\s*$']
|
||||
# Background commands, monitors, and subagents left running after Grok returns
|
||||
# to its prompt do not count as working, matching Claude and agy (#1217, #4735).
|
||||
|
||||
[[rules]]
|
||||
id = "osc_progress_working"
|
||||
|
||||
Reference in New Issue
Block a user