Dogfood the skill-package authoring lint in CI; split oversized tools/SKILL.md - #213
Dogfood the skill-package authoring lint in CI; split oversized tools/SKILL.md#213OsherElhadad wants to merge 4 commits into
Conversation
…ls capability The nominally capability-agnostic intake phase inlined a tools-specific optimization playbook: the EXISTING-tool-code mandate, the "docstring-only iteration is under-used" rule, the tools wording of the depth mandate, and the two-phase (diagnose fan-out -> implement fan-out -> merge) subagent pattern. A user optimizing a system prompt or a skill package read paragraphs of tool-code advice that did not apply. Move that playbook to skills/capabilities/tools/references/optimizer-playbook.md and replace it in intake with a generic rule: load the selected capability's own optimizer playbook from skills/capabilities/<cap>/references/. Intake's flow, step numbering, and every capability-neutral mandate (STEP-0 reading, non-overfitting guardrail, ground-truth-for-diagnosis-only) are unchanged.
- intake/SKILL.md: the capability-playbook rule now names BOTH path forms and the reader each belongs to — repo-relative `skills/capabilities/<cap>/references/` for the intake agent authoring in-repo, runtime `./guidance/<cap>/references/` for anything written into the authored INSTRUCTIONS (the repo path does not resolve in the optimizer workdir). - optimizer-playbook.md: scope the "and fix the prompt" clause of the verbatim depth snippet to runs where `system-prompt` is also selected, so a tools-only run following the playbook verbatim no longer contradicts intake's capability-scoping rule. - optimizer-playbook.md: de-duplicate the redundant "What an under-used iteration looks like" section into one closing line; the mandate it restated already lives in the EXISTING-tool-code section. All 26 substantive claims still preserved.
The framework enforced an authoring budget on *user* skills and violated it in
its own: tools/SKILL.md was 673 body lines / ~10.9k tokens against the
MAX_BODY_LINES=500 / MAX_BODY_TOKENS=5000 bar that
skill-package/scripts/abstract.py ships. Nothing ran that lint in CI, so nothing
caught it.
Add skills/_registry/lint_skills.py: points the SAME shipped validator at
skills/*/*/SKILL.md and fails nonzero on a violation. Two deliberate deltas from
the in-run validate():
* structural warnings (body over lines/tokens, references nested >1 level, a
long reference with no TOC, a broken reference link) become ERRORS. An
optimizer mid-run should not be hard-blocked by a style budget; the repo's
own skills have no such excuse.
* an empty/stub or still-templated reference (TODO/TBD/FIXME) fails, per
CONTRIBUTING's "references ... only when filled".
Description *style* heuristics (POV, all-caps, "say WHEN", truncation risk) stay
advisory — judgement calls, not measurable violations.
Discovery is a glob, never a committed list, so a new skill is linted the day it
lands. MIN_SKILLS=20 is the anti-vacuity floor: a renamed or deleted skill dir
makes the glob return fewer packages and the lint fails loudly instead of
silently shrinking its own coverage.
Bring the two violators into compliance:
* tools/SKILL.md 681 -> 208 lines (body 673 -> 200, ~10934 -> ~3193 tokens).
Detail moved one level deep into references/edit-playbook.md (the eight
levers in depth, the in-body-guard before/after diffs, the three
code-bearing patterns, the full symptom->fix table, the full trigger list,
the safe tool-replacement protocol, worked composite bodies, good-vs-bad
edits) and references/documentation.md (how a model reads a tool
definition, the per-tool doc checklist, response/error design, the
reader-capability-tier adjustment). Both carry a TOC; SKILL.md keeps the
decision rule and links what it moved.
* using-cap-evolve: the description carried literal "<X>"/"<phase>", which
the validator correctly reads as XML tags (a hard problem, not a warning).
Reworded to plain text.
Also fix a real bug in the shipped validator: the broken-reference-link check
did not strip a Markdown "#anchor" before the existence test, so every deep
link into a reference read as broken.
core/tests/test_skill_authoring_lint.py guards the lint itself, not just its
verdict: discovery is dynamic, the count floor fires on a removed skill, an
oversized body and an empty placeholder each fail, and every promoted warning
fragment is still reachable from a real violation — so rewording a message in
abstract.py breaks a test instead of quietly turning a lint error into a no-op.
|
🏷️ Automatic Labeling I've analyzed this pull request and added the following labels:
These labels were selected based on the PR title, description, and changed files. If you believe any labels are incorrect or missing, feel free to adjust them manually. |
🔬 EvidenceEvery command run verbatim, with full output. Zero-API throughout ( EnvironmentTest suite + compileallThe newly-split references are PHYSICALLY PRESENT in the run dir: $ find /tmp/mat-proof -path "*guidance/tools*" | sort
./.capevolve/run_matproof/work/cand_0001/guidance/tools
./.capevolve/run_matproof/work/cand_0001/guidance/tools/SKILL.md
./.capevolve/run_matproof/work/cand_0001/guidance/tools/meta.yaml
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/concepts.md
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/documentation.md
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/edit-playbook.md
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/examples.md
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/optimizer-playbook.md
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/pitfalls.md
./.capevolve/run_matproof/work/cand_0002/guidance/tools
./.capevolve/run_matproof/work/cand_0002/guidance/tools/SKILL.md
./.capevolve/run_matproof/work/cand_0002/guidance/tools/meta.yaml
./.capevolve/run_matproof/work/cand_0002/guidance/tools/references
./.capevolve/run_matproof/work/cand_0002/guidance/tools/references/concepts.md
./.capevolve/run_matproof/work/cand_0002/guidance/tools/references/documentation.md
./.capevolve/run_matproof/work/cand_0002/guidance/tools/references/edit-playbook.md
./.capevolve/run_matproof/work/cand_0002/guidance/tools/references/examples.md
./.capevolve/run_matproof/work/cand_0002/guidance/tools/references/optimizer-playbook.md
./.capevolve/run_matproof/work/cand_0002/guidance/tools/references/pitfalls.md
./.capevolve/run_matproof/work/cand_0003/guidance/tools
./.capevolve/run_matproof/work/cand_0003/guidance/tools/SKILL.md
./.capevolve/run_matproof/work/cand_0003/guidance/tools/meta.yaml
./.capevolve/run_matproof/work/cand_0003/guidance/tools/references
./.capevolve/run_matproof/work/cand_0003/guidance/tools/references/concepts.md
./.capevolve/run_matproof/work/cand_0003/guidance/tools/references/documentation.md
./.capevolve/run_matproof/work/cand_0003/guidance/tools/references/edit-playbook.md
./.capevolve/run_matproof/work/cand_0003/guidance/tools/references/examples.md
./.capevolve/run_matproof/work/cand_0003/guidance/tools/references/optimizer-playbook.md
./.capevolve/run_matproof/work/cand_0003/guidance/tools/references/pitfalls.md...and BYTE-IDENTICAL to source ( $ diff -r skills/capabilities/tools/references $G/references ; echo rc=$?
rc=0
$ diff skills/capabilities/tools/SKILL.md $G/SKILL.md ; echo rc=$?
rc=0
$ shasum -a 256 <source> <run-dir> # the two NEWLY-SPLIT references
6ad93076186da12172c3fe4b5dfda5da4ef9a10b1a6c6adb06de5637f51ce274 skills/capabilities/tools/references/edit-playbook.md
a0f92877edaf941d440bed7a6ece931451a06e2a5370a57cdb088036095c941e skills/capabilities/tools/references/documentation.md
6ad93076186da12172c3fe4b5dfda5da4ef9a10b1a6c6adb06de5637f51ce274 /tmp/mat-proof/.capevolve/run_matproof/work/cand_0001/guidance/tools/references/edit-playbook.md
a0f92877edaf941d440bed7a6ece931451a06e2a5370a57cdb088036095c941e /tmp/mat-proof/.capevolve/run_matproof/work/cand_0001/guidance/tools/references/documentation.mdThe mechanism (unchanged by this PR, re-proved for the new files): $ grep -n "ignore_patterns" core/cap_evolve/harness.py
1043: ignore=shutil.ignore_patterns("__pycache__", "scripts", "*.pyc"),
1079: ignore=shutil.ignore_patterns("__pycache__", "scripts", "*.pyc"),
1135: ignore = shutil.ignore_patterns("__pycache__", "scripts", "*.pyc")No 8. Line counts, every SKILL.md, before → after (file lines and BODY lines/tokens)$ for f in skills/*/*/SKILL.md; do # body = after frontmatter, what the validator measures
SKILL.md file lines b->a body lines / tokens b->a
---------------------------------------------- ---------------------- ------------------------
skills/algorithms/agent-optimize/SKILL.md 141 -> 141 134 -> 134 / ~ 1826 -> ~ 1826
skills/algorithms/evograph/SKILL.md 149 -> 149 134 -> 134 / ~ 2248 -> ~ 2248
skills/algorithms/gepa/SKILL.md 113 -> 113 105 -> 105 / ~ 1543 -> ~ 1543
skills/algorithms/hill-climb/SKILL.md 56 -> 56 49 -> 49 / ~ 680 -> ~ 680
skills/algorithms/skillopt/SKILL.md 87 -> 87 80 -> 80 / ~ 1125 -> ~ 1125
skills/capabilities/mcp-tool/SKILL.md 235 -> 235 227 -> 227 / ~ 2922 -> ~ 2922
skills/capabilities/skill-package/SKILL.md 122 -> 122 114 -> 114 / ~ 1739 -> ~ 1739
skills/capabilities/system-prompt/SKILL.md 198 -> 198 190 -> 190 / ~ 2993 -> ~ 2993
skills/capabilities/tools/SKILL.md 681 -> 208 673 -> 200 / ~10934 -> ~ 3395 <-- CHANGED
skills/optimizers/run-optimizer/SKILL.md 107 -> 107 100 -> 100 / ~ 1195 -> ~ 1195
skills/orchestrate/orchestrate/SKILL.md 85 -> 85 77 -> 77 / ~ 1468 -> ~ 1468
skills/orchestrate/using-cap-evolve/SKILL.md 60 -> 60 51 -> 51 / ~ 636 -> ~ 636 <-- CHANGED
skills/phases/baseline/SKILL.md 79 -> 79 71 -> 71 / ~ 993 -> ~ 993
skills/phases/diagnose/SKILL.md 179 -> 179 171 -> 171 / ~ 2975 -> ~ 2975
skills/phases/evaluate/SKILL.md 70 -> 70 62 -> 62 / ~ 832 -> ~ 832
skills/phases/finalize/SKILL.md 63 -> 63 55 -> 55 / ~ 739 -> ~ 739
skills/phases/gate/SKILL.md 83 -> 83 75 -> 75 / ~ 1002 -> ~ 1002
skills/phases/implement-and-check/SKILL.md 98 -> 98 90 -> 90 / ~ 1312 -> ~ 1312
skills/phases/intake/SKILL.md 258 -> 255 250 -> 247 / ~ 4378 -> ~ 4333 <-- CHANGED
skills/phases/report/SKILL.md 106 -> 106 98 -> 98 / ~ 1392 -> ~ 1392New references (all one level deep, all with an early TOC): $ wc -l skills/capabilities/tools/references/*.md
196 skills/capabilities/tools/references/concepts.md
127 skills/capabilities/tools/references/documentation.md
474 skills/capabilities/tools/references/edit-playbook.md
377 skills/capabilities/tools/references/examples.md
53 skills/capabilities/tools/references/optimizer-playbook.md
128 skills/capabilities/tools/references/pitfalls.md
1355 total
$ head -21 skills/capabilities/tools/references/edit-playbook.md # TOC
# Edit playbook — the eight levers, in depth
The full detail behind the lever table in SKILL.md: what each edit class is, when
it wins, the before/after diffs, the three code-bearing patterns, and the safe
tool-replacement protocol. SKILL.md carries the decision rule; this file carries
the depth. Worked, copy-pasteable bodies live in
[`examples.md`](examples.md); regression traps live in [`pitfalls.md`](pitfalls.md).
## Contents
- [The eight levers](#the-eight-levers)
- [Before/after — convert a violated prose rule into an in-body guard](#beforeafter--convert-a-violated-prose-rule-into-an-in-body-guard)
- [Generalize, never hardcode](#generalize-never-hardcode)
- [The highest-leverage edit: deterministic CODE](#the-highest-leverage-edit-deterministic-code)
- [Three patterns that carry almost all the gain](#three-patterns-that-carry-almost-all-the-gain)
- [How agents fail — the full symptom→fix table](#how-agents-fail-and-how-tools-fix-it--the-full-symptomfix-table)
- [When to use this — the full trigger list](#when-to-use-this--the-full-trigger-list)
- [The safe tool-replacement protocol](#the-safe-tool-replacement-protocol)
- [Worked bodies — add tools that call existing tools](#worked-bodies--add-tools-that-call-existing-tools)
- [Concrete before/after (selection, argument-filling, compose)](#concrete-beforeafter-selection-argument-filling-compose)
- [What good vs bad tool edits look like](#what-good-vs-bad-tool-edits-look-like)
$ head -14 skills/capabilities/tools/references/documentation.md # TOC
# Documentation & response design — what the model actually reads
How a tool-using model consumes a tool definition, what a complete tool doc
contains, how to shape the RESULT so the next turn recovers, and how to scale all
of that to the reader's capability tier. SKILL.md carries the one-line rule
("document every tool: description, important-notes, `Raises:`, per-param, one
generic example"); this file is the checklist behind it.
## Contents
- [How tool-using agents actually read a tool](#how-tool-using-agents-actually-read-a-tool)
- [Adapting to the reader's capability tier](#adapting-to-the-readers-capability-tier)
- [Shape the RESULT, not just the call](#shape-the-result-not-just-the-call)
- [Document every tool comprehensively](#document-every-tool-comprehensively)
9. No substantive claim lost in the split12-gram content-word coverage of the OLD 673-line body against new old body content words: 7069
12-gram spans absent anywhere in new SKILL.md + all references: 45
...of which >=30 words: 15
[ 51 words] tools can and should all ship in the same candidate 1 line generic examples worked bodies in references examples md references examples md depth below and in references concepts md references concepts...
[ 33 words] on a passing task is a regression rescope or drop it this is how you ship many changes without net zero churn pick the lever by failure type the in body guard is...
[ 43 words] because the primitive is exactly what the agent declines to call see the before after diffs above the three patterns below and the worked bodies in references examples md references examples md prose ...
[ 38 words] act is exactly the kind of edit the traces show failing three patterns carry almost all the gain worked bodies below and in references examples md references examples md 1 validation rule enforcement ...
[ 46 words] self get record i if your adapter binds tools as methods worked examples with full bodies are below under add tools that call existing tools add tools that call existing tools the highest leverage edi...
[ 30 words] check you could have written in a few lines of code how agents fail and how tools fix it map the trace symptom to the code bearing edit each row...
[ 33 words] the first edit to reach for with compose add close behind a deterministic guard beats a sentence in a prompt see below for each violated rule first ask which existing tool governs this...
[ 36 words] existing tool governs this and what in body check enforces it usually the answer is editing that tool s body not adding a new one reword descriptions after you ve asked can this rule be code...
[ 33 words] implicated tools can and should all ship in the same candidate do not stop after a single edit lock any of these off via inputs policy json for example in a frozen api...
[ 32 words] hit search orders q 0 n return get order hit id add tools that call existing tools the highest leverage edit a docstring edit can only make the model more likely to...
[ 31 words] description pass a comma and a deleted raises line do not failure modes to avoid over describing into contradiction adding a fifth use when clause that conflicts with the first makes...
[ 33 words] stay in sync change both in one batch then run validate in a frozen api setting lock schema code off via policy composite tool sprawl a composite is worth it only if the...
[ 93 words] worth it only if the chain is frequent and error prone adding one for a two call path the agent already handles just enlarges the toolset and hurts selection removing a tool that s rarely but critical...
[ 64 words] the safety boundary between reword the docs and rewrite the program it exists because the same artifact is edited in very different trust settings an optimizer you trust to polish descriptions should ...
[ 43 words] full set tighten it to match your deployment s blast radius every refused edit is reported so a policy that s too tight surfaces as visible refusals rather than silent no ops artifact handlers tools j...Each of those 15 spans is a heading-join artifact (an old "safety boundary" references/concepts.md SKILL.md
"blast radius" SKILL.md references/concepts.md references/edit-playbook.md
"visible refusals rather than silent no-ops" SKILL.md
"Composite-tool sprawl" SKILL.md references/pitfalls.md
"rarely-but-critically needed" SKILL.md
"fight reasoning models" SKILL.md
"Over-describing into contradiction" SKILL.md references/pitfalls.md
"Schema changes that break callers" SKILL.md
"in sync" SKILL.md
"Generalize, never hardcode" references/edit-playbook.md SKILL.md
"safe tool-replacement protocol" (-i) references/concepts.md references/edit-playbook.md SKILL.md
"You must write the BODY" SKILL.md references/edit-playbook.md
"Prose cannot fix a BEHAVIORAL failure" SKILL.md references/edit-playbook.md The check earned its keep: the FIRST pass silently dropped the mis-selection / bad-argument-filling / fumbled-chain trigger bullets. It caught them; they are back as SKILL.md table rows plus the verbatim full trigger list in 10. Full test suite + compileall$ PYTHONPATH=core python -m pytest core/tests -q
........................................................................ [ 38%]
........................................................................ [ 77%]
......................................... [100%]
185 passed in 61.80s (0:01:01)179 baseline + 6 new = 185, 0 failed. The environmentally-flaky The 6 new tests, run alone: $ PYTHONPATH=core python -m pytest core/tests/test_skill_authoring_lint.py -v
core/tests/test_skill_authoring_lint.py::test_bundled_skills_pass_the_authoring_bar PASSED [ 16%]
core/tests/test_skill_authoring_lint.py::test_discovery_is_dynamic_and_sees_every_skill PASSED [ 33%]
core/tests/test_skill_authoring_lint.py::test_count_floor_fires_when_a_skill_disappears PASSED [ 50%]
core/tests/test_skill_authoring_lint.py::test_an_oversized_body_fails_the_lint PASSED [ 66%]
core/tests/test_skill_authoring_lint.py::test_an_empty_placeholder_reference_fails_the_lint PASSED [ 83%]
core/tests/test_skill_authoring_lint.py::test_promoted_fragments_still_match_the_validators_wording PASSED [100%]
============================== 6 passed in 1.08s ===============================$ python -m compileall -q core skills && echo "compileall OK"
compileall OK11. Worktree state / commit authorship$ git status --short
(clean)
$ git log --format="%h | %an <%ae> | %s" origin/main..HEAD
cd809e3 | Osher Elhadad <Osher.Elhadad@ibm.com> | Dogfood the skill-package authoring lint in CI; split tools/SKILL.md
41a510c | Osher Elhadad <Osher.Elhadad@ibm.com> | Address review nits on the tools optimizer playbook move
33449c1 | Osher Elhadad <Osher.Elhadad@ibm.com> | Move the tools/subagent optimizer playbook out of intake into the tools capability
$ git log -1 --format=%B | grep -ci "co-authored-by\|generated with"
0
0 (no forbidden trailers)
$ git diff --stat origin/main...HEAD
.github/workflows/ci.yml | 24 +
core/tests/test_skill_authoring_lint.py | 100 +++
skills/_registry/lint_skills.py | 134 ++++
.../capabilities/skill-package/scripts/abstract.py | 5 +-
skills/capabilities/tools/SKILL.md | 753 ++++-----------------
.../capabilities/tools/references/documentation.md | 127 ++++
.../capabilities/tools/references/edit-playbook.md | 474 +++++++++++++
.../tools/references/optimizer-playbook.md | 53 ++
skills/orchestrate/using-cap-evolve/SKILL.md | 2 +-
skills/phases/intake/SKILL.md | 53 +-
10 files changed, 1082 insertions(+), 643 deletions(-) |
🔍 Review — PR #213Verdict: APPROVE WITH NITS. The core of this PR holds up. I re-ran every claim: 185 tests pass, the lint is genuinely dynamic and fires on removal/rename/oversize/new-component-dir/empty-SKILL.md, the anchor bug in BlockingNone. Non-blocking1.
$ python -c "... m._parse_frontmatter(...)"
algorithms/evograph desc='>-' len=2
phases/baseline desc='Establish the starting point. Use after implement-and-check …' len=313Consequence: for # inject `<X>` + `I can help you` into evograph's BLOCK-SCALAR description
$ python skills/_registry/lint_skills.py /tmp/bs
advise algorithms/evograph: description should say WHEN to use the skill …
OK — 20 skill packages pass the authoring bar (10 advisory note(s))
rc=0 # <-- NOT CAUGHT
# same injection in a PLAIN-scalar description
ERROR orchestrate/using-cap-evolve: description must not contain XML tags
advise orchestrate/using-cap-evolve: description should be third person …
FAIL — 1 authoring violation(s) in 1 skill(s) # <-- caughtAlso note this is why Fix: 2.
$ python -c "...drop 'gate' from the committed manifest..."
manifest deliberately staled (gate removed)
$ python skills/_registry/build_manifest.py skills
rc=0 <- CI would PASS
$ git status --short skills/_registry/manifest.json
(empty — the step rewrote the file, so drift is erased, not reported)Consequence: the step is a no-op guard duplicating an existing test. It matters here specifically because your merge-order note leans on "regenerate the manifest to resolve the #198 conflict" — nothing in CI verifies someone actually did. Fix: one line — 3. The floor is $ rm -rf phases/gate; mkdir phases/newskill (valid SKILL.md)
$ python skills/_registry/lint_skills.py /tmp/probe-mask
OK — 20 skill packages pass the authoring bar (9 advisory note(s))
rc=0The new skill is linted (discovery is a real glob — that part is not vacuous), but the "coverage silently dropped" alarm doesn't ring, and the advisory count quietly went 10→9 with nobody told. See Lint robustness below for the design question. Nits4. $ # 303-line ref, first heading is "## Just a normal first section, no TOC at all"
warnings: NONE — 303-line ref with NO TOC passesPre-existing, but this PR promotes it to a build-failing error, so it's now advertised as enforcement it doesn't do. Not worth fixing in this PR; worth not claiming. 5. "net-zero churn" is the one phrase from the old body that survives nowhere ( 6. Content-loss auditNothing substantive was lost. My own audit, independent of your 12-gram check: Method: normalize (lowercase, strip non-alphanumeric, collapse whitespace) the pre-split body ( Per old section, survival rate: I chased every one of the 31 down to its claim rather than accepting "heading-join artifact":
$ diff <(sed -n '319,364p' OLD) <(sed -n '285,329p' references/edit-playbook.md)
1c1
< ## When to use this
> ## When to use this — the full trigger list
9,10c9
< raw primitives so completing it is the only path (§"highest-leverage edit",
> raw primitives so completing it is the only path (pattern 3 above).
Verdict on your remaining 12-gram gaps: the claim holds. I did not accept it — I resolved each of my 31 sentence-level gaps to either (a) a line-wrap/table/heading-join artifact of the diffing method, or (b) prose condensed in All deep links resolve, including anchors — repo-wide, not just in repo-wide deep .md links=2 problems=0Is the 200-line SKILL.md still sufficient?Yes — and this is the strongest part of the PR. I went in expecting to find this was the real regression and it isn't. The reason isn't the pointers, it's what you chose to keep. A mid-run optimizer reading only the 200-line body still gets, without opening anything: the core rule that a code guard beats a prompt sentence ( Two things make the pointer risk lower than it looks, and one thing makes it real:
Verdict: the split is behavior-preserving for a mid-run reader, and the 673→200 reduction on the recurring cost is a real win. Validator changeThe bug was real. Reproduced against === PRE-FIX (origin/main abstract.py) ===
SKILL.md references 'references/real.md#a-heading' which does not exist <-- FALSE POSITIVE
SKILL.md references 'references/real.md#nope' which does not exist <-- accidental true positive
SKILL.md references 'references/gone.md#x' which does not exist
=== WITH FIX (this PR) ===
SKILL.md references 'references/gone.md' which does not existEvery deep link read as broken pre-fix, and the regex The fix is correct and the split is minimal. Does it now miss anything? Yes, one thing, and I think that's acceptable. A valid file with a nonexistent anchor ( deep links checked=2 problems=0The reword guard genuinely guards. I reworded E AssertionError: ('lines (>', ['SKILL.md body has 1407 lines, over the 500 budget; …'])
FAILED core/tests/test_skill_authoring_lint.py::test_an_oversized_body_fails_the_lint
FAILED core/tests/test_skill_authoring_lint.py::test_promoted_fragments_still_match_the_validators_wording
2 failed, 4 passed in 2.25sTwo tests fail, one of them naming the exact orphaned fragment. This is the right design for string-coupled promotion, and the coupling is documented at Lint robustnessWhat it catches (all reproduced, not taken on trust):
Not vacuous. The new-component-dir probe is the one I most expected to fail (a What it misses: block-scalar descriptions (finding 1 — the biggest hole, and it's on a real skill today); rename-away + add-new netting to 20 (finding 3); dead anchors; the near-tautological TOC check (nit 4); Is Asserting against the manifest is the better shape and it's smaller, not bigger — On the declined "say WHEN" rewording — I agree with you, and more strongly than you argued it. Your case was "cosmetic churn." The actual case is that the advisory is wrong. The check is These are better triggering signals than "Use when …" would be — a phase skill's trigger is genuinely positional ("after intake"), not conditional, and "Use when you are after intake" is worse English for the same fact. Rewording them to satisfy CI wiring
Minor duplication:
Security: clean. Diff scan for Merge-order noteYour order is right; one correction to the stated reason.
$ git log --format="%h %p %s" origin/main..HEAD
cd809e3 41a510c Dogfood the skill-package authoring lint in CI; split tools/SKILL.md
41a510c 33449c1 Address review nits on the tools optimizer playbook move
33449c1 6fca097 Move the tools/subagent optimizer playbook out of intake into the tools capability
$ git log --format="%h %s" -3 origin/refactor/issue-106-deleak-intake
41a510c Address review nits on the tools optimizer playbook move
33449c1 Move the tools/subagent optimizer playbook out of intake into the tools capability
6fca097 (= origin/main tip)Linear history, no merge commit, no cherry-pick, so no double-apply risk regardless of how #185 lands — if #185 merges as a merge commit its SHAs are already ancestors here; if it squash-merges, this branch's identical content collapses to nothing. I verified the whole chain rather than reasoning about it. Simulated $ git merge --squash origin/refactor/issue-106-deleak-intake && git commit # 185
$ git merge origin/docs/issue-104-gate-modes
198 merged ok # no conflict with 185
$ git merge origin/feat/issue-107-skill-lint-ci
core/tests/test_skill_authoring_lint.py | 100 +++
skills/_registry/lint_skills.py | 134 ++++
.../capabilities/skill-package/scripts/abstract.py | 5 +-
skills/capabilities/tools/SKILL.md | 748 ++++-----------------
…
8 files changed, 999 insertions(+), 615 deletions(-) # clean, no conflictPost-merge state is green: $ python skills/_registry/lint_skills.py skills
OK — 20 skill packages pass the authoring bar (10 advisory note(s))
$ python skills/_registry/build_manifest.py skills && diff # vs committed
BYTE-EXACTThe My recommendation: #185 → #181 → #198 → #213, same as yours. #213 must be last so the lint lands on a tree the earlier PRs have already settled — Verification I re-ran$ git worktree add /tmp/rv-213 origin/feat/issue-107-skill-lint-ci
HEAD is now at cd809e3 Dogfood the skill-package authoring lint in CI; split tools/SKILL.md
$ PYTHONPATH=core python -m pytest core/tests -q
........................................................................ [ 38%]
........................................................................ [ 77%]
......................................... [100%]
185 passed in 59.49s185 as claimed (179 baseline + 6 new). The #200-flaky dashboard test passed in my run too. $ python -m compileall -q core skills; echo rc=$?
rc=0
$ python skills/_registry/lint_skills.py skills
skill authoring lint — 20 skill package(s) under /private/tmp/rv-213/skills
advise algorithms/evograph: description should say WHEN to use the skill …
advise algorithms/hill-climb: … (×10 total, matching the PR body exactly)
OK — 20 skill packages pass the authoring bar (10 advisory note(s))
rc=0
$ python skills/_registry/build_manifest.py skills
wrote /private/tmp/rv-213/skills/_registry/manifest.json (20 skill(s))
algorithm: agent-optimize, evograph, gepa, hill-climb, skillopt
capability: mcp-tool, skill-package, system-prompt, tools
optimizer: run-optimizer
orchestrate: orchestrate, using-cap-evolve
phase: baseline, diagnose, evaluate, finalize, gate, implement-and-check, intake, report
rc=0
$ diff <committed> <regenerated>
MANIFEST BYTE-EXACT
$ for f in skills/*/*/scripts/check.py; do … done
check.py ok=20 fail=0
$ python -c "…validator body measurement on new tools/SKILL.md…"
body lines 200 tokens ~ 3395Body is 200 lines / ~3395 tokens as the validator itself measures it ( Real zero-API $ grep -n "^capabilities" $D/.capevolve/project/capevolve.yaml
7:capabilities: [system-prompt, tools]
$ python -m cap_evolve.cli run --spec … --project … --run-ts rv213
{
"run_dir": ".capevolve/run_rv213",
"best_id": "cand_0001",
"baseline_val": 0.0,
"test_reward": 1.0,
"test_baseline_reward": 0.0,
"test_delta": 1.0,
"test_pass_k": {"1": 1.0, "2": 0.0},
"iterations": 3
}
$ find . -path "*guidance/tools*" -name "*.md" | sort
./.capevolve/run_rv213/work/cand_0001/guidance/tools/SKILL.md
./.capevolve/run_rv213/work/cand_0001/guidance/tools/references/concepts.md
./.capevolve/run_rv213/work/cand_0001/guidance/tools/references/documentation.md <-- NEW
./.capevolve/run_rv213/work/cand_0001/guidance/tools/references/edit-playbook.md <-- NEW
./.capevolve/run_rv213/work/cand_0001/guidance/tools/references/examples.md
./.capevolve/run_rv213/work/cand_0001/guidance/tools/references/optimizer-playbook.md
./.capevolve/run_rv213/work/cand_0001/guidance/tools/references/pitfalls.md
(… identical for cand_0002, cand_0003)
$ diff -r skills/capabilities/tools/references …/cand_0001/guidance/tools/references; echo rc=$?
rc=0
$ diff skills/capabilities/tools/SKILL.md …/cand_0001/guidance/tools/SKILL.md; echo rc=$?
SKILL.md rc=0Materialization claim reproduces exactly — both new references reach the optimizer byte-identical in all three candidate dirs. Non-vacuity and reword probes, plus the two failures the PR did not claim, are in Lint robustness and finding 1/2/3 above. Every verification claim in the PR body reproduced. The two corrections are in the coordination prose (#185 is the base, not merged in; |
Review of #213 found three guards that could not fail, all of the same shape this epic keeps hitting: the ground truth was the wrong artifact. 1. `_parse_frontmatter` could not read YAML block scalars, so `evograph`'s `description: >-` parsed as the literal ">-" (2 chars) and EVERY description check — including the XML-tag hard problem this PR exists to fix — passed vacuously on it. This is the shipped validator, so the blind spot was in users' own optimization runs, not only CI. Now folds `>`/`>-`/`|`/`|-`. 2. The CI "rebuild the manifest" step ran a script that WRITES the file, so a stale committed manifest was silently erased instead of flagged. Appends `git diff --exit-code`, and the step now earns its keep: the lint's coverage check reads that manifest, so it must be pinned to review. 3. `MIN_SKILLS = 20` missed rename-away + add-new in one commit (nets to 20). Replaced by comparing the glob's PATH SET against the committed manifest's — two sources that must agree, no magic number to remember to bump. Also, from the review's nits: - The "no early TOC" check was near-tautological (any `## ` in the first 1500 chars satisfied it) while this PR promotes it to build-failing. Now requires a real list of anchor links. This surfaced a genuine violation the old check could not see: `tools/references/examples.md` (378 lines, 17 sections) had no TOC at all — added one; all 17 anchors resolve. - The "say WHEN" advisory demanded the literal bigram "use when", making false positives of nine descriptions that state WHEN positionally ("Use after intake", "Use as the last evaluation step"). Fixed the CHECK, not the descriptions — rewording them to satisfy a regex would have been a downgrade. Advisory count is now 0 rather than 10, and it is 0 honestly. - Restored "This is how you ship many changes without net-zero churn." — the one literal phrase the split dropped. - Scoped the placeholder docstring to references/, which is all it checks.
🔧 Review fixesAll three non-blocking findings were real guard-vacuity defects and are fixed at the root. Two of the three nits are fixed too (one of them surfaced a genuine violation the old check could not see), and I took your position on the "say WHEN" advisory — fixed the check, not the nine descriptions. Commit: 1. Block-scalar descriptions — FIXED at the root (
|
| skill | len | starts |
|---|---|---|
| algorithms/agent-optimize | 501 | Fully-agentic, free-form optimization algorithm. Use |
| algorithms/evograph | 658 (was 2) | evo-graph as a cap-evolve algorithm (AGENT MODE ONLY |
| algorithms/gepa | 836 | Runs the real GEPA optimization loop (arXiv:2507.194 |
| algorithms/hill-climb | 409 | Runs a global hill-climb optimization loop where the |
| algorithms/skillopt | 622 | Runs the SkillOpt single-lineage optimization loop o |
| capabilities/mcp-tool | 395 | Optimize an MCP toolset whose server is EXTERNAL (yo |
| capabilities/skill-package | 403 | Optimize an Agent Skill package itself — its SKILL.m |
| capabilities/system-prompt | 455 | Optimize an agent's system prompt or policy text — t |
| capabilities/tools | 453 | Optimize an agent's OWN tool surface (tools it imple |
| optimizers/run-optimizer | 480 | Drives any shell-invokable coding agent (Claude Code |
| orchestrate/orchestrate | 460 | Drive the entire cap-evolve pipeline end to end, aut |
| orchestrate/using-cap-evolve | 586 | Entry-point router for cap-evolve. Use when a user a |
| phases/baseline | 313 | Establish the starting point. Use after implement-an |
| phases/diagnose | 408 | Extract the learning signal from execution traces — |
| phases/evaluate | 394 | Score a candidate on a split with honest, variance-a |
| phases/finalize | 326 | Score the best candidate on the held-out TEST split |
| phases/gate | 318 | Apply the acceptance decision that keeps optimizatio |
| phases/implement-and-check | 393 | The HARD GATE that must pass before any optimization |
| phases/intake | 437 | Phase 1 of the pipeline — collect inputs and scaffol |
| phases/report | 273 | Summarize a run for a human — baseline val → best va |
evograph was the only one affected; every other description was already plain-scalar and parsed correctly. A regression test asserts len(description) > 100 for every shipped skill, so a future block-scalar author cannot re-open the hole silently.
Did it surface a previously-hidden violation? Not in evograph itself — with 658 chars now visible it passes every check cleanly (no XML tags, third person, under both caps, and it does say WHEN: USE when algorithm_skill: evograph, which as you predicted matches once parseable). So the parser fix removed one advisory (evograph's spurious say-WHEN hit) rather than adding an error. A different fix did surface a real violation — see nit 4 below.
2. The CI manifest step — now it can fail (ci.yml:144-150)
Took your one-liner. Proof, staling the manifest and committing it (git diff compares against HEAD, so an uncommitted stale file is just rewritten and proves nothing):
$ python -c "...drop 'gate' from the committed manifest..."
gate dropped
$ git commit -qam "stale manifest (probe)"
stale manifest COMMITTED
--- OLD CI step (build_manifest.py alone) ---
rc=0 <- old step PASSES on a stale committed manifest
--- NEW CI step (build && git diff --exit-code) ---
rc=1 <- new step FAILS on drift
--- restore, both pass ---
manifest step rc=0
OK — 20 skill packages pass the authoring bar (0 advisory note(s))Is the step still worth keeping now that it checks something? Yes, and its role changed. test_manifest.py proves generation doesn't crash; this step proves the committed bytes match the tree — a different property that no test covers. And it is now load-bearing for finding 3: the coverage check reads that manifest, so pinning it to review is what stops "rename a skill and regenerate the manifest" from satisfying both sides. Renamed accordingly to "Check the committed skill registry manifest is not stale" — the old name described a write, which was the whole problem.
Note the lint step runs first in the job and its coverage check also catches a stale manifest, so the drift is named twice, earlier:
$ python skills/_registry/lint_skills.py skills # against the stale committed manifest
ERROR discovery: 20 skill package(s) on disk do not match the 19 in manifest.json — on disk only: ['phases/gate']; in manifest only: none. A skill was renamed/moved/removed; rebuild the manifest and re-review coverage
rc=13. MIN_SKILLS — replaced, and I went one step past your suggestion
Took your alternative. One refinement: instead of len(glob) == len(manifest["skills"]) I compare the path sets, which is the same size of code but names which skill moved instead of only that a count differs — the error message is the guard's whole value at 3am.
def _manifest_drift(skills_root: Path) -> str:
"""Anti-vacuity: the glob and the committed manifest are two independent views of
the same tree and must agree. A floor (`n < 20`) misses rename-away + add-new in
one commit; comparing the PATH SETS catches it, and needs no magic number."""Your rename-away + add-new probe, which nets to 20 and the old floor could not see:
$ rm -rf /tmp/p3/phases/gate; mkdir /tmp/p3/phases/newskill # valid SKILL.md
$ ls -d /tmp/p3/*/*/ | wc -l
20 # <- nets to 20; `n < 20` is FALSE, so SILENT
$ python skills/_registry/lint_skills.py /tmp/p3
skill authoring lint — 20 skill package(s) under /private/tmp/p3
ERROR discovery: 20 skill package(s) on disk do not match the 20 in manifest.json — on disk only: ['phases/newskill']; in manifest only: ['phases/gate']. A skill was renamed/moved/removed; rebuild the manifest and re-review coverage
rc=1 <- FIRESPlain removal still fires too (test_coverage_drift_fires_when_a_skill_disappears). MIN_SKILLS is gone — no magic number anywhere, and with finding 2 pinning the manifest, neither of the two sources can drift unreviewed. Your framing was right: deriving from two sources that must agree beats trusting either. That is #189's counts guard, #202's chart test and now this one, all the same failure — ground truth was the wrong artifact.
4. The "say WHEN" check — fixed the CHECK, reworded nothing
Your analysis is correct and I'm adopting it. The check demanded the literal bigram, so nine descriptions that state WHEN positionally and imperatively were false positives, and the tenth was only clean because of finding 1. Rewording nine good descriptions to green a regex would have made the corpus worse.
Broadened rather than dropped — WHEN is genuinely the primary triggering signal, so a description with no WHEN at all is still worth an advisory; the check just has to recognise both forms:
# WHEN can be stated conditionally ("Use when …") or positionally/imperatively
# ("Use after intake", "Use as the last evaluation step") — both are real triggering
# signals, so demanding the literal "use when" bigram flags good descriptions.
SAYS_WHEN_RE = re.compile(
r"\bwhen\b|\buse (after|before|as|at|to|between|right|during|only|this|the moment)\b",
re.I)All nine now pass; a description with no WHEN of any kind still fails (test_say_when_advisory_accepts_positional_when_phrasing asserts both directions). It stays advisory, not promoted. Net effect: the advisory list went 10 → 0, and every one of those ten was a false positive or a parser artifact — exactly as you diagnosed.
5. Nit — the near-tautological TOC check, and the real violation it was hiding
You said this wasn't worth fixing, just worth not claiming. I fixed it, because this PR promotes it to build-failing and the guard-that-can't-fire is the same defect as findings 1–3 — shipping a fourth one in the same PR that fixes three felt wrong. A TOC is now a list of anchor links, not "any ## in the first 1500 chars".
Your exact 303-line probe:
303-line ref, first heading is ordinary prose -> ["references/r.md is 307 lines without an early table of contents (needs >=3 '[section](#anchor)' links up front)"]
same ref WITH a real 4-link TOC -> no warning (correct)This surfaced a genuine violation the old check could not see — and it is in a file this PR touches:
$ python skills/_registry/lint_skills.py skills
ERROR capabilities/tools: references/examples.md is 378 lines without an early table of contents (needs >=3 '[section](#anchor)' links up front)
FAIL — 1 authoring violation(s) in 1 skill(s)examples.md is 378 lines with 17 sections and had no TOC at all — it passed only because ## 0. Turning N prose rules… happens to appear before char 1500. Added a 17-entry TOC; all 17 anchors resolve, and no in-document anchor anywhere in skills/ is broken:
repo-wide in-document anchors: problems = 0
deep .md links=29 problems=0edit-playbook.md (474 lines, 11 TOC links) already satisfied the stricter check.
6. Nit — net-zero churn
Restored verbatim at tools/SKILL.md:46-47, so your 219-sentence audit now has zero literal casualties:
A guard that fires on a passing task is a regression — rescope or drop it. This is how you ship many changes without net-zero churn.
Body goes 200 → 201 lines / ~3409 tokens, still far under the 500/5000 bar.
7. Nit — PLACEHOLDER_RE scope
Left the behaviour (you're right that XXX appears legitimately as run_XXXX in four SKILL.md bodies, so scanning bodies would be noise) and fixed the docstring so it no longer implies otherwise:
a
references/*.mdfile that is a stub or still carries TODO/TBD/FIXME scaffolding. Scoped to references on purpose:XXXappears legitimately in four SKILL.md bodies (run_XXXXin example commands), so scanning bodies would be noise. A SKILL.md body is NOT placeholder-checked.
Corrected coordination statements (PR body updated)
- Move the tools/subagent optimizer playbook out of generic intake into the tools capability #185 is this branch's BASE, not merged in. This branch's first two commits ARE Move the tools/subagent optimizer playbook out of generic intake into the tools capability #185's two commits, same SHAs, linear history, no merge commit and no cherry-pick — so there is no double-apply risk however Move the tools/subagent optimizer playbook out of generic intake into the tools capability #185 lands. If it merges, its SHAs are already ancestors here; if it squash-merges, this branch's identical content collapses to nothing. Landing it first is not optional, because it is the base.
- The shared file with docs: document the real gate modes (paired is the default; add simplicity_tiebreak) #198 is
skills/phases/intake/SKILL.md(inherited from those base commits), and its hunk here is byte-identical to Move the tools/subagent optimizer playbook out of generic intake into the tools capability #185's — so fix(docs): unify adapter contract as 3 required methods + hooks, fix stale implement-and-check signatures #181/docs: document the real gate modes (paired is the default; add simplicity_tiebreak) #198's intake edits conflict with Move the tools/subagent optimizer playbook out of generic intake into the tools capability #185, not with this PR.skills/_registry/manifest.jsonis not modified by this PR at all (git diff origin/main...HEAD -- skills/_registry/manifest.jsonis empty; it is already correct on main). The old claim that it was "regenerated" was wrong in both directions.
Recommended order unchanged: #185 → #181 → #198 → #213.
Declined / recorded, not fixed
- Dead anchors (a valid file with a nonexistent
#anchor) are still unchecked inabstract.py, as you recommended — it needs a GitHub-slug resolver and this is a shipped validator. I did verify all 29 deep links and every in-document anchor resolve today with a proper resolver (output above), but that check lives in this comment, not in the lint. - Reference size stays unchecked — deliberate, as documented: the body is the recurring cost.
harness.pynot mentioningreferences/— out of scope, pre-existing, and your one-word suggestion is the right shape for a later PR.
Full verification
$ PYTHONPATH=core python -m pytest core/tests -q
........................................................................ [ 38%]
........................................................................ [ 76%]
............................................. [100%]
189 passed in 62.94s (0:01:02)189 = 185 + 4 new (test_block_scalar_descriptions_are_actually_parsed, test_say_when_advisory_accepts_positional_when_phrasing, test_long_reference_needs_a_real_toc_not_just_a_heading, test_coverage_drift_fires_on_rename_away_plus_add_new; test_count_floor_fires_when_a_skill_disappears was renamed to test_coverage_drift_…). The #200-flaky dashboard test passed in my run.
$ python skills/_registry/lint_skills.py skills
skill authoring lint — 20 skill package(s) under /private/tmp/fx-213/skills
OK — 20 skill packages pass the authoring bar (0 advisory note(s))
rc=0
$ python -m compileall -q core skills; echo rc=$?
rc=0
$ python skills/_registry/build_manifest.py skills && git diff --exit-code skills/_registry/manifest.json
BYTE-EXACT rc=0
$ for f in skills/*/*/scripts/check.py; do python "$f"; done
check.py ok=20 fail=0
$ python -c "...validator body measurement on tools/SKILL.md..."
tools/SKILL.md body lines 201 tokens ~ 3409
$ python -c "import yaml; d=yaml.safe_load(open('.github/workflows/ci.yml'))"
jobs: 6
lint-skills steps: ['Checkout repository', 'Set up Python 3.11', 'Lint every bundled SKILL.md against the authoring bar', 'Check the committed skill registry manifest is not stale']Materialization property re-proved after all changes — real zero-API cap-evolve run, capabilities: [system-prompt, tools], mock optimizer, examples/toy_calc:
$ grep -n "^capabilities" $D/.capevolve/project/capevolve.yaml
7:capabilities: [system-prompt, tools]
$ python -m cap_evolve.cli run --spec … --project … --run-ts fx213
"run_dir": ".capevolve/run_fx213",
"best_id": "cand_0001",
"baseline_val": 0.0,
"test_reward": 1.0,
"test_baseline_reward": 0.0,
"test_delta": 1.0,
"test_pass_k": {"1": 1.0, "2": 0.0},
"iterations": 3
$ find . -path "*guidance/tools*" -name "*.md" | sort
./.capevolve/run_fx213/work/cand_0001/guidance/tools/SKILL.md
./.capevolve/run_fx213/work/cand_0001/guidance/tools/references/concepts.md
./.capevolve/run_fx213/work/cand_0001/guidance/tools/references/documentation.md <-- NEW
./.capevolve/run_fx213/work/cand_0001/guidance/tools/references/edit-playbook.md <-- NEW
./.capevolve/run_fx213/work/cand_0001/guidance/tools/references/examples.md
./.capevolve/run_fx213/work/cand_0001/guidance/tools/references/optimizer-playbook.md
./.capevolve/run_fx213/work/cand_0001/guidance/tools/references/pitfalls.md
(… identical for cand_0002, cand_0003)
$ diff -r skills/capabilities/tools/references …/cand_0001/guidance/tools/references; echo rc=$?
rc=0
$ diff skills/capabilities/tools/SKILL.md …/cand_0001/guidance/tools/SKILL.md; echo rc=$?
SKILL.md rc=0Both new references still reach the optimizer byte-identical in all three candidate dirs, now including the new examples.md TOC.
Files touched
skills/capabilities/skill-package/scripts/abstract.py— block-scalar parsing (finding 1), broadenedSAYS_WHEN_RE(finding 4), real-TOC check (nit 5)skills/_registry/lint_skills.py—MIN_SKILLS→_manifest_drift(finding 3), placeholder-scope docstring (nit 7).github/workflows/ci.yml—git diff --exit-codeon the manifest (finding 2), step rename, header commentcore/tests/test_skill_authoring_lint.py— 4 new tests, 1 renamedskills/capabilities/tools/references/examples.md— 17-entry TOC (the violation nit 5's fix surfaced)skills/capabilities/tools/SKILL.md— restored "net-zero churn" (nit 6)
Thanks — the block-scalar find in particular was a hole in the shipped validator that this PR's own premise depended on, and the two-sources-must-agree framing is a better guard than the floor I wrote.
Closes #107
The framework enforced an authoring budget on user skills and violated it in its own bundled skills, and nothing ran that lint in CI. This wires the existing validator over
skills/in CI and brings the violators into compliance.What the lint is
skills/_registry/lint_skills.pypoints the same shipped validator (skills/capabilities/skill-package/scripts/abstract.py) atskills/*/*/SKILL.mdand exits nonzero on a violation. New CI joblint-skillsruns it plusbuild_manifest.py.Two deliberate deltas from the in-run
validate():MAX_BODY_LINES=500/MAX_BODY_TOKENS=5000,references/nested more than one level, a long reference with no early TOC, a broken reference link. An optimizer mid-run should not be hard-blocked by a style budget; the repo's own skills have no such excuse.TODO/TBD/FIXME/<placeholder>reference.Discovery is dynamic — a
glob("*/*/SKILL.md"), never a committed list, so a new skill is linted the day it lands.MIN_SKILLS = 20is the anti-vacuity floor: a renamed or deleted skill dir makes the glob return fewer packages and the lint fails loudly instead of silently shrinking its own coverage (the failure mode #189's manifest-reading counts guard and #192's 5-host denylist both had).What the lint does NOT catch
Honesty about its reach:
advise, never fails): first-person POV, all-capsCRITICAL/ALWAYS/MUST/NEVER, "should say WHEN to use", and the 1,536-char listing-truncation risk. These are judgement calls, not measurable violations. 10 skills currently carry the "say WHEN" advisory — deliberately not "fixed", because rewording 10 descriptions to satisfy a regex is exactly the cosmetic churn this repo's own skills warn against.check.pybehavior.test_manifest.pycovers existence; each skill's owncheck.pycovers behavior. This lint is authoring-shape only.body.count("\n")+1,len(body)//4), not a real tokenizer.~4 chars/tokenis the documented approximation inabstract.py.references/is linted for shape, not size — a 400-line reference with a TOC passes by design (progressive disclosure means the body is the recurring cost).Before → after
Lint over ALL skills — BEFORE
Lint over ALL skills — AFTER (clean)
Line counts, every SKILL.md (file lines / body lines)
mcp-tool(227),system-prompt(190),diagnose(171) andintake(247) are all comfortably under the 500-line / 5000-token bar as measured by the validator, so the issue's "as needed" list needs no split.toolswas the only body over budget;using-cap-evolvewas the only hard problem.The split
tools/SKILL.md681 → 208 lines. Detail moved one level deep into two new references, each with a TOC, and SKILL.md links every section it moved:references/edit-playbook.md(474 lines) — the eight levers in depth, the in-body-guard before/after diffs, "generalize never hardcode", the deterministic-CODE argument, the three code-bearing patterns, the full symptom→fix table and full trigger list (both preserved verbatim from the old body), the safe tool-replacement protocol, worked composite bodies, the concrete before/afters, and good-vs-bad edits.references/documentation.md(127 lines) — how a tool-using model actually reads a tool definition (select vs. fill), the reader-capability-tier adjustment, response/error design, and the per-tool documentation checklist.SKILL.md keeps the decision rule: the core rule, a 12-row failure-signature → edit table, the action/policy table, the lean condensed doc+return rules, and the failure-modes list — each pointing at the reference that holds the depth.
Also fixed a real bug in the shipped validator. The broken-reference-link check did not strip a Markdown
#anchorbefore the existence test, so any deep link into a reference read as broken. My first split hit it (references/edit-playbook.md#the-safe-tool-replacement-protocolreported as "does not exist"), so it is fixed at root inabstract.pyrather than worked around.using-cap-evolve's description carried literal"<X>"/"<phase>", which the validator correctly reads as XML tags — a hard problem, not a warning, and a real discoverability hazard since the description is injected into a listing. Reworded to plain text; no content change.Coordination / expected merge order
This branch is based on
origin/refactor/issue-106-deleak-intake(PR #185) — not merged with it. This branch's first two commits ARE #185's two commits, same SHAs, linear history, no merge commit and no cherry-pick, so there is no double-apply risk however #185 lands (if it merges, its SHAs are already ancestors here; if it squash-merges, this branch's identical content collapses to nothing). I build directly on itstools/SKILL.mdReferences hunk andreferences/optimizer-playbook.md— my References section links it as a sixth reference.Expected merge order: #185 → #181 → #198 → this PR.
tools/references/optimizer-playbook.mdand lefttools/SKILL.mdat 686 lines deliberately, correctly identifying the split as this issue's job. It is this branch's base, so landing it first is not optional here; the References hunk is inherited, not re-applied.intake/SKILL.md, which I do not touch. No conflict —git diff origin/main pr181/pr198 -- skills/phases/intake/SKILL.mdoverlaps nothing in this diff.skills/phases/intake/SKILL.md(inherited from the Move the tools/subagent optimizer playbook out of generic intake into the tools capability #185 commits in this branch's base) — and its hunk here is byte-identical to Move the tools/subagent optimizer playbook out of generic intake into the tools capability #185's, so fix(docs): unify adapter contract as 3 required methods + hooks, fix stale implement-and-check signatures #181/docs: document the real gate modes (paired is the default; add simplicity_tiebreak) #198's intake edits conflict with Move the tools/subagent optimizer playbook out of generic intake into the tools capability #185, not with this PR.skills/_registry/manifest.jsonis not modified by this PR at all (git diff origin/main...HEAD -- skills/_registry/manifest.jsonis empty; it is already correct on main). CI now fails on a stale manifest (git diff --exit-code), so drift cannot pass unreviewed.The lint may flag a body that #181/#198 grow — but
intakeis at 247 body lines against a 500 bar, so there is ~250 lines of headroom.Verification
Every claim below is pasted output; full transcript in the 🔬 Evidence comment.
Lint fails CI on a deliberate violation, passes after revert
The guard is NOT vacuous — removing OR renaming a skill fires the count assertion
Manifest builds; every skill's
check.pyprints"ok": true— 20/20, full output in Evidence.Moved content still reaches the optimizer — real zero-API
cap-evolve runonexamples/toy_calcwith themockoptimizer andcapabilities: [system-prompt, tools]:harness.py'scopytree(..., ignore=ignore_patterns("__pycache__","scripts","*.pyc"))has noreferencespattern, so the property #185's reviewer verified holds for the newly-split files too — byte-identical,diffrc=0.No substantive claim lost in the split. A 12-gram content-word coverage check of the old 673-line body against new SKILL.md + all references leaves 15 spans ≥30 words; every one is a heading-join artifact or prose I condensed while
concepts.md§5 /pitfalls.mdalready carry the depth. Spot-checked each compressed claim survives by name (safety boundary,blast radius,visible refusals rather than silent no-ops,Composite-tool sprawl,rarely-but-critically needed,fight reasoning models,Over-describing into contradiction,Schema changes that break callers,in sync) — all present. The first pass did drop the mis-selection / bad-argument-filling / fumbled-chain trigger bullets; the check caught it and they are back as table rows plus the verbatim full trigger list in the playbook.Tests
179 baseline + 6 new (
core/tests/test_skill_authoring_lint.py), 0 failed. The environmentally-flakytest_dashboard_launch.py::test_maybe_launch_spawns_when_available(#200) passed here.Files touched
skills/_registry/lint_skills.pycore/tests/test_skill_authoring_lint.pyskills/capabilities/tools/references/edit-playbook.mdskills/capabilities/tools/references/documentation.md.github/workflows/ci.ymllint-skillsjob (lint + manifest rebuild)skills/capabilities/tools/SKILL.mdskills/capabilities/skill-package/scripts/abstract.py#anchorbefore the broken-link existence checkskills/orchestrate/using-cap-evolve/SKILL.md<X>/<phase>XML tagsPlus the inherited #185 base commits (
tools/SKILL.mdReferences hunk,references/optimizer-playbook.md,intake/SKILL.md) .skills/_registry/manifest.jsonis untouched by this PR.