Skip to content

Fix skill-creator trigger detection reporting 0% recall - #1769

Open
ChiFungHillmanChan wants to merge 2 commits into
anthropics:mainfrom
ChiFungHillmanChan:fix/skill-creator-trigger-detection
Open

ChiFungHillmanChan wants to merge 2 commits into
anthropics:mainfrom
ChiFungHillmanChan:fix/skill-creator-trigger-detection

Conversation

@ChiFungHillmanChan

@ChiFungHillmanChan ChiFungHillmanChan commented Sep 14, 2026 •

Copy link
Copy Markdown

The issue

Fixes #1721. skill-creator's trigger evaluation reports precision=100% recall=0% for every skill, whatever its description says. run_loop then tunes the description against evidence that every positive query failed, and reports that as an optimisation. Nothing errors, so the output reads like a real measurement of a bad description.

The diagnosis is the reporter's. This implements the three paths they identified.

How it works now, and why that is wrong

run_single_query() in skills/skill-creator/scripts/run_eval.py decides whether the skill fired by watching the claude -p event stream. Three paths reach a verdict before the evidence can arrive.

Any other tool opening a block is treated as proof of failure:

if tool_name in ("Skill", "Read"):
    pending_tool_name = tool_name
    accumulated_json = ""
else:
    return False

A concrete query makes the model orient itself with Bash or Grep first, which is exactly the shape of query the skill-creator guide asks authors to write.

The first content_block_stop then decides the whole query, so a Read of an unrelated file before the Skill call settles it against that block's accumulated input:

elif se_type in ("content_block_stop", "message_stop"):
    if pending_tool_name:
        return clean_name in accumulated_json

And the non-streaming fallback returns inside its own for, so only the first tool_use in an assistant message is ever examined.

Tested before the fix

At the merge base, 34040c9, with scripts/test_run_eval.py from this branch:

$ python scripts/test_run_eval.py
FAIL: test_another_tool_before_the_skill_is_not_disproof
FAIL: test_skill_found_beyond_the_first_tool_of_a_message
FAIL: test_unrelated_read_before_the_skill_is_not_disproof
----------------------------------------------------------------------
Ran 4 tests in 0.003s

FAILED (failures=3)

In each of those three the skill was invoked and the harness scored it as not triggered. The fourth test is the negative case, a query where the skill never runs, and it passes here.

Tested after the fix

Same command, same file, at this PR's tip b7a840e:

$ python scripts/test_run_eval.py
----------------------------------------------------------------------
Ran 4 tests in 0.003s

OK

The negative case passes in both states, so the change does not buy recall with false positives.

What the fix does

Detection keeps scanning until the result event. Another tool opening a block clears the pending state rather than ending the query, content_block_stop resets per-block state instead of concluding from it, and the fallback walks the whole assistant message before moving on.

scripts/test_run_eval.py is new. It replays recorded streams through a real OS pipe, so select() and os.read() behave as they do against a live child and only the bytes are scripted. No API calls, and nothing beyond the standard library.

What it does not do

  • It does not address the --num-workers > 1 behaviour the reporter mentions as possibly separate. That is untouched here.
  • There is a fourth path with the same symptom that this PR deliberately leaves alone, so the change stays to the three causes in skill-creator: trigger detection reports 0% recall for every skill #1721: when claude -p has already exited at the next poll, its output is read into buffer and the loop breaks without parsing it. I will raise that separately.
  • The recorded streams are modelled on the shapes described in the issue. They are not captured from a live claude -p session.

run_single_query concluded that a skill had not triggered before the evidence
could arrive, so every eval reported recall=0% and run_loop then "improved"
descriptions against measurements that were all false.

Three paths ended the judgement early: another tool opening a block was treated
as proof of failure, the first content_block_stop decided the whole query, and
the non-streaming fallback returned inside its own loop after one tool_use.
Detection now keeps scanning until the result event, resetting per-block state
instead of concluding from it.

Adds tests that replay recorded streams through a real pipe, so no API calls.
@98zc5g5jyw-arch

Copy link
Copy Markdown

Reviewed this PR against current main (34040c9). The fix is correct and well-targeted.

Two-state verification (macOS, system python3, stdlib only):

  • merge-base main (34040c9) + test_run_eval.py → FAILED (failures=3) — reproduces exactly the false-negative paths this PR fixes (other-tool block bailing out, first content_block_stop deciding the query, non-streaming fallback returning after the first tool).
  • PR head (b7a840e) → Ran 4 tests in 0.004s OK — including the negative case (test_unrelated_read_before_the_skill_is_not_disproof), so fixing recall does not buy false positives.

Non-blocking observations:

  • The docstring's python -m unittest skills.skill_creator.scripts.test_run_eval module path doesn't match the actual tree (skill-creator, not a package); direct execution (python3 .../test_run_eval.py) works fine. Cosmetic only.
  • The test file (+~150 lines) ships inside the skill directory — presumably intentional (self-test after install), just flagging for awareness.
  • The 4th same-symptom path (residual buffer not parsed once claude -p has exited) is explicitly deferred — acceptable scope boundary.

Heads-up for maintainers: skill-creator/scripts/run_eval.py trigger detection is also being changed by #1298, #1755 and #1757, which touch overlapping lines. This PR is the smallest diff (2 files) and includes regression tests for the fixed paths. Whichever lands first will conflict with the others — picking one canonical fix would unblock the queue.

Overall: looks good to merge from a code standpoint.

The documented module path could never resolve: the directory is skill-creator,
with a hyphen, so skills.skill_creator.scripts is not an importable path. Name
the working directory and use the path that works from it.
@ChiFungHillmanChan

Copy link
Copy Markdown
Author

Thanks for re-running it independently, and you are right about the docstring.

skills.skill_creator.scripts.test_run_eval could never resolve: the directory is skill-creator, with a hyphen, so that is not an importable module path. Fixed in 73cf998. Both documented commands now run from skills/skill-creator:

python -m unittest scripts.test_run_eval   ->  Ran 4 tests in 0.002s  OK
python scripts/test_run_eval.py            ->  Ran 4 tests in 0.002s  OK

On the test file living inside the skill directory: that is intentional, and #1744 asks for test coverage here. Happy to move it if maintainers would rather it sat elsewhere.

On the collision, your read matches mine, and it is worth being plain about it. #1757 also closes #1721, two days before this one, and addresses the same three causes: the early return False when another tool opens a block, the first content_block_stop deciding the whole turn, and the non-streaming fallback returning on its first item. It does that as a TriggerDetector state machine with 14 tests; this PR is a smaller edit to the existing function with 4. Those are different trade-offs on the same fix, not two different fixes, and only one of them should land.

If maintainers prefer #1757, I am glad to close this one, just say so. The outcome I would rather avoid is four PRs on the same function continuing to sit while the harness still reports 0% recall.

@98zc5g5jyw-arch

Copy link
Copy Markdown

Hi @ChiFungHillmanChan — verified the follow-up commit (73cf998) on the current head, macOS system python3, run from skills/skill-creator:

  • python -m unittest scripts.test_run_eval → Ran 4 tests in 0.005s — OK
  • python scripts/test_run_eval.py → Ran 4 tests in 0.006s — OK

The docstring now documents both working invocations, and the non-importable skills.skill_creator... path is gone. The fix from b7a840e still holds at head — both commands verified end-to-end. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skill-creator: trigger detection reports 0% recall for every skill

2 participants