Repository navigation
feat(catalog): trigger-probe in real Claude Code; payload leak audit - #217
Conversation
A probe registry (dokimi.task_formats) names the format of a file, a task directory or a tree, and reports kind, item count, present/missing parts, lint problems and which dokimi backends consume it today. Probes: harbor and task-md (wrapping harnesses.tasks), terminal-bench v1, SWE-bench, Inspect AI samples and @task files (text scan, never imported), OpenAI evals, skill-creator evals.json and trigger sets, skill-reach QuerySet, promptfoo and dokimi questions. Unknown paths get a best-effort structure report; malformed files are lint problems. Detection only: nothing new runs. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Readers for the three trigger-set formats dokimi already writes (skill-reach QuerySet, skill-creator / agentskills.io eval queries, questions with expected_skill) into TriggerQuery, held to the writers by round-trip tests. catalog trigger-set --trigger-set relints an imported set against the catalog and hands it to the skill-catalog: arm; arms run points external sets at it. catalog formats names the skill/rule packages of other ecosystems (Cursor rules, Copilot instructions, Gemini extensions, Claude plugins) next to agentskills.io SKILL.md, with spec lint and the selecting mechanism. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Hits over the questions where the arm loaded any skill, strict on multi-load like recall; rendered in the arms report's Skill selection table. Replaces the ad-hoc calculation used for the intel#211 evidence table. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…load leak audit catalog trigger-probe runs every trigger-set query in a fresh claude -p: a new git repo with the catalog committed in .claude/skills, scratch HOME and config, the query verbatim, bundled skills on. Skill calls are scored by the same selection summary as the skill-catalog arm, one column per trial. payload_audit reports dokimi artifacts (arm names, label keys, question ids, grading words, task paths) in a captured request, argv or env, with operator content blanked and the stock harness's own text subtracted. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
CLI interface change detectedThis PR changes dokimi's user-facing CLI surface (commands and/or options): + added command `dokimi catalog trigger-probe`
+ option `--catalog`
+ option `--claude`
+ option `--concurrency`
+ option `--help/-h`
+ option `--limit`
+ option `--listing-budget-fraction`
+ option `--max-turns`
+ option `--model`
+ option `--out`
+ option `--sealed`
+ option `--target`
+ option `--timeout`
+ option `--trials`
+ option `--trigger-set`Updated on every push; the |
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #217 +/- ##
==========================================
- Coverage 92.74% 92.73% -0.02%
==========================================
Files 254 259 +5
Lines 32087 32711 +624
Branches 5344 5469 +125
==========================================
+ Hits 29759 30334 +575
- Misses 1560 1579 +19
- Partials 768 798 +30
🚀 New features to boost your workflow:
|
Claude Code streams one assistant event per content block, so two Skill calls in one turn were scored as two turns and the strict first-turn rule, multi_load_rate and mean_first_turn_loads never fired. Group by message.id as agent_runner groups by iteration, and skip subagent events. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
run_query checked timed_out first, so a query whose Skill call was already in the transcript, or whose result event was read before the CLI hung on a backgrounded Bash call, was dropped as n_failed. A Skill call seen is now the choice, and a result event decides the rest. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
--sealed set only the env seal, so the built-in plugin skills the docker-claude arms switch off via --settings stayed listed. Sealed argv now carries the same enabledPlugins, merged with --listing-budget-fraction into one --settings. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
A relative --claude passed the which() preflight but was spawned from the scratch work dir, and any OSError/RuntimeError in one run escaped pool.map and lost every paid result before --out was written. Resolve --claude to an absolute path and record a failed run as that trial's error. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
Ran the full suite and lint, plus live
Pushed:
Left for the author:
|
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 0d83412. The trigger-probe stream parsing, timeout handling and sealed mode hold up. The problems are a few gaps around the probe and a branch that needs main merged in. Before merge I'd fix the tools_before_choice count, the late --out write and the branch.
What I ran, on a Linux review host at 0d83412:
make ci-local: 32 passed, 1 failed, 6 skipped. The failure ispip-audit, which reports filelock and setuptools advisories in the lock files; this PR does not touch them.- 7,876 tests passed, 1 skipped; changed-line coverage 95% (1,184 lines).
- Reproductions for the code findings, with no model or network involved, and mutation runs on the new tests: 61 tests in
test_trigger_probe.pyandtest_skill_selection.pystay green under 15+ mutations of the probe and the command, and 4 mutants of the payload audit pass its tests (both listed below).
Findings not anchored inline:
- The branch conflicts with
mainand carries three commits that are already there.f4ce883(tasks inspect),f7abf7c(trigger-set readers,catalog formats) and19822ba(precision) reachedmainthrough #211.git merge-treeof the head withmainconflicts in 6 files:catalog.py,trigger_set.py,formats.py,growth_baseline.json,test_skill_formats.pyandtest_trigger_set_readers.py. Merged intomain, the PR adds only the trigger-probe:catalog_probe.py,trigger_probe.py,payload_audit.py, their tests and the docs. Suggestion: mergemainin (or rebase on it), so the diff and CI show only that. - The probe's flag handling is nearly untested. The CLI test uses a stub
claudethat ignores its arguments. Mutation check, each applied alone to the probe and the command, all pass the fulltest_trigger_probe.pyandtest_skill_selection.py: dropping--verbose,--no-session-persistenceor--max-turnsfrom the argv; dropping theTMPDIRoverride orinput=""; replacingtimeout=cfg.timeout_secwith1e9; flippingsymlinks=False; invertingstock=not args.sealed; dropping--model,--limit,--trials,--max-turns,--timeout,--listing-budget-fraction,--targetor--concurrencyin the CLI. Suggestion: one test that runs the command with every flag set and asserts the argv, env andProbeConfigthe runner receives. - The payload audit has survivors too. Each of these passes its tests: no
IGNORECASEon the marker pattern, no lookbehind on it, the longest-first sort ofallowedreversed, and tuples not walked. No test uses a capitalised marker, a marker preceded by letters, or a tuple.
@xaleryb's two points for the author, the backgrounded shell that survives the timeout kill and the "never reaches" wording in the docs, are still open.
Checked, no issues
- Stream parsing: blocks of one turn group by
message.id, subagent events are skipped, and a missing id falls back to a per-event key. - Errors: a Skill call counts whatever follows it,
error_max_turnsis an observation, and--timeout -5fails before any run is spent. - CLI surface: the
trigger-probeflags inevaluating-treatments.mdmatchcatalog_probe.py. The changelog fragment exists andCHANGELOG.mdis untouched.
vchamarthi
left a comment
There was a problem hiding this comment.
No blockers. 3 non-blocking comments.
No blockers found beyond the open points from the earlier review (merge main, tools_before_choice, --out written after all runs). Three further points below, all in the trigger-probe code.
CI at send time: 0 failing, 0 pending.
| raise SystemExit(f"{args.claude!r} is not on PATH") | ||
| try: | ||
| data = json.loads(Path(args.trigger_set).read_text(encoding="utf-8")) | ||
| _fmt, queries = ts.read_trigger_set(data, target=args.target) |
There was a problem hiding this comment.
Non-blocking: trigger-probe accepts any --target; reject one that is not a skill in --catalog, as catalog trigger-set already does.
dokimi/dokimi/commands/catalog_probe.py:31 passes target=args.target straight to read_trigger_set and never loads the catalog skills. cmd_catalog_trigger_set in catalog.py (_trigger_set_sources) refuses with "skill-creator target ... is not a skill in --catalog". A typo'd target here makes every positive query expect a skill that cannot load, so recall reads 0% after the full run is paid for.
| other_tools: List[str] = [] | ||
| for n, ev in enumerate(_events(stream)): | ||
| if ev.get("type") == "system" and ev.get("subtype") == "init": | ||
| listed = [str(s) for s in ev.get("skills") or []] |
There was a problem hiding this comment.
Non-blocking: trigger-probe should compare the skills Claude Code reports in its init event (listed) with the catalog and fail when a catalog skill is missing.
dokimi/dokimi/harnesses/trigger_probe.py:147 stores listed from the init event and :162 returns it, but catalog_probe.py reads only first_turn_loaded and cost_usd. A catalog whose frontmatter name differs from its directory, or uses a nested layout, is copied to .claude/skills with no complaint, so every positive query expects a skill that was never offered. Same silent-zero shape as the --target finding, different cause. Read at head 0d83412; not run against a real claude.
There was a problem hiding this comment.
Fixed in e27d2bd: catalog skill names are compared with the init event's listed; missing ones go to not_listed in the output and the command exits non-zero after writing the file.
| p.add_argument("--target", help="Target skill of a skill-creator set") | ||
| p.add_argument("--model", help="--model passed to claude (default: the harness default)") | ||
| p.add_argument("--trials", type=positive_int, default=1, help="Fresh runs per query (default: 1)") | ||
| p.add_argument("--max-turns", type=positive_int, default=3, dest="max_turns", |
There was a problem hiding this comment.
Question: Is --max-turns default 3 right for stock mode, where the module docstring says the model may look around before loading a skill?
trigger_probe.py treats error_max_turns with no Skill call as an observation (run_query, error stays None), and parse_stream records tools_before_choice. A model that reads a file or two first reaches the turn cap before it calls Skill, and that run counts as a miss for recall and as a correct 'no load' for negatives, so a low cap biases both rates in opposite directions. Consider reporting how many runs ended on error_max_turns in the output so the cap's effect is visible.
There was a problem hiding this comment.
Kept the default at 3 (a cost cap for single-shot trigger checks), but made the effect visible in e27d2bd/6901cde9: each run records result_subtype, and the output carries max_turns_hit, so a run cut by the cap is countable; raise --max-turns when that number is non-zero.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> # Conflicts: # dokimi/changelog.d/20261023_120000_napetrov_task-format-detect.md # dokimi/docs/coding-harnesses.md # dokimi/dokimi/commands/catalog.py # dokimi/dokimi/commands/tasks.py # dokimi/dokimi/external/trigger_set.py # dokimi/dokimi/skills/formats.py # dokimi/dokimi/task_formats/configs.py # dokimi/dokimi/task_formats/datasets.py # dokimi/dokimi/task_formats/detect.py # dokimi/dokimi/task_formats/dirs.py # dokimi/dokimi/task_formats/model.py # dokimi/dokimi/task_formats/source.py # dokimi/scripts/growth_baseline.json # dokimi/tests/test_skill_formats.py # dokimi/tests/test_task_format_detect.py # dokimi/tests/test_trigger_set_readers.py
- tools_before_choice stops at the first Skill turn. - --out directory and --target are checked before any paid run; records are written after each query, so an interrupt keeps what ran. - Fail when a catalog skill is missing from the init event's listing; report runs that ended on error_max_turns (max_turns_hit). - Kill processes left in the run's workspace (a backgrounded Bash job runs in its own session and escaped the group kill). - Docs: the operator's config is not loaded, but host paths stay readable. - Tests: every CLI flag reaches ProbeConfig, argv/env/timeout/stdin of a run, symlinked skills copied as files, payload-audit case, lookbehind, tuple and longest-first blanking. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…result_subtype Keep in-progress records in <out>.partial so a rerun does not clobber a finished result; refuse a non-writable --out and a malformed catalog as input errors (exit 2) before any run; name the field result_subtype as the rest of the codebase does; document max_turns_hit, not_listed and the partial file. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Review follow-up, head 7c7bb67:
|
antonwolfy
left a comment
There was a problem hiding this comment.
Both earlier threads are fixed and the new not_listed, max_turns_hit and .partial paths work. One thing is worth fixing before merge: the fix for the backgrounded-shell leak has no test that would notice if it were removed. Three nits inline.
What I ran, on Linux at 7c7bb67:
make ci-local: 33 passed, 0 failed, 6 skipped.- Reproductions for the CLI findings, with a stub
claudeand no model or network involved. - Mutation runs on
test_trigger_probe.pyandtest_payload_audit.py: 60 mutants of the probe, 27 pass the suite; 59 mutants of the payload audit, 8 pass.
Checked, no issues
- Stream parsing:
tools_before_choicestops at the firstSkillturn, and turns still group bymessage.id. - Input checks:
--out,--targetand--catalogare rejected before any run;.partialis replaced atomically and removed on success. kill_strays: matches the root exactly. With realsleepprocesses inproject-abcandproject-abcd, cleaningproject-abckills only the first.- Repo rules: changelog fragment present, docs page already in
nav:, no generated artifacts in the diff,catalog.pystays inside its growth baseline.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Only kill_strays itself was tested, so dropping its call from run_query or matching a sibling root's prefix left the suite green. Run a fake claude that leaves a job in its workspace and one in a same-prefix sibling root: the first must die, the second must survive. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
A query whose expected_skill is not in --catalog can never be loaded, so it scored as a miss after the paid run: a typo read as recall 0. The skill-catalog: arm refuses such a label; trigger-probe now does too, before any run, as it already does for --target. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
Re-checked at ea9d304: full suite, lint, types, security; each earlier point re-run with its original input (stub and live Claude Code).
Pushed:
Left for the author: nothing blocking. |
…#267) Settles two points from the review of #217 (catalog trigger-probe). A --trigger-set with no labelled query (a questions set without expected_skill, or an empty set) ran nothing, wrote "answers": [], printed "summary": {} and exited 0. _labelled_queries now raises InputError, which the CLI turns into exit 2, before any run and before --out or <out>.partial is written; the message names --target when it was given. When runs happened but no run's init event listed any skill (an older Claude Code, or every run failing before init), the not_listed guard was skipped silently. The command now prints a warning to stderr saying not_listed was not checked; the output JSON and the exit code are unchanged, and nothing is printed when no run happened. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…obe strict Main now has flags the registry does not know: catalog trigger-probe's --claude, --sealed, --listing-budget-fraction and --trials (intel#217), and novelty routing's --all-vs-all, --include-disabled and --queries-by-skill (intel#172). Each gets an allowlist row with its reason. --trials is a banned synonym of reps; renaming it would change a shipped command, so it waits for the F1b migration. The banned-synonym probe in test_cli_flags.py used --trials, which is now allowlisted, so the probe would pass for the wrong reason. It runs with that row removed instead. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
#211 has merged;
intel/mainis merged in, so the diff is only the trigger-probe.Summary
The
skill-catalog:arm over-triggers (recall 0.77, false-trigger 0.46 on 30 skill-reach queries). This PR adds a way to measure the real harness instead, plus an audit that checks what dokimi sends carries none of its own artifacts.Why the emulated arm over-triggers. I captured both request bodies through a logging proxy, then ablated them one at a time (haiku-4.5, 30 queries x 2, single turn):
The main driver is that the arm offers only skill tools and no system prompt. The enum and the wording are within noise.
Evidence
catalog trigger-probe, stock, CC 2.1.304, haiku-4.5, 30 queries x 3 trials: recall 0.23 / 0.27 / 0.27, false-trigger 0.00, misroute 0.09, cost $2.30. This is far below the emulated arm and in line with the full-replay ablation.--listing-budget-fraction 0.03(skill-reach's setting), 2 trials: recall 0.45 / 0.36, false-trigger 0.00, cost $2.59. skill-reach on the same set gives recall 0.41–0.50 and false-trigger 0.19–0.31. Recall matches. False-trigger is lower, but there are only 8 negatives, so one false trigger moves the rate by 0.125.dokimi-run-through the working directory that CC shows the model. Fixed with a neutralproject-prefix and a neutral "Initial commit".tests/test_trigger_probe.pyandtests/test_payload_audit.py, FakeRunner, no network; every CLI flag asserted intoProbeConfig.max_turns_hit,not_listed(exit non-zero when a catalog skill was not offered) and per-query<out>.partial.make ci-local: all pass except pip-audit. pip-audit fails on filelock/setuptools CVEs in the lock that this PR does not change.Merge Danger
Door: two-way
The command and modules are new. The only edit to existing code is two lines in
catalog.py.Blast Radius: none
Existing arms and outputs are unchanged.
🤖 Generated with Claude Code