refactor(skills): using-scribe keeps the every-turn practices; moment-specific depth moves to reference files (#4398)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Successful in 1m44s
CI & Build / Build & push image (push) Successful in 21s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Successful in 1m44s
CI & Build / Build & push image (push) Successful in 21s
Anthropic's skill guidance: keep SKILL.md under 500 lines, split into reference files linked one level deep as it nears that. using-scribe was 478 and every new practice lands there. - SKILL.md 478 -> 317 lines. It keeps orientation, one copy, the reflexes, scope, the judge section, UI and the process-skill index, plus a "Read these when the moment comes" list naming each file with its moment. - projects.md: binding a non-git directory (.scribe) and project inception. - writing-records.md: where a new rule goes, lesson growth, and notes that carry their own check (reflex 10 keeps a pointer). - missed-retrieval.md: the record-before-dial route, verbatim. - Text moved, not rewritten, except for the seams and one cross-reference. Tests: - tests.helpers.skill_text reads SKILL.md plus its reference files. The ownership registry, the miss-route and the verification tests use it, so a topic stays owned by its skill whichever file holds it. - The force test scans every skill .md on its own, since each file is read on its own. - New test_skill_structure: SKILL.md <= 350 lines, every reference file is linked from SKILL.md, none links another, and one over 100 lines opens with Contents. Each guard is shown to fail. The plugin version is minted. That also clears 4fb53b8's red Plugin hooks lane, which failed only because PACKAGING.md changed without a mint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""Every bundled skill keeps the shape Agent Skills can load well (#4398).
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
Anthropic's skill authoring guidance
|
||||
(platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices):
|
||||
keep the SKILL.md body under 500 lines, split into reference files as it
|
||||
nears that, and link each reference file ONE level deep from SKILL.md —
|
||||
Claude may only preview a file reached through another reference file.
|
||||
|
||||
using-scribe reached 478 lines because every new practice lands there, the
|
||||
same squeeze `_INSTRUCTIONS` was in before #4389. It was split: what matters
|
||||
on most turns stays in SKILL.md, what matters at one moment (placing a rule,
|
||||
a missed retrieval, starting a project) moved to files SKILL.md names with
|
||||
the moment to open them. These tests keep that shape from quietly undoing
|
||||
itself.
|
||||
|
||||
WHAT IT PINS
|
||||
|
||||
1. A BUDGET BELOW THE GUIDELINE. 500 is where loading degrades; the budget
|
||||
sits under it so the next addition is a choice about what moves out, not
|
||||
a squeeze past the line.
|
||||
2. EVERY REFERENCE FILE IS LINKED FROM SKILL.md. A file nothing links is
|
||||
never read — it states guidance that reaches nobody.
|
||||
3. NO REFERENCE FILE LINKS ANOTHER. One level deep, per the guidance.
|
||||
4. A LONG REFERENCE FILE OPENS WITH ITS CONTENTS, so a partial read still
|
||||
shows what the file covers.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
SKILLS = pathlib.Path(__file__).resolve().parents[1] / "plugin" / "skills"
|
||||
|
||||
SKILL_LINE_BUDGET = 350
|
||||
TOC_AFTER_LINES = 100
|
||||
_MD_LINK = re.compile(r"\]\(([^)#\s]+\.md)\)")
|
||||
|
||||
|
||||
def _skill_dirs() -> list[pathlib.Path]:
|
||||
return sorted(p.parent for p in SKILLS.glob("*/SKILL.md"))
|
||||
|
||||
|
||||
def _refs(folder: pathlib.Path) -> list[pathlib.Path]:
|
||||
return sorted(p for p in folder.glob("*.md") if p.name != "SKILL.md")
|
||||
|
||||
|
||||
def over_budget(texts: dict[str, str], budget: int) -> list[str]:
|
||||
return [f"{k}: {len(t.splitlines())} lines" for k, t in texts.items()
|
||||
if len(t.splitlines()) > budget]
|
||||
|
||||
|
||||
def unlinked(skill_md: str, ref_names: list[str]) -> list[str]:
|
||||
linked = set(_MD_LINK.findall(skill_md))
|
||||
return [n for n in ref_names if n not in linked]
|
||||
|
||||
|
||||
def nested(refs: dict[str, str]) -> list[str]:
|
||||
return [f"{name} → {target}" for name, text in refs.items()
|
||||
for target in _MD_LINK.findall(text)]
|
||||
|
||||
|
||||
def missing_contents(refs: dict[str, str], after: int) -> list[str]:
|
||||
return [name for name, text in refs.items()
|
||||
if len(text.splitlines()) > after and "## Contents" not in text]
|
||||
|
||||
|
||||
def test_every_skill_md_fits_its_budget():
|
||||
found = over_budget({d.name: (d / "SKILL.md").read_text() for d in _skill_dirs()},
|
||||
SKILL_LINE_BUDGET)
|
||||
assert not found, (
|
||||
f"SKILL.md over {SKILL_LINE_BUDGET} lines: {found}. Move what matters at "
|
||||
f"one moment into a reference file SKILL.md links with that moment "
|
||||
f"(#4398), rather than raising the budget toward 500."
|
||||
)
|
||||
|
||||
|
||||
def test_every_reference_file_is_linked_from_skill_md():
|
||||
found = [f"{d.name}/{n}" for d in _skill_dirs()
|
||||
for n in unlinked((d / "SKILL.md").read_text(), [p.name for p in _refs(d)])]
|
||||
assert not found, (
|
||||
f"reference files no SKILL.md links: {found}. Nothing reads a file "
|
||||
f"nothing names — link it from SKILL.md with the moment to open it."
|
||||
)
|
||||
|
||||
|
||||
def test_reference_files_are_one_level_deep():
|
||||
found = [f"{d.name}/{x}" for d in _skill_dirs()
|
||||
for x in nested({p.name: p.read_text() for p in _refs(d)})]
|
||||
assert not found, (
|
||||
f"reference files linking other files: {found}. Link each from SKILL.md "
|
||||
f"directly; a file reached through another may only be previewed."
|
||||
)
|
||||
|
||||
|
||||
def test_long_reference_files_open_with_contents():
|
||||
found = [f"{d.name}/{n}" for d in _skill_dirs()
|
||||
for n in missing_contents({p.name: p.read_text() for p in _refs(d)},
|
||||
TOC_AFTER_LINES)]
|
||||
assert not found, f"reference files over {TOC_AFTER_LINES} lines with no '## Contents': {found}"
|
||||
|
||||
|
||||
def test_the_guards_can_fail():
|
||||
"""Rule 167: each guard bites on the failure it exists for."""
|
||||
assert over_budget({"s": "x\n" * 400}, SKILL_LINE_BUDGET) == ["s: 400 lines"]
|
||||
assert unlinked("see [a](a.md)", ["a.md", "b.md"]) == ["b.md"]
|
||||
assert nested({"a.md": "then [b](b.md)"}) == ["a.md → b.md"]
|
||||
assert missing_contents({"a.md": "x\n" * 150}, TOC_AFTER_LINES) == ["a.md"]
|
||||
Reference in New Issue
Block a user