Repository navigation
feat(catalog): review skills against the Agent Skills spec, add --strict and --format json - #163
Conversation
`catalog scan` checked only that a skill loads. Each loaded skill is now also reviewed: the directory name must equal `name` and be kebab-case (at most 64 characters), and `description` must be at most 1024 characters (errors, exit 1). A missing `name` and a SKILL.md byte-identical to an earlier skill's are warnings naming the first copy. disable-model-invocation is shown as a note. `--format json` prints one object with a row per skill and task. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
`catalog scan` exited 1 on a skill spec error. Spec errors now print and exit 0; `--strict` makes them exit 1, as a load error does. Load errors still exit 1 always. A SKILL.md whose frontmatter is not valid YAML is reported as a `frontmatter-parse` error carrying the YAML message. The loader reads that as "no description", so the scan showed only a misleading missing description error before. The scan uses a new parse_frontmatter_strict path; parse_frontmatter is unchanged for other callers. Without a skills.yaml, the scan's discovery path now records a broken skill as an error row instead of stopping at the first one. validate.py is ruff-formatted; catalog.py only in the lines this branch added. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #163 +/- ##
==========================================
+ Coverage 92.01% 92.03% +0.01%
==========================================
Files 223 224 +1
Lines 27768 27929 +161
Branches 4563 4588 +25
==========================================
+ Hits 25552 25705 +153
- Misses 1570 1575 +5
- Partials 646 649 +3
🚀 New features to boost your workflow:
|
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 1868f79. The loader changes and the exit-code split between load errors and --strict hold up. The problems are at the edges: one JSON crash, one false --strict failure, and tests that leave the name rules unpinned. Fix the first two inline comments before merge.
What I ran, on a Linux review host at 1868f79:
make ci-local: 6,897 tests passed, 21 skipped; the changed-line coverage step,schema-check, the changelog check, the wheel smoke tests andgitleakspass. Theruff,mypy,deptry,bandit,zizmor,pre-commit,mkdocsandpip-auditsteps could not run in my venv:make devstops at the tools install on this base, which #179 fixed onmain. Static checks, mypy and the strict docs build pass on GitHub's CI at this head.- Reproductions for the code findings, on fixture catalogs, with no model or network involved.
- A mutation run on
validate.pyandcommands/catalog.py: 80 mutants, 48 killed, 32 survive the 17 tests intest_catalog_validate.py(7 of the survivors are equivalent).
Test gaps (the code is correct today; these changes would pass the suite):
- The name rules beyond one title-case name are not pinned. These mutants of
validate.py:116all pass the suite:>to>=onNAME_MAX,NAME_MAX = 200, the length check removed, a regex that allowsa--b, a trailing-?,_allowed,fullmatchreplaced bymatch. A 65-character name,a--b,a-,a_bwould then beok. Suggestion: a parametrized test where name and directory match, so onlyname-formatfires:"a"*64and"a1-2b"give no finding;"a"*65,"a--b","a-","-a","a_b","a b"give["name-format"]. - Duplicate detection is not pinned to whole-file bytes and to the first copy. Hashing only the frontmatter, hashing stripped text, and "latest copy wins" all pass. The existing test changes the description, so the frontmatter differs. Suggestion: same frontmatter with a different body is not a duplicate; a trailing-newline difference is not a duplicate; three identical copies all name the first, which has
duplicate_of is None. stateprecedence anddisable-model-invocation: falseare untested. Swapping thewarnanderrorranks inSkillReview.statepasses, so a mirror with a bad name would bewarnand pass--strict. Reading the flag withbool(meta.get(...))passes, sofalsewould hide a skill. Suggestion: a bad-name skill that is also a mirror expectsstate == "error"and--strictexit 1; adisable-model-invocation: falseskill expects the flag False.- The JSON
tasksarray has no content test. Every JSON test uses--kind skillsor assertsreport["tasks"] == []. Renamingtrackorreason, mappingskippedtook, dropping the append, or printing the# tasksheader in JSON mode all pass. Suggestion: one--kind all --format jsonrun on a catalog with a loaded and a skipped task; parse the whole stdout and assert the task rows.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy
left a comment
There was a problem hiding this comment.
I pushed a commit per nit, up to 1ed1cfb, which fix the two nits from my review:
7e1d17e:dokimi/dokimi/external/catalog.py:108: catchOSErrorinstead ofFileNotFoundErrorin the SKILL.md scan and in_skill_entry(:204), keepingValueError.1ed1cfb:dokimi/docs/external-content.md:92: the example header now shows the ok/warn/error counts, and a sentence at:130says the YAML message is on thefrontmatter-parseline while the row's main line and JSONerrorstill say the description is missing.
Left as is:
- The four test-gap points are not labelled
Nit:orNon-blocking:in the review body, so they stay with the author.
make ci-local at 1ed1cfb: 20 passed, 8 failed (all exit 127, tools missing on the host), 7 skipped.
Not approving yet. Still open: the bug and the two risks from my review.
|
Test gaps from the review body, all added in 67b683e (
|
…or a root SKILL.md An unquoted YAML date as status crashed catalog scan --format json. A SKILL.md directly in the scan root was compared with the root directory name and failed --strict. Adds the review's mutation-gap tests. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 67b683e. Both earlier threads are fixed and tested. One new crash in the duplicate check is worth fixing before merge, plus a test gap and a doc nit, three points in all, all inline.
What I ran, on a Linux review host at 67b683e:
make ci-local: 27 passed, 1 failed, 7 skipped. 6,933 tests passed, 1 skipped; the changed-line coverage step,schema-check,ruff,mypy,deptry,bandit,zizmor, the strict docs build, the wheel smoke tests andgitleakspass. The one failure ispip-auditon the lock files, which this PR does not touch.- Reproductions for the code finding, on fixture catalogs, with no model or network involved.
- A mutation run on
validate.pyandcommands/catalog.py: 56 mutants, 41 killed, 15 survive. The name-rule, duplicate, state and--strictmutants are all killed now. The survivors that matter are in the test-gap comment.
Checked, no issues
- JSON status: date, float and bool
statusvalues come out as strings in the skill and task rows, and--format jsonstdout parses as one object with and without--strict. - Root
SKILL.md: with a plain scan and withcatalog_path: .,name-mismatchis skipped andname-formatstill applies. - Exit codes: a non-UTF-8 file, a missing description and broken YAML each exit 1 as a load error. A spec error alone exits 0 without
--strict.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy
left a comment
There was a problem hiding this comment.
I pushed a commit per nit, up to 9ec812d, which fix the nit from my review:
9ec812d:dokimi/docs/external-content.md:120: added that aSKILL.mddirectly in the scanned root is not compared with the root directory's name.
make ci-local at 9ec812d: 27 passed, 1 failed (pip-audit, same as at the reviewed head), 7 skipped.
GitHub's Tests fail at 9ec812d on every Python version, in test_growth_ratchet.py: commands/catalog.py::cmd_catalog_scan has complexity 21 against its baseline of 12, and external/catalog.py::read_skill_manifest has 11 with no baseline entry (the cap is 10). The commit changes only external-content.md. The ratchet came to main (#93) after this PR's earlier green run, and the PR's changes to those two functions take them past it. Splitting the two functions makes Tests green again.
Not approving yet. Still open: the bug and the risk from my review.
…er the complexity ratchet Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ot crash the duplicate check; pin root-skill rules 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): ~ `dokimi catalog scan` parameters changed:
+ added option `--format`
+ added option `--strict`Updated on every push; the |
…iginal in scan The manifest-less scan replaced discover_skills with its own rglob, so on top of main's template/mirror handling it listed template scaffolds again (failing --strict on anthropics/skills) and flagged as the duplicate the copy the skill-catalog arm keeps. Skip TEMPLATE_DIR_NAMES and pick the fewest-parts copy. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…tter-parse review A skill that fails to load never reaches _skill_entry's "resolves outside root" check, so _unloaded_review read a symlinked SKILL.md outside --root and put a line of it into the text and JSON report. Check containment before reading. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…l error row Dropping `or any_failure` let a default `catalog scan` on a broken skill exit 0, and narrowing the OSError catch back to FileNotFoundError brought back the PermissionError traceback; the suite stayed green on both. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
Part of #4.
Summary
catalog scannow reviews each loaded skill against the Agent Skills spec, and the report is machine-readable.Spec errors are reported and do not change the exit code unless
--strictis given, so existing scripts keep their exit status. Broken YAML frontmatter is named (frontmatter-parse, with the YAML message) instead of showing as a missing description. Without askills.yaml, the scan now reviews everySKILL.mdunder the root, and a skill that does not load becomes an error row instead of aborting the scan.Evidence
The new tests live in
tests/test_catalog_validate.py(17 tests). The CLI-level tests call thecatalog scancommand, so they show the old behaviour directly.Local checks on the rebased head:
Merge Danger
Door: two-way. The new flags and the JSON object are additive, and the exit code without
--strictis unchanged for every case that exited 0 before. Reverting the commits restores the old scan.Blast Radius: low, confined to
catalog scan.Behaviour changes for callers:
--strict, a spec error prints as anERRORrow and the scan still exits 0. Users who want a failing gate must pass--strict.# skills (N declared under ...; x ok, y warn, z error), and finding lines are indented under their skill. Parsers of the old header line need updating.skills.yaml. It now enumerates everySKILL.mdunder the root (rglob), so vendored or mirrored copies appear as rows, and mirrors are reported aswarn.SKILL.mdwith invalid YAML exits 1, with or without--strict, as a load error does.parse_frontmatterkeeps its lenient behaviour; the strict variant is a new function used only by the validator.Checklist
pytestpassesruff,mypy, and schema checks passdocs/external-content.md(scan section) and the README step-1 command🤖 Generated with Claude Code