Skip to content

feat(repo): skill/skill-catalog arms, cache tokens, MCP config when repo ships one - #137

Merged
antonwolfy merged 42 commits into
intel:mainfrom
napetrov:feat/repo-skill-arms
Oct 26, 2026
Merged

antonwolfy merged 42 commits into
intel:mainfrom
napetrov:feat/repo-skill-arms

Conversation

@napetrov

@napetrov napetrov commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds two arm types, skill: and skill-catalog:, to repo run, and lets the claude-code profile start a repo's MCP servers.

 --arm guidance:none|raw|bridge
+--arm guidance:none+skill:<path>           # one skill → .claude/skills/<name>/
+--arm guidance:none+skill-catalog:<dir>    # every skill in <dir>

 result.json
+  skills: {skills_used, skill_invoked, skill_selected_correct}
   agent:  {input_tokens, output_tokens, cache_read_tokens, cache_write_tokens}   # existing; repo report now averages them

 harness profile
+  args_if_present: {".mcp.json": ["--mcp-config", ".mcp.json"]}   # appended only when the file is in the workspace
  • repo tasks check --arm rejects a skill path that does not exist.
  • Before this change, the stock claude-code profile passed --strict-mcp-config without --mcp-config, so a repo that ships .mcp.json ran with no MCP tools. Passing --mcp-config every time is not a fix either: Claude Code exits with MCP config file not found when the repo has no .mcp.json.

Evidence

Before: --arm skill:... was rejected. A blueprint repo with .mcp.json showed no mcp__* tools.

After: 5315 tests passed, 2 skipped. New tests:

  • tests/test_repo_skill_arms.py: skill arms, check validation, and the skills / cache fields in result.json.
  • test_claude_code_profile_passes_mcp_config_only_when_the_workspace_has_one.

The change was also used for about 870 real cells across 4 swcatalog blueprint task sets on Haiku 5.5 and Sonnet 5.5. A sample: banking-assistant on Haiku went from 23/25 with no guidance to 25/25 with a skill, and the skill was invoked in 19 of 25 runs.

Merge Danger

Door: two-way. The new profile key and result fields are additive. Existing arms behave as before.

Blast Radius: claude-code-runs. Repos that ship .mcp.json will now start those MCP servers, which changes the agent's tool set compared with earlier baselines for those repos.

🤖 Generated with Claude Code

…n results

`repo run` arms now compose with `+` like the Q&A --arms: a guidance part
(none/raw/bridge, raw by default) plus `skill:<path>` (one skill dir) and
`skill-catalog:<dir>` (every skill subdir), installed into the workspace at
.claude/skills/<name>/ in prep step 5 and listed with digests in meta.json.
Relative paths are under eval_dir; `repo run` and `repo tasks check --arm`
refuse missing skills and a catalog without a task's `expected_skill`.

A cell with skills records which ones the agent used (Skill tool calls or
reads of their SKILL.md) as skills.skill_invoked / skill_selected_correct;
`repo report` counts them. Each cell also records cache read/creation input
tokens, and the report gives per-row token means.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… repo's .mcp.json servers

The stock claude-code profile passes --strict-mcp-config with no --mcp-config,
so a repo that ships .mcp.json ran with no MCP tools. Passing --mcp-config
unconditionally breaks repos without the file (Claude Code exits on a missing
config). args_if_present appends arguments only when the named file exists in
the run's workspace.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@codecov-commenter

codecov-commenter commented Oct 20, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 93.75000% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.28%. Comparing base (af17696) to head (94d4323).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
dokimi/dokimi/repos/workspace.py 90.42% 8 Missing and 1 partial ⚠️
dokimi/dokimi/repos/arms.py 96.47% 2 Missing and 4 partials ⚠️
dokimi/dokimi/repos/manifest.py 77.77% 3 Missing and 3 partials ⚠️
dokimi/dokimi/commands/repo_run.py 93.54% 1 Missing and 1 partial ⚠️
dokimi/dokimi/repos/bases.py 94.73% 2 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #137      +/-   ##
==========================================
+ Coverage   92.24%   92.28%   +0.04%     
==========================================
  Files         239      240       +1     
  Lines       29934    30293     +359     
  Branches     4917     5003      +86     
==========================================
+ Hits        27613    27957     +344     
- Misses       1633     1642       +9     
- Partials      688      694       +6     
Files with missing lines Coverage Δ
dokimi/dokimi/repos/events.py 92.35% <100.00%> (+0.09%) ⬆️
dokimi/dokimi/repos/harness_profile.py 94.35% <100.00%> (+0.33%) ⬆️
dokimi/dokimi/repos/report.py 97.29% <100.00%> (+0.92%) ⬆️
dokimi/dokimi/repos/runner.py 94.26% <100.00%> (+0.34%) ⬆️
dokimi/dokimi/commands/repo_run.py 92.55% <93.54%> (+0.05%) ⬆️
dokimi/dokimi/repos/bases.py 96.96% <94.73%> (-3.04%) ⬇️
dokimi/dokimi/repos/arms.py 96.47% <96.47%> (ø)
dokimi/dokimi/repos/manifest.py 93.90% <77.77%> (-0.29%) ⬇️
dokimi/dokimi/repos/workspace.py 88.15% <90.42%> (+0.59%) ⬆️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@napetrov
napetrov force-pushed the feat/repo-skill-arms branch from ff1503d to 5c17c68 Compare October 20, 2026 05:27
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 5c17c68. The arm grammar, the guidance-then-skills ordering and the args_if_present design hold up. Four things are worth fixing before merge, three inline and one below.

What I ran, on a Linux review host at 5c17c68:

  • make ci-local: 20 passed, 0 failed, 4 skipped (docker-only and org-level checks); changed-line coverage gate passes.
  • Reproductions for the code findings, with the LLM transport mocked.
  • A mutation run over the new code: 56 mutants, 23 of which pass the full suite. The one that matters is inline on runner.py.

Findings that cannot be anchored inline:

  1. A skill is matched by its directory name, and nothing checks the name in its SKILL.md frontmatter. Skill.name is root.name (arms.py:100), and skill_usage and the expected_skill check compare against it. The Agent Skills spec requires the frontmatter name to equal the directory name, but SKILL.md is never parsed, so a skill that breaks this is accepted. If the agent reports the frontmatter name, as in skills/dirname/SKILL.md with name: pretty-name and an event {"kind": "tool", "name": "Skill", "skill": "pretty-name"}, skill_usage returns skills_used: [], skill_invoked: False, skill_selected_correct: False. The tests' _skill helper always writes name equal to the directory name. Suggestion: refuse a skill whose frontmatter name differs from its directory (or read the frontmatter name), and add a test with differing names.

Checked, no issues

  • Schema versioning: args_if_present is an optional key added to harness_profile.v1.json in place; old profiles still validate, and schema_compat against the base passes.
  • Old results: report.summarize reads the token fields with .get, so result files without cache fields or skills still summarize.
  • Arm ordering: guidance is applied before skills are installed, so guidance:none+skill:x keeps the skill. .. in a top-level skill: path is refused after resolve().
  • Housekeeping: the docs page is already in nav:, the changelog fragment is in changelog.d/, no generated artifacts are committed, and the tests run offline.

Comment thread dokimi/dokimi/repos/arms.py Outdated
Comment thread dokimi/dokimi/commands/repo_run.py Outdated
Comment thread dokimi/dokimi/repos/runner.py Outdated
…e a result dir

A skill or catalog entry with a symlink to a host path was copied with
its target's bytes (copytree dereferences), so the agent could read host
files. Links that leave a skill, and catalog entries that leave the
catalog, are now refused before any cell runs.

Two arms whose strings slug to the same cell name (skill:a/b and skill:a_b)
shared one result directory, so the second arm silently got no row. check_arms
now refuses such arms up front.

Regression tests cover both, and the runner's --mcp-config wiring.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/repos/arms.py Outdated
def check_arms(arms: list[str], content_dir: Path, tasks: list[Any]) -> list[RepoArm]:
"""Parse every arm and find its skills before anything runs; a catalog must hold each task's
``expected_skill``. Returns the parsed arms."""
from .runner import slug # runner imports this module

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The collision check works, but from .runner import slug here adds a new import cycle (arms -> runner -> grading -> workspace -> arms), and test_import_cycles_do_not_grow fails on a9b74b5.
Moving slug into arms.py (or a small module both import) would fix it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f7280b0: slug() moved into arms.py, and runner.py imports it from there, so arms no longer imports runner. test_import_cycles_do_not_grow passes.

dest = ws / skill.target
if dest.exists():
raise PrepError(f"task {task.id}: the workspace already has {skill.target}")
dest.parent.mkdir(parents=True, exist_ok=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the repo at the task base tracks .claude (or .claude/skills) as a symlink to a host path, this mkdir/copytree writes through it: with .claude -> /tmp/x in the base, --arm skill:skills/lint created /tmp/x/skills/lint/SKILL.md on the host and the cell still passed. Please refuse when ws/.claude or ws/.claude/skills is a symlink, or when dest.resolve() is not inside ws.resolve().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f7280b0: _install_skills refuses when .claude or .claude/skills is a symlink in the workspace, and when the destination would resolve outside the workspace. Regression test test_a_skill_is_not_written_through_a_repository_link (both link positions) fails on the old code.

if element == TOOLS_ELEMENT:
out.extend(self.tools[tools])
for name in sorted(present & self.args_if_present.keys()):
out.extend(self.args_if_present[name])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These args are added for every tool mode, so --tools none / --tools read on a repo with .mcp.json now start its MCP servers, and --tools does not limit MCP tools. The test in test_repo_reach.py even pins --tools "" --mcp-config .mcp.json. Should args_if_present apply only to the all mode (or be keyed per tool mode)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4e4e402 (option a): args_if_present is added only in the all tool mode, so --tools none/read no longer start the repo's MCP servers. tests/test_repo_reach.py now pins that none/read get no --mcp-config and all does; the end-to-end MCP test runs with --tools all.

Comment thread dokimi/dokimi/repos/runner.py Outdated
record: dict[str, Any] = {"task": task.id, "family": task.family, "arm": arm, "model": opts.model, "rep": rep,
"harness": opts.profile.name, "harness_version": meta["harness_version"],
"base_date": meta["base_date"], "score_task": bool(task.score), "agent": agent, "grade": grade}
if meta.get("skills"): # without a trace the agent used none of them

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the trace could not be read, this records skill_invoked=False and skill_selected_correct=False, and repo report counts the cell as "did not use the skill". That is "not measured" read as a result.
Please record null for both (or a measured: false flag) here and skip such cells in the report counts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f7280b0: a cell whose trace cannot be read now records skill_invoked and skill_selected_correct as null with measured: false. The report leaves those cells out of the counts, and the n/… denominator is the measured cells only. Test test_unmeasured_skill_usage_is_left_out_of_the_counts fails on the old code.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at a9b74b5. The three threads from my earlier review are fixed and each has a regression test. make ci-local fails on the import cycle @vlad-perevezentsev already flagged at arms.py:150; I agree, and the test passes on the PR's base. The link check still lets through links that make the copy fail. Two inline points, two more below.

What I ran, on a Linux review host at a9b74b5:

  • make ci-local: 19 passed, 1 failed, 4 skipped (docker-only and org-level checks). The failure is tests/test_import_architecture.py::test_import_cycles_do_not_grow; 5318 tests passed, 1 skipped, and the changed-line coverage gate passes.
  • Reproductions for the code findings, with the LLM transport mocked.
  • A mutation run over the new code: 75 mutants, 30 of which pass the touched test files. The ones that matter are inline.

Findings that cannot be anchored inline:

  1. repo compare cannot select a skill arm, because the selector splits on the last /. parse_selector (compare.py:75, outside this diff, predates the PR) does rpartition("/"). The PR makes / normal in an arm string. After running the docs' batch (guidance:raw and guidance:raw+skill:skills/lint), --treatment guidance:raw+skill:skills/lint is parsed as batch guidance:raw+skill:skills, arm lint, and fails with no finished cell matches the treatment 'guidance:raw+skill:skills/lint'. Suggestion: try the whole string as an arm first, and fall back to BATCH/ARM only when it matches no cell, or document that skill arms cannot be compared by repo compare.
  2. Nit: docs/repo-evaluation-contract.md is not updated. §3 still says every arm is a guidance:* arm, the §6 profile example omits args_if_present and shows --strict-mcp-config without --mcp-config, and the result tables omit skills, expected_skill and the cache-token fields.

Still open from my earlier review: none. The link-out, cell-name collision and runner-wiring threads are fixed.

Checked, no issues

  • Cell names: check_arms builds the slug exactly as cell_name does (slug(arm.replace(":", "-"))). skill:s and skill:s/ are refused, and skill:s and guidance:raw+skill:s are not wrongly refused. check_arms runs on both repo run and tasks check --arm.
  • Links out of a skill: a direct link, a chain a -> b -> host file, a dangling link to a host path and a catalog entry linking out are all refused.
  • MCP wiring: present is built from workspace/<name> for names in the profile's own args_if_present. Disabling present= or using rd / n makes the new runner test fail.
  • Schema versioning and housekeeping: the optional args_if_present key is added in place, schema_compat passes, old results still summarize, and no generated artifacts are committed.

Comment thread dokimi/dokimi/repos/arms.py Outdated
Comment thread dokimi/tests/test_repo_skill_arms.py
- arms: move slug() out of runner so arms no longer imports runner
  (closes the import cycle test_import_cycles_do_not_grow catches)
- arms: refuse skill links that dangle, loop, point at a directory, or
  are not regular files; they crashed every cell at workspace prep
- workspace: refuse to install a skill through a .claude or
  .claude/skills that the repository ships as a link
- runner/report: a cell whose trace could not be read records skill
  usage as null and is left out of the skill counts
- docs: contract §3 covers skill arms; §6 shows args_if_present
- tests: link cases, catalog with the expected skill first, guidance:none
  keeping a skill, tree hash sensitivity, unmeasured cells, write-through

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Skill arms become forms of workspace.Arm (skill:<dir>, skill-catalog:<dir>, joined to a
guidance part with +), named by directory so the name holds no '/'. The separate parse_arm
grammar of arms.py is gone; guidance:rev: and patch: and pin_arm work with skills. Cache
token fields use main's cache_read_tokens/cache_write_tokens next to usage_scope and
subagent_calls; skill cells follow main's unsupported.json handling.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
--tools none/read do not limit MCP tools, so a repo's .mcp.json started its servers in arms
meant to give the agent no tools.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…tory

The agent calls a skill by its frontmatter name, usage matches on the directory name; a
mismatch scored skill_invoked False. The merge with main added the check; this adds its
regression test and the changelog line.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Skill arms are named by directory (no '/'), so parse_selector needs no whole-string
fallback; the test pins that.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov

Copy link
Copy Markdown
Contributor Author

Pushed e1dd820 (merge of main in 263da4a, then one commit per point).

  1. Skill name vs directory (review 5583818902, item 1): option a. resolve_skills reads name from the SKILL.md frontmatter and raises ArmError when it differs from the directory name (a SKILL.md with no frontmatter name still takes the directory name). The check came in with the merge; 353ad50 adds the regression test and changelog line.
  2. repo compare and skill arms (review 5584559334, item 1): option a was not needed in the end. With the merge, skill arms are workspace.Arm forms named by directory (guidance:raw+skill:lint, no /), so parse_selector already reads the whole string as the arm and BATCH/ARM still works. No change to compare.py; e1dd820 adds a test that pins it. Two skill arms whose directories share a name are refused before the run.

Arm.skills is a tuple of (kind, path) pairs; the annotation said tuple[str, ...]
and mypy strict failed on it. Add a test for the guard, which no test ran:
two paths with one directory name in one invocation are refused.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
A skill arm is named by its directories, so resuming a batch with another
path of that name, or after editing a host skill, pooled two skills in one
row. The batch now records each skill arm's tree sha256 in _eval/skills.json
and refuses a mismatch; a new arm is added, the same bytes resume.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
4e4e402 limited the arguments to --tools all and updated the changelog and
the contract, but the profile schema still said they apply whenever the file
exists.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
An agent that exits before its first turn (for example on an MCP config it
rejects) leaves an empty trace that reads without error, so the cell was
recorded as measured and counted as not using the skill. A trace with no
event and no result line is now unmeasured, as an unreadable one is.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
These mutants passed the suite: skill_invoked as bool(used), the Skill event
without the command fallback, the expected_skill name check disabled, the
report's selection count for tasks without expected_skill, and the frontmatter
quote strip removed. Each now fails a test.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Oct 21, 2026

Copy link
Copy Markdown
Contributor
  • Ran: the full suite, ruff, mypy strict and bandit on the head, plus fake-harness repo run/repo report repros of each point below.
  • Same task base ships .claude/skills/lint, arm installs lint -> repo tasks check --arm says ok, then every cell crashes with "the workspace already has .claude/skills/lint" -> refused by check_arms before any cell runs.
  • Repo ships .mcp.json = { -> every --tools all cell exits 1 before its first turn and is graded as a task failure -> refused up front, or recorded as an agent error.
  • Batch resumed from main onto this head with .mcp.json present -> one row pools cells with and without --mcp-config, and meta.json shows no difference -> the applied args_if_present names stamped in meta.json.
  • Pushed:
    • 0413c5e fixes the red mypy job and tests the same-leaf guard.
    • 0c8a6e7 refuses a resume that runs other skill content under one arm name (_eval/skills.json).
    • 7785155 aligns the schema text with the all-only behaviour.
    • ee9d017 marks a cell whose harness never started as unmeasured.
    • c3527d9 adds tests for the surviving metric and validation mutants.
  • Left for the author: the three items above.

antonwolfy and others added 4 commits October 22, 2026 14:30
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…irectory reads; document a malformed .mcp.json

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov

Copy link
Copy Markdown
Contributor Author

Review at 135b273, items not on a thread:

  • args_if_present in the meta.json fields of the contract: done in 03f0fb2.
  • xaleryb's malformed .mcp.json (CLI exits 1 with --tools all): documented in ecd26bf, in the profile notes of docs/repo-evaluation-contract.md section 6 (args_if_present checks the file exists, not that the harness accepts it; fix or remove the file, or use --tools read/none). There is no up-front refusal or agent-error classification: that is a code change the author has not decided on.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at ecd26bf. The three test gaps, the patch docstring and the stale comment from my last review are closed, and the new tests kill the mutants they were written for. The base check still disagrees with preparation in two places, and one frontmatter value crashes check_arms. Four things are worth fixing before merge: three inline and one below.

What I ran, on a Linux review host at ecd26bf:

  • make ci-local: 25 passed, 1 failed, 6 skipped. The failure is pip-audit: it reports gitpython 3.2.0 and virtualenv 21.14.7 in the CI tooling lock, which this PR does not touch (main fixed the virtualenv one in #152; the gitpython advisory is new). 6770 tests passed, 1 skipped.
  • Reproductions for the code findings, with the LLM transport mocked.
  • A mutation run over the code my threads named: 7 mutants, all killed by the new tests.

Findings that cannot be anchored inline:

  1. docs/preparing-a-repository.md:250-252 (outside this diff) is now false. It says the claude-code profile "passes --strict-mcp-config with no --mcp-config, an MCP server added by a patch is not loaded, so this route cannot test an added MCP tool yet". With this PR, --tools all passes --mcp-config .mcp.json whenever the prepared workspace has the file, and present is computed after the arm and patch are applied (runner.py:163), so a patch: arm that adds .mcp.json is loaded. A reader would skip a test that now works. Suggestion: replace the sentence with the args_if_present rule (all mode only, file must exist in the workspace).

@xaleryb's question on a malformed .mcp.json waits for the author.

Still open from my earlier review: none. All five threads are fixed.

Checked, no issues

  • Thread tests: each of the seven mutants from my threads (rstrip, unfiltered written, arm.rev, utf-8, unstripped fence, if meta, len(parts) == 4 and) fails the touched suite now.
  • args_if_present: meta.json and the argv use the same rule; the list is [] outside --tools all and when the workspace has no matching file, as the new contract sentence says.
  • Schema versioning and housekeeping: the optional key was added in place to harness_profile.v1.json; both docs pages are in nav:; the changelog fragment is in changelog.d/; no generated artifacts are committed.

Comment thread dokimi/dokimi/repos/arms.py Outdated
Comment thread dokimi/dokimi/repos/arms.py Outdated
Comment thread dokimi/dokimi/repos/arms.py Outdated
@napetrov

Copy link
Copy Markdown
Contributor Author

On xaleryb's malformed .mcp.json item (the cell graded as a task failure): decided to refuse it up front, done in 36a9448. repo validate reports an error for a .mcp.json that is not valid JSON, not an object, or whose mcpServers is not an object, at each task base (at the eval tree when the source has no git history); repo run --tools all checks every task base before the image build and exits 1 naming the file and the JSON error; a cell whose arm wrote such a file (patch:) is a cell error before the agent starts. --tools read/none do not pass the file, so they are not checked. Tests: test_validate_refuses_a_malformed_mcp_json_at_a_task_base_and_at_a_tree_without_history, test_run_refuses_a_malformed_mcp_json_before_any_cell_when_the_agent_is_given_it, test_run_refuses_a_malformed_mcp_json_that_an_arms_patch_wrote (all fail on the old code), plus test_validate_accepts_a_valid_or_absent_mcp_json and the existing mcp-config argv test for valid and absent files. The contract note and changelog fragment describe the refusal.

…vers links and emptied dirs

repo validate and the repo run pre-flight (and a cell whose arm wrote the
file) refuse a .mcp.json the agent's CLI would exit on. check_arms refuses a
base that tracks .claude or .claude/skills as a symlink, a guidance:none arm
installs a skill whose directory the guidance drop emptied, and a SKILL.md
name YAML reads as an impossible date is not a crash.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…helpers from the arm, manifest and workspace checks

repo validate reports a malformed .mcp.json as a warning and succeeds; repo run
--tools all and a patch arm that writes one still refuse it. check_arms,
resolve_skills, check_repo and prepare_workspace are back under the complexity cap.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov

Copy link
Copy Markdown
Contributor Author

Follow-up to the .mcp.json threads: 09f903e changes the earlier 36a9448 behavior. repo validate now warns about a malformed .mcp.json and succeeds; repo run --tools all and a patch: arm that writes one still refuse it. Test: test_validate_warns_about_a_malformed_mcp_json_at_a_task_base_and_at_a_tree_without_history.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 980e7d8. The three fixes from my last review hold: a guidance:none skill arm now passes both check_arms and preparation, a tracked .claude or .claude/skills symlink is refused up front, and name: 2024-13-45 no longer crashes. The new .mcp.json pre-flight has a hole for non-UTF-8 files and two test gaps. Four things are worth fixing before merge, and there are three nits inline.

What I ran, on a Linux review host at 980e7d8:

  • make ci-local: 28 passed, 1 failed, 7 skipped. The failure is pip-audit: it reports gitpython 3.2.0 (CVE-2026-114145, fix 3.2.1) in the lock file, which this PR does not touch. 7276 tests passed, 1 skipped; the changed-line coverage gate passes.
  • Reproductions for the code findings, with the LLM transport mocked.
  • Mutation runs over the code added since my last review: the _only_empty_dirs and check_mcp_configs mutants that matter are inline.

Findings that cannot be anchored inline:

  1. docs/preparing-a-repository.md:244-252 (outside this diff) is still false, and it now also hides the skill arms. It says the claude-code profile "passes --strict-mcp-config with no --mcp-config, an MCP server added by a patch is not loaded, so this route cannot test an added MCP tool yet", and its status banner says arms that add a tool are planned. With this PR, --tools all passes --mcp-config .mcp.json whenever the prepared workspace has the file; the profile's argv with all ends in --mcp-config .mcp.json, and with read it has no --mcp-config. A reader skips a route that works, or never learns that skill: and skill-catalog: arms exist. Suggestion: replace the sentence with the args_if_present rule (all mode only, the file must exist in the workspace) and point to the skill arms in benchmarking-and-comparison.md.

Checked, no issues

  • Thread fixes: for a guidance:none arm with a skill under the globs (also nested references/deep/a.md), check_arms and prepare_workspace agree. Both refuse a non-glob helper.py in the skill directory, guidance:raw, a plain skill: arm, a patch: arm and guidance:rev:HEAD where HEAD ships the skill. A base with only .claude/settings.json is not refused.
  • Frontmatter dates: 2024-13-45, 2024-02-30 and out-of-range timezone forms return no name; 2024-01-02, 123, yes and [a] come back as strings and hit the name-mismatch ArmError.
  • manifest.py refactor: _check_eval_paths keeps the messages, order and conditions of the base.
  • .mcp.json edge cases: an empty file, a BOM, a duplicate mcpServers key, mcpServers: null, a 5000-digit integer and 100000 nested [ give a message, not a crash; --tools read leaves a malformed base alone; repo validate stays ok with a warning.
  • Housekeeping: the optional args_if_present key was added in place to harness_profile.v1.json and schema_compat passes; no new doc page, so no nav: change; no generated artifacts; the changelog fragment is in changelog.d/; the docs build with --strict.

Comment thread dokimi/dokimi/repos/bases.py Outdated
Comment thread dokimi/dokimi/repos/bases.py
Comment thread dokimi/dokimi/repos/workspace.py
Comment thread dokimi/dokimi/repos/arms.py Outdated
Comment thread dokimi/docs/repo-evaluation-contract.md Outdated
Comment thread dokimi/changelog.d/20261020_090000_napetrov_repo-skill-arms.md Outdated
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
@antonwolfy

Copy link
Copy Markdown
Contributor

I pushed fixes for the three nits from my review, one commit each:

  • 904355a: _frontmatter_name computes str(name) inside the try and also catches RecursionError and ValueError.
  • 24e3532: the contract says repo run --tools all exits 1 before any cell, and that the check reads the task base, so a patch: arm that repairs .mcp.json does not help.
  • 9e66dd3: the .mcp.json changelog bullet is split up, and the guidance:none bullet now says the arm can install the skill.

make ci-local at 9e66dd3 passes except for pip-audit (gitpython 3.2.0, CVE-2026-114145; #185 bumps it), the same as at 980e7d8. GitHub CI has not run on these commits: the branch conflicts with main, and Actions runs no workflow on a conflicting PR. Once main is merged in, CI will validate them.

Still open: the four risks from my review (the non-UTF-8 .mcp.json, the test gaps in bases.py and workspace.py, and preparing-a-repository.md).

@xaleryb

xaleryb commented Oct 23, 2026

Copy link
Copy Markdown
Contributor
  • Re-ran at 980e7d8: full suite, ruff, mypy, bandit, and fake-harness repo run repros of each earlier point.
  • Every earlier point passes at this head. A malformed .mcp.json ({, [], {"mcpServers": "x"}) with --tools all exits 1 before any cell. With a valid file the agent gets --mcp-config .mcp.json and meta.json records [".mcp.json"]. With --tools read no flag is passed and [] is recorded. The same-leaf and revision-arm base checks refuse up front.
  • Open: the branch conflicts with main (feat(repo): record whether the agent used what a patch: arm added (#41 D4) #153) in repos/runner.py, repos/report.py, docs/repo-evaluation-contract.md and tests/repo_eval_fixture.py. No CI run covers the merged result yet.
  • No commits pushed this round.
  • Left for the author: resolve the conflict. The resolved head will be re-checked.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov

Copy link
Copy Markdown
Contributor Author

Review-body items from 980e7d8: the non-UTF-8 .mcp.json hole and the two test gaps are fixed in 75e181b (replied on the three threads). docs/preparing-a-repository.md: the status banner now names the skill: and skill-catalog: arms (linking benchmarking-and-comparison.md), and the false "--strict-mcp-config with no --mcp-config" paragraph is replaced by the args_if_present rule (--tools all only, .mcp.json must exist in the workspace, a patch can add it), also 75e181b. The branch is merged with main in 9ef37f6 (conflicts in the contract, report.py, runner.py and two test fixtures; run_cell was split so the growth ratchet stays green).

…and the skill-dir guard; fix the MCP paragraph

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@github-actions github-actions Bot added the cli-interface-change Changes dokimi's user-facing CLI surface label Oct 23, 2026
@github-actions

Copy link
Copy Markdown

CLI interface change detected

This PR changes dokimi's user-facing CLI surface (commands and/or options):

~ `dokimi repo tasks check` parameters changed:
    + added option `--arm`

Updated on every push; the cli-interface-change label is on while the diff is non-empty.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 75e181b. The three threads from my last review are fixed: the non-UTF-8 .mcp.json hole is closed on the pre-flight side, the bases.py pre-flight tests kill the mutants I listed, and _only_empty_dirs is pinned. One risk remains, in the same fix: the runner half of it is not tested. One nit inline.

What I ran, on a Linux review host at 75e181b:

  • make ci-local: 26 passed, 3 failed, 7 skipped. 7816 tests passed, 1 skipped; changed-line coverage 96% (400 lines). The failures are not the PR's: pip-audit reports filelock 3.32.7 and setuptools 84.0.0 in the lock files, which this PR does not touch, and both gitleaks jobs fail because the tool download failed. A gitleaks scan of the PR's commits finds no leaks.
  • Reproductions for the code findings, with the LLM transport mocked.
  • A mutation run over the code added since my last review: tree_mcp_error, the every-base pre-flight and _only_empty_dirs mutants are killed by the new tests, except the two inline.

Checked, no issues

  • Pre-flight and runner agree on the bytes: \xff{}, UTF-16, an overlong sequence and surrogates give not valid UTF-8 text from GitTree, WorkTree and the runner's strict read; a BOM, a NUL byte, CRLF and an empty file give the same result on all three.
  • Failure class: a non-UTF-8 .mcp.json in the workspace raises PrepError before meta.json is written and before any agent starts, so it is a cell error, not a graded task failure.
  • Docs: the new preparing-a-repository.md paragraph matches the profile (read and none never pass --mcp-config; a patch that adds .mcp.json is loaded in all mode), the #skill-arms-in-repository-mode anchor exists, and mkdocs build --strict passes.
  • Housekeeping: harness_profile.v1.json only gained an optional key, schema_compat passes, no generated artifacts are committed.

Comment thread dokimi/dokimi/repos/runner.py
Comment thread dokimi/dokimi/repos/workspace.py
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a commit per nit, up to 61b3f25, which fix the nit from my review:

  • 61b3f25: dokimi/tests/test_repo_skill_arms.py:181: added a regular_file case, a regular file at the skill destination, to the parametrized refusal test.

make ci-local at 61b3f25: 26 passed, 3 failed, 7 skipped.

Not approving yet. Still open: the risk from my review.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…reads it

The runner's strict read of the prepared workspace's .mcp.json had no test:
a mutant that decodes with errors="replace" passed the whole suite. The new
test writes the bytes after preparation (the base check would stop a tracked
file first) and asserts the cell is refused as not valid UTF-8 text.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The runner half of the non-UTF-8 .mcp.json refusal is now pinned, and nothing else changed in the PR's own files since my last review.

What I ran, on a Linux review host at 94d4323:

  • make ci-local: 29 passed, 0 failed, 7 skipped (docker-only and org-level checks). 8,275 tests passed, 2 skipped; changed-line coverage 96% (400 lines).
  • A mutation run over the new test: read_text(encoding="utf-8", errors="replace") at runner.py:179 fails it, and so does removing the UnicodeDecodeError handling. It passes on the unmutated code.

Checked, no issues

  • Runner test: test_run_refuses_a_non_utf8_mcp_json_in_the_workspace_as_the_runner_reads_it writes b"\xff{}" after prepare_workspace, so the base check cannot stop it first. It asserts exit 1, no agent started, and the not valid UTF-8 text message.
  • Housekeeping: the added lines contain no network or credential access; the subprocess and environ hits are in test fixtures (a fake agent script). schema_compat passes, and no generated artifacts are committed.

@antonwolfy
antonwolfy merged commit fabf6ff into intel:main Oct 26, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli-interface-change Changes dokimi's user-facing CLI surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants