From 638605e4fc1c42e1117da4efadeebe9040801b5a Mon Sep 17 00:00:00 2001 From: Dan Morosanu Date: Tue, 14 Jul 2026 18:14:51 +0300 Subject: [PATCH] fix: detect Windows skill paths in telemetry --- src/coder_eval/criteria/skill_triggered.py | 18 +++++++++--------- tests/test_early_stop.py | 8 ++++++++ tests/test_skill_triggered.py | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/coder_eval/criteria/skill_triggered.py b/src/coder_eval/criteria/skill_triggered.py index 06b90d0e..3d1aaf65 100644 --- a/src/coder_eval/criteria/skill_triggered.py +++ b/src/coder_eval/criteria/skill_triggered.py @@ -34,8 +34,11 @@ _NO = "no" # Extracts the skill name between ``skills//`` path segments (the Codex -# file-read signal). The leading class keeps a bare ``skills//`` from matching. -_SKILL_PATH_RE = re.compile(r"skills/([A-Za-z0-9][A-Za-z0-9_-]*)/") +# file-read signal). Accept both POSIX and Windows separators because telemetry +# records the command exactly as the agent emitted it. One-or-more separators +# also handles JSON-escaped commands that retain doubled backslashes. The +# lookahead permits overlapping matches such as ``.../skills/skills//...``. +_SKILL_PATH_RE = re.compile(r"(?=skills[\\/]+([A-Za-z0-9][A-Za-z0-9_-]*)[\\/]+)") def _engaged_skill_names(cmd: CommandTelemetry) -> set[str]: @@ -46,9 +49,9 @@ def _engaged_skill_names(cmd: CommandTelemetry) -> set[str]: - the Claude ``Skill`` tool call's ``skill`` parameter, namespace-stripped via ``.split(":")[-1]`` (the SDK reports ``plugin:skill-name``); and - - every ``skills//`` path segment appearing in any string parameter - (the Codex / file-read signal — repo layout or the ``.agents/skills`` - sandbox symlink). + - every ``skills//`` or ``skills\\\\`` path segment appearing + in any string parameter (the Codex / file-read signal — repo layout or + the ``.agents/skills`` sandbox symlink). Returns the (possibly empty) set of engaged skill names for this command. """ @@ -77,10 +80,7 @@ def _engaged_skill(cmd: CommandTelemetry, skill_name: str) -> bool: (Bash ``parameters['command']``) or a file-path parameter. The trailing slash prevents prefix collisions (``uipath-agents`` vs ``uipath-agents-foo``). """ - if cmd.tool_name == "Skill" and cmd.parameters.get("skill", "").split(":")[-1] == skill_name: - return True - needle = f"skills/{skill_name}/" - return any(isinstance(v, str) and needle in v for v in cmd.parameters.values()) + return skill_name in _engaged_skill_names(cmd) @register_criterion diff --git a/tests/test_early_stop.py b/tests/test_early_stop.py index f4af3a85..33f6a694 100644 --- a/tests/test_early_stop.py +++ b/tests/test_early_stop.py @@ -242,6 +242,14 @@ def test_file_read_path_segment(self) -> None: got = _engaged_skill_names(_cmd("Read", {"file_path": "/repo/skills/date-teller/SKILL.md"})) assert got == {"date-teller"} + def test_windows_file_read_path_segment(self) -> None: + got = _engaged_skill_names(_cmd("Read", {"file_path": r"C:\repo\skills\date-teller\SKILL.md"})) + assert got == {"date-teller"} + + def test_json_escaped_windows_path_segment(self) -> None: + got = _engaged_skill_names(_cmd("Bash", {"command": r"type C:\\repo\\skills\\date-teller\\SKILL.md"})) + assert got == {"date-teller"} + def test_bash_command_path_segment(self) -> None: assert _engaged_skill_names(_cmd("Bash", {"command": "cat skills/foo/refs.md"})) == {"foo"} diff --git a/tests/test_skill_triggered.py b/tests/test_skill_triggered.py index 5dbe1ad7..ea1a8168 100644 --- a/tests/test_skill_triggered.py +++ b/tests/test_skill_triggered.py @@ -133,6 +133,22 @@ def test_codex_reads_skill_md_agents_symlink_tp(self) -> None: ) assert result.score == 1.0 and result.observed_label == "yes" + def test_codex_reads_skill_md_windows_path_tp(self) -> None: + result = _check( + expected_skill="uipath-agents", + skill_name="uipath-agents", + commands=[self._sed(r"C:\sandbox\.agents\skills\uipath-agents\SKILL.md")], + ) + assert result.score == 1.0 and result.observed_label == "yes" + + def test_codex_reads_skill_md_json_escaped_windows_command_tp(self) -> None: + result = _check( + expected_skill="uipath-agents", + skill_name="uipath-agents", + commands=[self._sed(r"C:\\sandbox\\.agents\\skills\\uipath-agents\\SKILL.md")], + ) + assert result.score == 1.0 and result.observed_label == "yes" + def test_codex_reads_wrong_skill_not_counted(self) -> None: # Read uipath-rpa's SKILL.md, but criterion filters on uipath-agents -> observed="no". result = _check(