Skip to content

Dogfood the skill-package authoring lint in CI; split oversized tools/SKILL.md - #213

Open
OsherElhadad wants to merge 4 commits into
mainfrom
feat/issue-107-skill-lint-ci
Open

Dogfood the skill-package authoring lint in CI; split oversized tools/SKILL.md#213
OsherElhadad wants to merge 4 commits into
mainfrom
feat/issue-107-skill-lint-ci

Conversation

@OsherElhadad

@OsherElhadad OsherElhadad commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

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.py points the same shipped validator (skills/capabilities/skill-package/scripts/abstract.py) at skills/*/*/SKILL.md and exits nonzero on a violation. New CI job lint-skills runs it plus build_manifest.py.

Two deliberate deltas from the in-run validate():

  1. Structural warnings become ERRORS — body over 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.
  2. Empty / still-templated references fail — per CONTRIBUTING's "references one level deep with a TOC if long, and only when filled": a stub (<5 lines) or a 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 = 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 (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:

  • Description style stays advisory (printed as advise, never fails): first-person POV, all-caps CRITICAL/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.
  • No semantic review. It cannot tell whether a reference is good, whether a claim is grounded, or whether the split preserved meaning. That is what the content-loss check below and human review are for.
  • No check.py behavior. test_manifest.py covers existence; each skill's own check.py covers behavior. This lint is authoring-shape only.
  • Line/token counts are the validator's own arithmetic (body.count("\n")+1, len(body)//4), not a real tokenizer. ~4 chars/token is the documented approximation in abstract.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
skill authoring lint — 20 skill package(s) under /private/tmp/wt-107/skills
  ERROR   capabilities/tools: SKILL.md body is 673 lines (>500); split detail into references/ (progressive disclosure)
  ERROR   capabilities/tools: SKILL.md body is ~10934 tokens (>5000); it is a recurring per-session cost — move detail into references/
  ERROR   orchestrate/using-cap-evolve: description must not contain XML tags
  advise  algorithms/evograph: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  algorithms/hill-climb: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  optimizers/run-optimizer: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  orchestrate/using-cap-evolve: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/baseline: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/diagnose: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/finalize: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/gate: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/implement-and-check: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/intake: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/report: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
FAIL — 3 authoring violation(s) in 2 skill(s)
rc=1
Lint over ALL skills — AFTER (clean)
skill authoring lint — 20 skill package(s) under /private/tmp/wt-107/skills
  advise  algorithms/evograph: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  algorithms/hill-climb: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  optimizers/run-optimizer: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/baseline: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/diagnose: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/finalize: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/gate: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/implement-and-check: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/intake: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
  advise  phases/report: description should say WHEN to use the skill ('Use when …') — it is the primary triggering signal
OK — 20 skill packages pass the authoring bar (10 advisory note(s))
rc=0

Line counts, every SKILL.md (file lines / body lines)

SKILL.md before after body before body after body tokens before → after
capabilities/tools 681 208 673 200 ~10934 → ~3193
phases/intake 258 255 250 247 ~4378 → ~4310 (from #185)
capabilities/mcp-tool 235 235 227 227 ~2922
capabilities/system-prompt 198 198 190 190 ~2993
phases/diagnose 179 179 171 171 ~2975
algorithms/evograph 149 149 134 134 ~2248
algorithms/agent-optimize 141 141 134 134 ~1826
capabilities/skill-package 122 122 114 114 ~1739
algorithms/gepa 113 113 105 105 ~1543
optimizers/run-optimizer 107 107 100 100 ~1195
phases/report 106 106 98 98 ~1392
phases/implement-and-check 98 98 90 90 ~1312
algorithms/skillopt 87 87 80 80 ~1125
orchestrate/orchestrate 85 85 77 77 ~1468
phases/gate 83 83 75 75 ~1002
phases/baseline 79 79 71 71 ~993
phases/evaluate 70 70 62 62 ~832
phases/finalize 63 63 55 55 ~739
orchestrate/using-cap-evolve 60 60 51 51 ~636
algorithms/hill-climb 56 56 49 49 ~680

mcp-tool (227), system-prompt (190), diagnose (171) and intake (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. tools was the only body over budget; using-cap-evolve was the only hard problem.

The split

tools/SKILL.md 681 → 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 #anchor before 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-protocol reported as "does not exist"), so it is fixed at root in abstract.py rather 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 its tools/SKILL.md References hunk and references/optimizer-playbook.md — my References section links it as a sixth reference.

Expected merge order: #185#181#198 → this PR.

The lint may flag a body that #181/#198 grow — but intake is 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

===== 1) DELIBERATE OVERSIZED BODY =====
  ERROR   phases/gate: SKILL.md body is 575 lines (>500); split detail into references/ (progressive disclosure)
  ERROR   phases/gate: SKILL.md body is ~8725 tokens (>5000); it is a recurring per-session cost — move detail into references/
FAIL — 2 authoring violation(s) in 1 skill(s)
rc=1

===== 2) DELIBERATE EMPTY PLACEHOLDER REFERENCE =====
  ERROR   phases/gate: references/notes.md is an empty/stub reference (<5 lines) — fill it or delete it
  ERROR   phases/gate: references/notes.md still contains the template placeholder 'TODO'
FAIL — 2 authoring violation(s) in 1 skill(s)
rc=1

===== 3) REVERTED — lint passes again =====
OK — 20 skill packages pass the authoring bar (10 advisory note(s))
rc=0

The guard is NOT vacuous — removing OR renaming a skill fires the count assertion

===== remove skills/phases/gate =====
  ERROR   discovery: found 19 skill packages, expected at least 20 — a skill was renamed/removed and lint coverage silently dropped
rc=1

===== rename the dir + its SKILL.md (dir count unchanged) =====
  ERROR   discovery: found 19 skill packages, expected at least 20 — a skill was renamed/removed and lint coverage silently dropped
rc=1

Manifest builds; every skill's check.py prints "ok": true — 20/20, full output in Evidence.

wrote skills/_registry/manifest.json (20 skill(s))
build_manifest rc=0

Moved content still reaches the optimizer — real zero-API cap-evolve run on examples/toy_calc with the mock optimizer and capabilities: [system-prompt, tools]:

{"run_dir": ".capevolve/run_matproof", "best_id": "cand_0001",
 "baseline_val": 0.0, "test_reward": 1.0, "test_delta": 1.0, "iterations": 3}

$ find . -path "*guidance/tools*"
./.capevolve/run_matproof/work/cand_0001/guidance/tools/SKILL.md
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/documentation.md   <-- NEW
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/edit-playbook.md   <-- NEW
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/optimizer-playbook.md
./.capevolve/run_matproof/work/cand_0001/guidance/tools/references/{concepts,examples,pitfalls}.md

$ 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

6ad9307…  skills/capabilities/tools/references/edit-playbook.md
6ad9307…  …/guidance/tools/references/edit-playbook.md
a0f9287…  skills/capabilities/tools/references/documentation.md
a0f9287…  …/guidance/tools/references/documentation.md

harness.py's copytree(..., ignore=ignore_patterns("__pycache__","scripts","*.pyc")) has no references pattern, so the property #185's reviewer verified holds for the newly-split files too — byte-identical, diff rc=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.md already 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

$ PYTHONPATH=core python -m pytest core/tests -q
185 passed in 58.83s

179 baseline + 6 new (core/tests/test_skill_authoring_lint.py), 0 failed. The environmentally-flaky test_dashboard_launch.py::test_maybe_launch_spawns_when_available (#200) passed here.

$ python -m compileall -q core skills && echo OK
compileall OK

Files touched

File Change
skills/_registry/lint_skills.py new — the dogfooding lint
core/tests/test_skill_authoring_lint.py new — 6 tests guarding the lint itself
skills/capabilities/tools/references/edit-playbook.md new — 474 lines of moved depth + TOC
skills/capabilities/tools/references/documentation.md new — 127 lines of moved depth + TOC
.github/workflows/ci.yml new lint-skills job (lint + manifest rebuild)
skills/capabilities/tools/SKILL.md 681 → 208 lines; links what moved
skills/capabilities/skill-package/scripts/abstract.py strip #anchor before the broken-link existence check
skills/orchestrate/using-cap-evolve/SKILL.md description: remove literal <X>/<phase> XML tags

Plus the inherited #185 base commits (tools/SKILL.md References hunk, references/optimizer-playbook.md, intake/SKILL.md) . skills/_registry/manifest.json is untouched by this PR.

Osher Elhadad added 3 commits July 30, 2026 00:22
…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.
Copilot AI review requested due to automatic review settings July 29, 2026 23:37

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@skillberry-bot skillberry-bot added documentation Improvements or additions to documentation dx Developer/onboarding experience enhancement New feature or request labels Jul 29, 2026
@skillberry-bot

Copy link
Copy Markdown
Contributor

🏷️ Automatic Labeling

I've analyzed this pull request and added the following labels:

  • documentation - dx - enhancement

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.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔬 Evidence

Every command run verbatim, with full output. Zero-API throughout (mock optimizer on examples/toy_calc).

Environment

$ git log --format="%an <%ae> | %s" -1
Osher Elhadad <Osher.Elhadad@ibm.com> | feat(observability): terminal degradation ladder + redacted forensic crash log
$ python -V
Python 3.14.2

Test suite + compileall

$ python -m compileall -q core skills; echo exit=$?
exit=0
$ PYTHONPATH=core python -m pytest core/tests -q

### 7. Moved content still reaches the optimizer — real zero-API `cap-evolve run`

Spec: `capabilities: [system-prompt, tools]`, optimizer `mock` (zero-API), `examples/toy_calc`.
```console
$ sed "s/^capabilities: .*/capabilities: [system-prompt, tools]/" templates/project/capevolve.yaml > $D/.capevolve/project/capevolve.yaml
$ grep -n "^capabilities" $D/.capevolve/project/capevolve.yaml
7:capabilities: [system-prompt, tools]
$ python -m cap_evolve.cli run --spec ... --project ... --run-ts matproof
{
  "run_dir": ".capevolve/run_matproof",
  "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
}

The 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 rc=0 on the whole references/ tree AND SKILL.md):

$ 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.md

The mechanism (unchanged by this PR, re-proved for the new files): core/cap_evolve/harness.py

$ 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 references pattern in the ignore list, so references/ rides along — the property PR #185's reviewer verified holds for the two new files too.

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 -> ~ 1392

New 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 split

12-gram content-word coverage of the OLD 673-line body against new SKILL.md + ALL references. Only spans ≥30 words are reported (shorter gaps are heading-join artifacts by construction).

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 ## Heading that became a link, so the words either side now abut different text) or prose condensed in SKILL.md while references/concepts.md §5 / references/pitfalls.md already carry the depth. Verified every compressed claim survives BY NAME:

"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 edit-playbook.md.

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 test_dashboard_launch.py::test_maybe_launch_spawns_when_available (#200, unrelated) passed in this run.

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 OK

11. 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(-)

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔍 Review — PR #213

Verdict: 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 abstract.py was real and the fix is correct, the reworded-message guard actually fails when I reword, materialization reproduces byte-identical from a real zero-API run, manifest is byte-exact, all 20 check.py pass. My independent content-loss audit found no substantive guidance lost — but it did find two ordinary bugs the new lint is supposed to be the backstop for, and one of them means the lint is partly blind on the very skill it is linting.


Blocking

None.


Non-blocking

1. skills/capabilities/skill-package/scripts/abstract.py:43-53_parse_frontmatter cannot read YAML block scalars, so evograph's entire description is invisible to every description check.

_parse_frontmatter does k, _, v = line.partition(":") and skips continuation lines (if ":" in line and not line.startswith(" ")). skills/algorithms/evograph/SKILL.md:3 uses description: >-, so the parsed description is the literal string '>-', length 2:

$ python -c "... m._parse_frontmatter(...)"
algorithms/evograph      desc='>-'                 len=2
phases/baseline          desc='Establish the starting point. Use after implement-and-check …' len=313

Consequence: for evograph the XML-tag check (a hard problem, not a warning), the POV check, the all-caps check, the 1,536-char truncation check and the say-WHEN check all operate on ">-" and pass vacuously. This PR's whole premise is using-cap-evolve's literal <X> being a real discoverability hazard — that exact hazard is undetectable in evograph today. Proof:

# 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)   # <-- caught

Also note this is why evograph shows regex-hit=True for say-WHEN while the other nine show False — the advisory list is itself partly an artifact of this parser, not of the descriptions.

Fix: evograph is the only block-scalar description in the repo, so the lazy fix is one of (a) join continuation lines in _parse_frontmatter when the value is >-/>/|, or (b) flatten evograph's description to a plain scalar and add a lint error for description: >/| so the parser is never handed input it can't read. (a) is the root fix — the parser is the shipped validator users' own runs depend on, so the blind spot is in a run too, not just CI.

2. .github/workflows/ci.yml:144-145 — the "Rebuild the skill registry manifest" step cannot fail on a stale committed manifest.

build_manifest.py writes the file, so running it in CI proves only that generation doesn't crash — which core/tests/test_manifest.py:12 already covers in test-python. A manifest committed out of sync with skills/ passes:

$ 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 — run: python skills/_registry/build_manifest.py skills && git diff --exit-code skills/_registry/manifest.json.

3. skills/_registry/lint_skills.py:32MIN_SKILLS = 20 does not detect the case it's written for when a skill is added in the same commit.

The floor is n < MIN_SKILLS, not n != len(manifest). Rename-away + add-new in one PR nets to 20 and passes silently:

$ 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=0

The 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.


Nits

4. skills/capabilities/skill-package/scripts/abstract.py:231 — the promoted "no early TOC" error almost never fires. The condition is "## " not in text[:1500], i.e. any ## in the first 1500 chars satisfies it — not a table of contents. A 303-line reference whose first section heading is ordinary prose passes:

$ # 303-line ref, first heading is "## Just a normal first section, no TOC at all"
warnings: NONE — 303-line ref with NO TOC passes

Pre-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 (/tmp/old_tools_SKILL.md:49 → absent from new SKILL.md + all six references). It was a rhetorical tail on a sentence whose substance ("A guard that fires on a passing task is a regression — rescope or drop it") is verbatim in SKILL.md:44-45. Zero information loss; recorded only because it's the single literal gap I could not account for.

6. skills/_registry/lint_skills.py:52PLACEHOLDER_RE is applied to references/*.md only, never to SKILL.md. A TODO in a SKILL.md body passes. Low value (XXX appears legitimately in four SKILL.md bodies as run_XXXX), so if you leave it, the docstring at line 33-36 shouldn't imply otherwise.


Content-loss audit

Nothing substantive was lost. My own audit, independent of your 12-gram check:

Method: normalize (lowercase, strip non-alphanumeric, collapse whitespace) the pre-split body (41a510c:skills/capabilities/tools/SKILL.md, 686 lines) and the union of new SKILL.md + all six references/*.md, then ask for every old sentence ≥8 content words (219 of them, split on unwrapped paragraphs so line-wrap isn't a false positive) whether it survives as a substring.

old sentences>=8 words: 219  absent from new corpus: 31

Per old section, survival rate:

 38/ 46  ## What you can change here
 29/ 35  ## The highest-leverage edit: deterministic CODE
  6/  7  ## How agents fail (and how tools fix it)
 11/ 13  ## When to use this
  2/  6  ## What can be optimized (default policy = all of these)
 11/ 12  ## How tool-using agents actually read a tool
  4/  4  ## Adapting to the reader's capability tier
  7/  9  ## Shape the RESULT, not just the call
  9/  9  ## Document every tool comprehensively
  5/  6  ## Concrete before/after
 18/ 21  ## Add tools that call existing tools
 10/ 10  ## What good vs bad tool edits look like
  0/  4  ## Failure modes to avoid          <-- rewritten, not dropped
  1/  3  ## The action policy (safety knob) <-- condensed

I chased every one of the 31 down to its claim rather than accepting "heading-join artifact":

  • ## Failure modes to avoid 0/4 looks alarming and is not. Every bullet was tightened, not cut, and every one has both a shorter form in SKILL.md:155-171 and a full symptom→detect→fix treatment in references/pitfalls.md (## Over-describing into contradiction :65, ## Schema and code drift apart :73, ## Composite-tool sprawl :81, ## Removing a rarely-but-critically-needed tool :90, ## Example dumps that hurt reasoning models :98). pitfalls.md is stronger than the old prose — it adds a Detect: line each bullet never had.
  • ## The action policy 1/3. Your claim that concepts.md §5 carries the depth reproduces: references/concepts.md:153-165 has "safety boundary", the trust-settings rationale, both policy presets, the mutation-lock citation, and "apply() reports every refusal, so an over-tight policy is visible, not silent." The condensed form in SKILL.md:126-131 keeps "safety boundary", "blast radius", and "visible refusals rather than silent no-ops". The specific phrasings not anywhere ("arbitrary handler code", "wire schema of a tool other systems depend on", "very different trust settings") are wording, and concepts.md:155-158 says the same thing.
  • ## What can be optimized 2/6. Four of six "missing" are the markdown table row-joins (the table is intact at SKILL.md:94-102) plus two sentences whose text is verbatim at SKILL.md:104-110 with different line-wrapping — my normalizer's paragraph split caught the wrap, not a loss.
  • The trigger list is preserved essentially verbatim in references/edit-playbook.md:285-329; the only diff vs the old body is the heading suffix and one cross-reference rewritten from §"highest-leverage edit", pattern 3 to pattern 3 above:
$ 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).
  • Spot-checked the load-bearing claims by name across the new corpus; every one lands: ~20 (4 files), input_examples, strict, verbosity/response_format, ISO-8601, whole US cents, 3–4 sentences, by far the most important, tools/list, THE READER, frontier, Guardrails, namespaced, apply_change_plan, un-skippable, blast radius, 1–2 currently-PASSING, rescope or drop, identify the class.

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 SKILL.md with the depth verifiably present in pitfalls.md/concepts.md. The single genuine textual casualty is "net-zero churn" (nit 5), which carries no information the surrounding sentence doesn't. Your first pass did drop real trigger bullets and your check caught them; the restored version is verbatim.

All deep links resolve, including anchors — repo-wide, not just in tools/:

repo-wide deep .md links=2  problems=0

Is 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 (SKILL.md:31-34), "Generalize, never hardcode" with the good/bad diff pair inline (:49-54), "Prose cannot fix a BEHAVIORAL failure" (:56-59), the per-change SAFETY blast-radius protocol (:43-47), the full 12-row failure-signature → edit table (:63-81), the "diagnose the first four rows FIRST / verify the guard FIRES / fix as MANY as appear" mandate (:83-91), the action table with code marked default (:94-102), "You must write the BODY" (:112), "Never bare-remove a tool" (:116), the <~20 ceiling (:122), the policy/blast-radius rule (:126-131), the condensed doc+return rules (:135-151), and all six failure modes (:155-171). That is the entire decision surface. The references hold depth on a decision already made, which is exactly the right cut — the old body's ## Add tools that call existing tools was 56 lines of worked bodies, useful once you've decided to compose, useless for deciding to.

Two things make the pointer risk lower than it looks, and one thing makes it real:

  • The pointers are decision-shaped, not topic-shaped. SKILL.md:116-119 doesn't say "see edit-playbook for tool replacement" — it states the rule ("Bare-removing strands every task; leaving the primitive exposed lets the model route around the guard") and then deep-links the protocol. A model that never opens the link still doesn't bare-remove.
  • harness.py:1810 already tells the optimizer to read ./guidance/<cap>/SKILL.md IN FULL, and SKILL.md:192 labels edit-playbook.md "read before editing". At 200 lines / ~3395 tokens, reading in full is now cheap enough to actually happen — whereas 673 lines / ~10934 tokens was the condition under which a model skims. Net attention on the decision rule plausibly goes up, not down.
  • The real gap: harness.py never mentions references/grep -c "guidance/.*references" core/cap_evolve/harness.py0. The instruction is "read ./guidance/<cap>/SKILL.md", singular. So following the links depends entirely on the model honoring in-document links. That's a pre-existing property of the six references that already existed before this PR (concepts.md, examples.md, pitfalls.md), not something Dogfood the skill-package authoring lint in CI; split oversized tools/SKILL.md #213 introduced, and it's out of scope here — but if a future PR wants to harden this, one word in harness.py:1810 ("SKILL.md and the references it links") closes it.

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 change

The bug was real. Reproduced against origin/main's abstract.py with a fixture containing a valid deep link, a valid file with a bad anchor, and a genuinely missing file:

=== 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 exist

Every deep link read as broken pre-fix, and the regex (?:references|scripts|assets)/[^)\s]+ matches anchors, so this fired for any author using them. Fixing it at root in abstract.py:238 rather than avoiding anchors in the split was the right call — this is the validator users' own skill-package runs execute, so the false positive was polluting real optimization runs, not just your CI.

The fix is correct and the split is minimal. rel.split("#", 1)[0] on a link with no anchor is identity, so no behavior change for the existing case. A genuinely missing file is still caught (line 3 of the after-output).

Does it now miss anything? Yes, one thing, and I think that's acceptable. A valid file with a nonexistent anchor (references/real.md#nope) was flagged pre-fix and is silent post-fix. That flagging was accidental — the old code wasn't anchor-aware, it was anchor-blind, and it caught bad anchors only as collateral of a check that also produced a false positive on every good one. Should dead anchors be caught? Ideally yes, but it needs a heading-slug resolver (GitHub slugification, em-dashes, code spans — I had to write one to audit this PR and got it wrong on the first attempt). Not worth adding to a shipped validator here. Recording it so nobody later assumes anchor validity is checked. Your own two deep links do resolve — I verified with a proper GitHub-slug resolver:

deep links checked=2 problems=0

The reword guard genuinely guards. I reworded abstract.py:216 from body is {n_body} lines (>{MAX_BODY_LINES}) to body has {n_body} lines, over the {MAX_BODY_LINES} budget:

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.25s

Two 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_skills.py:42-43. No fragment collides with an advisory ("chars;" truncation vs "lines (>"; "lines without an early table of contents" matched by "table of contents" not "lines (>", verified — the lines (> fragment requires the literal (>).


Lint robustness

What it catches (all reproduced, not taken on trust):

Probe Result
remove phases/gate ERROR discovery: found 19 … expected at least 20 rc=1
rename dir and SKILL.mdSKILL2.md ERROR discovery: found 19 … rc=1
oversized body in an existing component ERROR phases/fixture-skill: … 605 lines (>500) rc=1
oversized body in a NEW component dir (newcomp/bigskill) ERROR newcomp/bigskill: … 605 lines (>500) rc=1 — discovery is genuinely a glob
empty SKILL.md ERROR phases/emptyskill: frontmatter missing 'name' + missing … 'description' rc=1
stub / TODO reference ERROR … stub.md is an empty/stub reference rc=1
reworded promoted message 2 tests fail

Not vacuous. The new-component-dir probe is the one I most expected to fail (a */*/ glob is easy to get wrong) and it works.

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); TODO in a SKILL.md body (nit 6); reference size (documented and deliberate — edit-playbook.md at 474 lines passes, correctly, since the body is the recurring cost).

Is MIN_SKILLS = 20 the right design? No — but it's the right first design, and I would not block on it. The friction is real and you named it yourself in the comment at line 30-32 ("Raise this when skills are added"): every skill-adding PR either bumps a magic number or leaves a floor that has drifted below the real count, and a floor that has drifted is exactly the guard-that-stopped-guarding failure mode this epic keeps finding (#189, #192). The rename+add probe shows the drift can start immediately.

Asserting against the manifest is the better shape and it's smaller, not bigger — manifest.json is generated from the same tree by build_manifest.py, it's already committed, and core/tests/test_manifest.py already reads it. len(glob) == len(manifest["skills"]) is one line, needs no maintenance, catches the rename+add case, and fails loudly if either source drifts. The one thing it loses is independence: a bad PR that renames a skill and regenerates the manifest satisfies both. So the genuinely correct version is len(glob) == len(manifest) plus finding 2's git diff --exit-code on the manifest — then the manifest is pinned to review, and the count is pinned to the manifest, with no magic number anywhere. That's two one-liners replacing a number someone must remember to bump. Worth doing, not worth blocking this PR for.

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 re.search(r"\b(use when|when )\b", desc) (abstract.py:197) — it demands the literal bigram. Every one of the nine plain-scalar flagged skills already states WHEN, in the second sentence, imperatively:

phases/baseline               says: ['Use after']    -> "Use after implement-and-check and before any algorithm"
phases/diagnose               says: ['Use between']  -> "Use between evaluation and proposing edits"
phases/finalize               says: ['Use as']       -> "Use as the last evaluation step, after optimization stops"
phases/gate                   says: ['Use to']       -> "Use to inspect or reproduce a single accept/reject decision"
phases/implement-and-check    says: ['Use right']    -> "Use right after intake"
phases/intake                 says: ['Use at']       -> "Use at the very start of any optimization"
phases/report                 says: ['Use after']    -> "Use after finalize"
algorithms/hill-climb         says: ['Use as']       -> "Use as the algorithm for most runs"
optimizers/run-optimizer      says: ['Use this']     -> "Use this as the optimizer for every run"

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 \buse when\b would degrade nine descriptions to green a regex. And the tenth, evograph, is only "clean" because the parser can't read it (finding 1) — its description literally ends USE when algorithm_skill: evograph, which would satisfy the regex if it were parseable. So the advisory's current 10-item list is 9 false positives and 1 parser bug. Declining was correct; keeping it advisory rather than promoting it was also correct. If anything the regex should accept \bUse (when|after|before|as|at|to|between|right|this)\b, but that's a different PR.


CI wiring

.github/workflows/ci.yml parses (yaml.safe_load → 6 jobs), triggers are {'push': {'branches': ['main']}, 'pull_request': {'branches': ['main']}} — inherited from the existing on: block, so the new job runs on both. lint-skills steps: ['Checkout repository', 'Set up Python 3.11', 'Lint every bundled SKILL.md …', 'Rebuild the skill registry manifest']. Both scripts are stdlib-only (re, pathlib, json, sys, importlib.util), so skipping pip install is correct and the job is fast. Header comment updated (ci.yml:12-13). The lint step fails the build on violation (rc=1 verified above). The manifest step is the no-op — finding 2.

Minor duplication: core/tests/test_skill_authoring_lint.py::test_bundled_skills_pass_the_authoring_bar runs the same lint inside test-python, so a violation fails twice. That's fine — a dedicated red job names the problem where a reviewer looks, and the test gives a local signal.

using-cap-evolve description (finding 10 of the brief): accurate and still routes. <X>X and <phase>PHASE lose nothing — a description is a triggering signal, not a syntax reference, and the literal /cap-evolve: command forms are spelled out in the body (SKILL.md:25,29,32,37). One inconsistency: the description now says /cap-evolve:PHASE while docs/ARCHITECTURE.md:101 and docs/INSTALL.md:39 both say /cap-evolve:<skill>; the repo convention is angle brackets, and the description is the one place that can't use them. Not worth churning the docs.

"Use the moment a user asks""Use when a user asks" slightly improves triggering (it now hits the say-WHEN regex, and "the moment" was doing no work). "optimize <X>""optimize X" and "score higher on <benchmark>""score higher on some benchmark" keep the example-phrase shape that drives matching. No regression.

Security: clean. Diff scan for api[_-]?key|secret|token *=|password|sk-… → empty. Both scripts are read-only (read_text, glob, rglob); build_manifest.py writes only manifest.json. _load_validator does exec_module on a path inside the repo — that's the same loader pattern harness.py:1681 and skillcheck.py already use, no new trust boundary. No network, no shell-out in the lint.


Merge-order note

Your order is right; one correction to the stated reason.

#185 is not merged into this branch — it is based on it. git merge-base --is-ancestor origin/refactor/issue-106-deleak-intake origin/main → not merged; and this branch's first two commits ARE #185's two commits, same SHAs, same patch-ids:

$ 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 main + squash-merged #185 + #198 + this PR:

$ 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 conflict

Post-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-EXACT

The intake/SKILL.md hunk in this diff is byte-identical to #185's (diff of the two ranges → IDENTICAL), so #181/#198's intake edits conflict with #185, not with #213. And #185#198 on intake is conflict-free (git merge-tree → 0 conflict markers). Correction to the PR body: it claims "Only shared file with #198 is manifest.json" — the actual shared file is skills/phases/intake/SKILL.md (from the inherited #185 commits), and manifest.json is not modified by this PR at all (git diff origin/main...HEAD -- skills/_registry/manifest.json → empty; it's already correct on main). Neither error changes the outcome.

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 — intake is at 247/500 body lines, so #181/#198's growth has headroom, but a lint that goes red because a sibling merged first is exactly the churn worth avoiding. Landing #185 first is not optional here (it's this branch's base). If #181/#198 slip, #213 can merge right after #185 with no change.


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.49s

185 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; dodone
check.py ok=20 fail=0

$ python -c "…validator body measurement on new tools/SKILL.md…"
body lines 200 tokens ~ 3395

Body is 200 lines / ~3395 tokens as the validator itself measures it (body.count("\n")+1, len(body)//4) — which answers brief item 4: the lint and the in-run validate() call the same function in the same file, so the thresholds cannot diverge. There is no second token estimator in the lint path. (skills/capabilities/skill-package/scripts/token_report.py:24-25 duplicates the constants, but nothing in this PR's path calls it — a pre-existing duplication, not a divergence risk here.)

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 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=0

Materialization 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; manifest.json is untouched, not regenerated), not in any verification output.

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.
Copilot AI review requested due to automatic review settings July 30, 2026 00:31

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔧 Review fixes

All 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: 9e963b2. Suite is 189 passed, 0 failed (185 → +4 new tests). Advisory count is now 0, and it is 0 honestly rather than by parser blindness.


1. Block-scalar descriptions — FIXED at the root (abstract.py:43-72)

You were right that this is the important one: abstract.py is the shipped validator, so the blind spot was in users' own optimization runs, not only in CI. _parse_frontmatter now folds YAML block scalars (>, >-, |, |-, and the +/digit variants).

Your exact probe — inject <X> + I can help you into evograph's block-scalar description:

$ python skills/_registry/lint_skills.py /tmp/p1
skill authoring lint — 20 skill package(s) under /private/tmp/p1
  ERROR   algorithms/evograph: description must not contain XML tags
  advise  algorithms/evograph: description should be third person (e.g. 'Processes X …'), not first person ('I can …') — POV drift hurts discovery
FAIL — 1 authoring violation(s) in 1 skill(s)
rc=1  <- NOW CAUGHT (was rc=0)

Old vs new parser on that same input:

PRE-FIX  desc='>-' len=2
POST-FIX desc len=675  XML tag found=True

Reverted, it passes again:

$ python skills/_registry/lint_skills.py /tmp/p1
OK — 20 skill packages pass the authoring bar (0 advisory note(s))
rc=0  <- passes when clean

Per-skill parsed description length — proving no description is silently truncated anywhere, not just evograph:

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=1

3. 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  <- FIRES

Plain 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=0

edit-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/*.md file that is a stub or still carries TODO/TBD/FIXME scaffolding. Scoped to references on purpose: XXX appears legitimately in four SKILL.md bodies (run_XXXX in example commands), so scanning bodies would be noise. A SKILL.md body is NOT placeholder-checked.


Corrected coordination statements (PR body updated)

Recommended order unchanged: #185#181#198#213.

Declined / recorded, not fixed

  • Dead anchors (a valid file with a nonexistent #anchor) are still unchecked in abstract.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.py not mentioning references/ — 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=0

Both 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), broadened SAYS_WHEN_RE (finding 4), real-TOC check (nit 5)
  • skills/_registry/lint_skills.pyMIN_SKILLS_manifest_drift (finding 3), placeholder-scope docstring (nit 7)
  • .github/workflows/ci.ymlgit diff --exit-code on the manifest (finding 2), step rename, header comment
  • core/tests/test_skill_authoring_lint.py — 4 new tests, 1 renamed
  • skills/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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation dx Developer/onboarding experience enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dogfood the skill-package authoring lint in CI; split oversized tools/intake SKILL.md

3 participants