You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(harnesses): skill discovery arm (skill installed, no treatment notice) - #110
dokimi's skill arm can only answer "is the content useful once the skill is loaded": its prompt opens with TREATMENT_NOTICE, which tells the agent to load the skill. Skill authors also ask whether the agent finds the skill without being told. This PR adds a discovery arm for that question. It installs the same plugin and leaves the notice out. The existing arm and its default are unchanged.
Arm
Skill installed
Notice
Pass rate measures
docker-claude
no
no
baseline
docker-claude-skill:<p>
yes
yes
the skill's content, given that it is loaded
docker-claude-skill-discover:<p> (new)
yes
no
content × whether the agent finds the skill from its description
registry._create
docker-claude[-host]-skill:<p> -> DockerSolverHarness(skill_notice=True)
+ docker-claude[-host]-skill-discover:<p> -> DockerSolverHarness(skill_notice=False)
DockerSolverHarness._claude_args
- prompt = treatment_notice(t) + "Solve this coding task..."+ prompt = (treatment_notice(t) if self.skill_notice else "") + "Solve this coding task..."
_record_treatment # both skill arms, container and host
metrics.treatment # unchanged
+ metrics.skill_notice true|false+ metrics.skill_listing {description_chars, max_desc_chars: 1536, budget_chars: 8000, truncated}+ run.log WARNING if discovery arm and truncated
summarize_task_results
+ summary.discovery_gaps[<discover>] = {with_skill_harness, with_skill_pass_rate,+ discover_pass_rate, gap = with − discover (withheld if arms lost different cells),+ with_skill_invocation_rate, discover_invocation_rate}
task_runs_report
+ "## Skill discovery" table (only when a pair ran)
Same launch. Same plugin dir, --tools, --settings and env. A test compares the two arms' claude argv with the prompt taken out, and they match.
Listing visibility. The solver did not pin skillListingBudgetFraction, and this PR still doesn't. Claude Code 2.1.296 cuts each listed description at skillListingMaxDescChars (1536). It also shortens the whole listing to fit 0.01 × context × 4 characters, which is 8000 on a 200k window. These arms list one plugin skill plus doctor (and design on the host route), so the budget can't be reached. Only the per-skill cut can apply, and metrics.skill_listing.truncated records it. A missing plugin was already a solver_error in the init event, and that check also covers the new arm. The hidden-description problem seen in the external tools came from 33-skill catalogs, which these arms don't run. Pinning the fraction would add a second difference between the arms, so I left it out.
Skill calls:n_skill_invocations and the treatment-loaded check come from stream_stats, the same as for the skill arm. The headline already shows each harness as its own row with "Skill invoked".
Scope:treatments/specs.py (the Q&A --arms grammar) and eval/arm_summary.py (Q&A rollups) don't see docker-* harness ids, so both are untouched. harnesses/command.py is untouched too.
Schema:task_runs.v1 is widened in place: optional metrics.skill_notice and optional summary.discovery_gaps. schema_compat against main passes.
Evidence
Why. Measured 2026-10-06 with haiku-4.5 on the onetbb-quickstart skill, in real Claude Code, with the skill installed and no notice:
runner
Skill calls
pass
Harbor
0/6
1/6
Inspect
0/10
4/10 (same as no-skill)
Promptfoo
0/6
—
with notice (any runner)
10/10
10/10
After (live smoke, this branch).onetbb-transpose, us.anthropic.claude-haiku-4-5-20251001-v1:0, 3 reps, in-container agent. Total about $0.26.
arm
pass
Skill invoked
cost
docker-claude
0/3
—
$0.082
docker-claude-skill:onetbb-quickstart
2/2 + 1 errored, rerun 1/1, so 3/3
3/3
$0.096
docker-claude-skill-discover:onetbb-quickstart
0/3
0/3
$0.082
In every discovery row the plugin was offered: offered_plugins: ["onetbb-quickstart"], with onetbb-quickstart:onetbb-quickstart in offered_skills.
skill_notice: false in every discovery row. skill_listing.description_chars: 454, truncated: false.
So the description was in the listing in full, and haiku never loaded the skill. The discovery gap on this task is +100 pp.
The errored skill cell was FileNotFoundError on the sandbox's /tmp/dokimi-env-*/env-* file. It happened in a shared /tmp before the solve and has nothing to do with this change.
In the first run, the report's gap reads "— (arms lost different cells)", because the arms lost different cells. That is the intended withholding.
Checks (local).
ruff check .: clean.
mypy: no issues in 212 files.
pytest: 5186 passed, 2 skipped.
pytest --outcomes -m outcome: pass.
mkdocs build --strict: OK.
schema_compat --base intel/main: OK.
pre-commit on the changed files: pass.
diff-cover vs intel/main: 100% of 65 lines.
pre-commit run --all-files reports 50 ruff findings, all in .agent-eval/. Those files are on main and this PR doesn't touch them.
New tests (fake runner, no LLM):
discover arm (container): prompt starts "Solve this coding task.", skill not named;
claude argv minus prompt == skill arm's; skill_notice False; same treatment;
n_skill_invocations counted; plugin missing -> solver_error
discover arm (host): parametrized with the existing sealed-env host test
listing: description + when_to_use > 1536 -> truncated, run.log warning
registry: both aliases, with_skill_counterpart, empty path refused, --no-skill-notice in command
summary: discovery_gaps value, withheld when cells differ, absent without a pair
report: three arms in headline, Skill discovery row, truncation note, artifact validates
Merge Danger
Door: two-way
Adds new harness ids and optional artifact keys. The existing arms' prompt, argv and summary are unchanged. Reverting removes the aliases, and artifacts already written still validate.
Blast Radius: additive
Skill-arm rows gain skill_notice and skill_listing. Summaries gain discovery_gaps only when a discovery arm and its pair run together. A discovery arm also enters the McNemar Holm family as one more arm against the baseline, which slightly changes the adjusted p of other arms in runs that include it.
docker-claude-skill-discover:<skill> and docker-claude-host-skill-discover:<skill>
install the skill exactly as the skill arm does (same plugin, Skill tool and
launch) but leave the treatment notice out of the prompt, so the agent has to
find the skill by its description. The default skill arm keeps the notice.
Skill rows record metrics.skill_notice and metrics.skill_listing (description
length against Claude Code's per-skill listing cap); a discovery cell whose
description the listing cuts warns in run.log. When a discovery arm runs next
to its with-skill pair, summary.discovery_gaps holds both pass rates, the gap
and both skill-invocation rates, and the task report gains a Skill discovery
table. task_runs.v1 is widened in place for both keys.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.34%. Comparing base (6e695ce) to head (eba03bd). ⚠️ Report is 17 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
The metric counts Skill tool invocations only. Clarify that zero does not
prove the agent never accessed the skill's documentation—it may have read
SKILL.md directly, which this counter does not track.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approval recommended
The additive changes preserve defaults and have targeted regression coverage; the remaining report-wording issue is non-blocking.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Handle no-scored-cells case separately from differing lost cells
dokimi/dokimi/report/task_runs_report.py:164
Non-blocking: If both arms error on every cell, their errored-cell sets match, but the gap is null because neither has a pass rate. The report incorrectly labels this as “arms lost different cells.” Use a separate explanation when either arm has no scored cells.
The reason will be displayed to describe this comment to others. Learn more.
The arm itself is wired correctly: the notice is dropped on both routes, the argv matches the skill arm's, and the registry pairing is right. Two things are worth fixing before merge, both in _discovery_gaps, plus one nit.
make ci-local with docker: 23 passed, 0 failed, 3 skipped (semgrep, test-latest, docs deploy). 5,187 tests, changed-line coverage 100% of 65 lines, schema_compat and mkdocs build pass.
Reproductions for the _discovery_gaps findings, with no LLM involved.
A mutation run over the new code: 45 mutants, 9 of which pass the five changed test files. One equivalent mutant aside, the survivors are the None guard (inline) and the four gaps listed below.
Test gaps found by mutation (all four mutants pass the changed test files):
The truncation boundary is not pinned.skill_treatment.py:238 uses >. Changing it to >= passes. No test uses a description of exactly 1536 characters, so a description that fits would be flagged as cut.
The not self.skill_notice condition on the warning is not pinned.docker_solver.py:520: dropping it passes. The only warning test runs the discovery arm, so the skill arm (notice on, long description) could start writing the "description cut" warning to run.log unnoticed.
The report's harness in gaps filter is not pinned.task_runs_report.py:169: replacing it with True passes. _record_treatment writes skill_listing on the with-skill rows too, so with the filter gone those rows would be counted as discovery rows. The test's only truncated row is the discovery row.
The skill_notice default is not pinned.docker_solver.py:126: flipping it to False passes, because the registry always passes the flag explicitly. A hand-built DockerSolverHarness(skill_path=...) would silently lose the notice.
I agree with @copilot-pull-request-reviewer on the null-gap label (task_runs_report.py:164). New evidence: the schema description of gap already says null also covers "one has none scored", so the report text and the schema disagree.
Checked, no issues
Registry:docker-claude-skill-discover: cannot match docker-claude-skill:, -host- is tried first, and with_skill_counterpart maps docker-claude-host-skill-discover:/a:b to docker-claude-host-skill:/a:b and a with-skill id to None.
skill_listing: folded and literal scalars, numeric and list descriptions, and CRLF files parse. 1536 characters is not truncated and 1537 is. check_skill already rejects a missing SKILL.md before it runs.
Docs: 1536, 8000 and the harness ids match the code. The command example's flags exist in commands/tasks.py. No new page, so mkdocs.yml is unaffected.
Schema: the change only widens task_runs.v1 (optional skill_notice, optional discovery_gaps), which the in-place rule in AGENTS.md allows. schema_compat passes.
… gap
The gap is now withheld when the two arms attempted different cells, as with
pass_rate_delta. The comparable flag now checks both attempted.get(harness) ==
attempted.get(pair) and errored cell equality.
Add tests: discovery_gap_withheld_when_arms_attempted_different_cells covers
the case where discovery arm ran fewer tasks. discovery_gap_both_arms_fully_errored
covers the case where both arms fully errored (gap is None with no crash).
Also add optional skill_listing schema entry with description_chars, max_desc_chars,
budget_chars and truncated boolean fields.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The reason will be displayed to describe this comment to others. Learn more.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 5904e51. Updated the schema description for description_chars to clarify it counts the listing entry text shown to the model before truncation (description plus optional when_to_use with ' - ' separator), not just description alone. Also updated the docstring in skill_treatment.skill_listing() to match.
Three fixes from thread review:
1. _fmt_rate bool guard (4314920279): bool is a subclass of int in Python,
so _fmt_rate(True) would render as 100% rather than —. Add explicit bool
check to prevent masking data issues.
2. Gap message ambiguity (4314920354): Distinguish between gap=None due to
no scored cells (both arms fully errored) vs gap=None due to arms
attempting different cells. Render "— (no scored cells)" for the former,
"— (arms lost different cells)" for the latter.
3. Schema description_chars text (4314920422): Clarify that description_chars
counts the combined listing entry text (description + optional when_to_use
with ' - ' separator) before truncation, not just description alone.
Update schema and docstring.
Added tests for all three fixes.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The reason will be displayed to describe this comment to others. Learn more.
Review at 5904e51. The three threads from my earlier review are fixed: the attempted-cells check, the None guard and the skill_listing schema entry are all in, with tests that fail without them. Two things remain: the four test gaps I listed last time are untouched, and the new null-gap label is still imprecise.
make ci-local with docker: 23 passed, 0 failed, 3 skipped. 5,191 tests passed, changed-line coverage 100%. Python 3.13 was used here, so the project's own coverage gate was not applied.
Mutation run over the changes since 058646e (attempted-cells check, None guard, the new report branch, _fmt_rate), plus the four gaps from my last review. Reproductions for the gap/label behaviour, with no LLM involved.
Test gaps from my earlier review, still not covered (the five changed test files still pass with each mutant):
The truncation boundary is not pinned.skill_treatment.py:242: changing > to >= passes. Add a test with a description of exactly 1536 characters and assert truncated is false, and one with 1537 that asserts true.
The not self.skill_notice condition on the warning is not pinned.docker_solver.py:520: reducing it to if listing["truncated"]: passes. Add a skill-arm (notice on) test with a long description that asserts no [skill] WARNING line in run.log.
The report's harness in gaps filter is not pinned.task_runs_report.py:174: dropping it passes (18 passed in test_task_runs_report.py), because the only truncated row is on the discovery arm. Add a truncated skill_listing on a -skill row and assert the footnote count excludes it.
The skill_notice default is not pinned.docker_solver.py:126: flipping True to False passes, because the registry always passes the flag. Add a test that builds DockerSolverHarness(skill_path=...) directly and asserts the prompt opens with the notice.
Checked, no issues
Threads: the attempted-cells mutant (True and ...) fails test_discovery_gap_withheld_when_arms_attempted_different_cells. Removing the None guard raises TypeError and fails test_discovery_gap_both_arms_fully_errored.
Pairing:docker-claude-host-skill-discover:X pairs with docker-claude-host-skill:X. A discover arm with no pair, or beside a host/non-host mismatch, produces no discovery_gaps and no section.
Gap values: 2/3 against 0 gives 0.6667 and +66.67 pp. A zero-scored discovery arm gives gap: null and "— (no scored cells)". _fmt_rate(True) now renders "—".
Schema:validate_artifact accepts real summarize_task_results output for a normal gap, a null gap from differing cells and a null gap from fully errored arms. The change only adds optional keys.
Docs and repo rules: changelog fragment format, no new page for mkdocs.yml, no generated artifacts, no with_docs/without_docs keys touched.
The gap is withheld when the arms ran different cells or lost different
ones, but the report called every such case a loss. Say 'ran or lost'.
Also state that skill_listing.truncated is set only by the per-skill cap,
not the whole-listing budget.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The gap was withheld for two different reasons, but the summary only said
it was withheld, so the report called every case a loss. Record whether the
arms attempted the same cells and whether they errored on the same cells,
and label the withheld gap by which check failed. Artifacts from before the
flags keep the combined label.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
docker-claude-skill:s/SKILL.md and docker-claude-skill-discover:s install one
skill, but the pairing compared the id strings, so summary.discovery_gaps and
the report's Skill discovery table were dropped without a word. Pair on launch
plus skill directory, resolved against the cwd as the harness resolves it.
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…t run
The note was rendered only inside the Skill discovery table, which needs the
with-skill arm, so a discovery arm run against the baseline alone lost it while
run.log still warned. Count rows by skill_notice false, as the run.log warning does.
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…e default
The 1536 boundary (> not >=), the run.log warning firing only when the notice
is off, and skill_notice defaulting to on for a directly built harness each
survived mutation of the source with the suite green.
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Run: full suite on the head and on the head merged into current main, ruff, mypy, bandit, a mutation pass over the new guards, and a CLI dry run on intel/skills' onetbb-transpose.
docker-claude-skill:$S beside docker-claude-skill-discover:$S/, $S/SKILL.md or a relative spelling of the same directory → no discovery_gaps, no Skill discovery table, no warning → the arms pair on launch plus skill directory.
A truncated -skill-discover row run against the baseline only → run.log warns, but the report says nothing → the report note appears whether or not the pair ran, as the docs say.
Pushed:
9f5e27d: pair by resolved skill directory; docs and changelog fragment updated.
0820840: count truncated rows by skill_notice: false, outside the table.
331c66c: tests for the four mutation gaps raised in review (1536 boundary, warning guard, notice default; the report filter is pinned in 0820840).
Left for the author: resolve the outdated description_chars thread (fixed in 5904e51).
The reason will be displayed to describe this comment to others. Learn more.
Approved at 331c66c. No must-fix or mid item remains after our commits, each red without its fix; full suite, ruff, mypy and bandit clean on Linux; CI green.
Re-checked each point against the fork head 331c66c:
Verification: the touched test files pass on 331c66c (test_container_agent, test_skill_treatment, test_task_runs_report, test_docker_solver_harness: 213 passed, 1 skipped), and ruff is clean. I did not re-run the full suite, mypy, bandit, the mutation pass or the CLI dry run.
Pushed commits: 9f5e27d (pair by resolved skill directory), 0820840 (count truncated rows by skill_notice false) and 331c66c (tests for the 1536 boundary, warning guard and notice default) are all on feat/skill-discover-arm. Thanks for verifying them.
description_chars thread: already answered in 5904e51 and left unresolved for you to close.
The reason will be displayed to describe this comment to others. Learn more.
Review at 331c66c. The pairing by skill directory, the ran/lost/ran-and-lost labels and the report note for unpaired rows all work, and the earlier test gaps are now pinned. Three points remain, all in the tests of the new guards.
make ci-local with docker: 23 passed, 0 failed, 3 skipped (semgrep, test-latest, docs deploy). 5,203 tests passed, changed-line coverage 100% of 93 lines.
Reproductions with the LLM transport mocked, and a mutation run over the new code: 58 mutants, 45 killed, 13 survived. Three are equivalent and one is near-equivalent. Three of the survivors are listed below; the rest are rounding, warning text and a mode guard on a dry-run label.
Checked, no issues
Pairing:s, s/, ./s, s/SKILL.md and an absolute path pair with the same discovery arm. A host arm never pairs with a container arm. Two discovery arms with different spellings each get their own gap entry.
Schema: real summarize_task_results output, including a null gap with errored_cells_match: false, validates against task_runs.v1.json. The change only widens the schema.
Docs: 1536, 8000, the harness ids and the three withheld-gap labels match the code. mkdocs build --strict passes.
Earlier test gaps: the 1536 boundary, the skill_notice default and the not self.skill_notice guard now fail when mutated.
…ne-unscored gap
The discovery-arm warning guard, the report's cut-description note on a row
that was not cut, and the no-scored-cells check when only one arm is
unscored each survived mutation of the source with the suite green.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
make ci-local with docker: 23 passed, 0 failed, 3 skipped. 5,207 tests passed, 1 skipped, changed-line coverage 100% of 93 lines.
Mutation run over the three guards from my last review: if not self.skill_notice: at docker_solver.py:520, .get("truncated") is not None at task_runs_report.py:150, and or changed to and at task_runs_report.py:171. The new tests kill all three. Reducing the warning guard to if listing["truncated"]: is killed by the existing announced-arm test.
Checked, no issues
Warning guard: a test exists in each direction (truncated with notice off, truncated with notice on, untruncated with notice off).
No-scored-cells label: the new test is parametrized over each arm being the unscored one, and both cases assert — (no scored cells).
Docs and schema: 1536, 8000, the harness ids and the withheld-gap labels match the code. The schema change only widens task_runs.v1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dokimi's skill arm can only answer "is the content useful once the skill is loaded": its prompt opens with
TREATMENT_NOTICE, which tells the agent to load the skill. Skill authors also ask whether the agent finds the skill without being told. This PR adds a discovery arm for that question. It installs the same plugin and leaves the notice out. The existing arm and its default are unchanged.docker-claudedocker-claude-skill:<p>docker-claude-skill-discover:<p>(new)--tools,--settingsand env. A test compares the two arms'claudeargv with the prompt taken out, and they match.skillListingBudgetFraction, and this PR still doesn't. Claude Code 2.1.296 cuts each listed description atskillListingMaxDescChars(1536). It also shortens the whole listing to fit0.01 × context × 4characters, which is 8000 on a 200k window. These arms list one plugin skill plusdoctor(anddesignon the host route), so the budget can't be reached. Only the per-skill cut can apply, andmetrics.skill_listing.truncatedrecords it. A missing plugin was already asolver_errorin theinitevent, and that check also covers the new arm. The hidden-description problem seen in the external tools came from 33-skill catalogs, which these arms don't run. Pinning the fraction would add a second difference between the arms, so I left it out.n_skill_invocationsand the treatment-loaded check come fromstream_stats, the same as for the skill arm. The headline already shows each harness as its own row with "Skill invoked".treatments/specs.py(the Q&A--armsgrammar) andeval/arm_summary.py(Q&A rollups) don't seedocker-*harness ids, so both are untouched.harnesses/command.pyis untouched too.task_runs.v1is widened in place: optionalmetrics.skill_noticeand optionalsummary.discovery_gaps.schema_compatagainstmainpasses.Evidence
Why. Measured 2026-10-06 with haiku-4.5 on the onetbb-quickstart skill, in real Claude Code, with the skill installed and no notice:
After (live smoke, this branch).
onetbb-transpose,us.anthropic.claude-haiku-4-5-20251001-v1:0, 3 reps, in-container agent. Total about $0.26.docker-claudedocker-claude-skill:onetbb-quickstartdocker-claude-skill-discover:onetbb-quickstartoffered_plugins: ["onetbb-quickstart"], withonetbb-quickstart:onetbb-quickstartinoffered_skills.skill_notice: falsein every discovery row.skill_listing.description_chars: 454,truncated: false.FileNotFoundErroron the sandbox's/tmp/dokimi-env-*/env-*file. It happened in a shared/tmpbefore the solve and has nothing to do with this change.Checks (local).
ruff check .: clean.mypy: no issues in 212 files.pytest: 5186 passed, 2 skipped.pytest --outcomes -m outcome: pass.mkdocs build --strict: OK.schema_compat --base intel/main: OK.diff-covervsintel/main: 100% of 65 lines.pre-commit run --all-filesreports 50 ruff findings, all in.agent-eval/. Those files are onmainand this PR doesn't touch them.New tests (fake runner, no LLM):
Merge Danger
Door: two-way
Adds new harness ids and optional artifact keys. The existing arms' prompt, argv and summary are unchanged. Reverting removes the aliases, and artifacts already written still validate.
Blast Radius: additive
Skill-arm rows gain
skill_noticeandskill_listing. Summaries gaindiscovery_gapsonly when a discovery arm and its pair run together. A discovery arm also enters the McNemar Holm family as one more arm against the baseline, which slightly changes the adjusted p of other arms in runs that include it.🤖 Generated with Claude Code