Every loaded SKILL.md and every listed description is paid on every turn of every session, and every delivered message costs its recipient a full turn. scripts/budget.py gains SKILL_WORDS = 600, enforced by test_lock, and test_naming forbids a skill naming its own slash command; 53 descriptions and 10 body lines drop it. The 18 longest skills move details into 31 side files read only in their situation (organize's capacity, succession, peers, offices, relay and lineage; cleanup's docker, worktrees, execute and ask; code-tune, changelog, triage, dream, split and others), so all SKILL.md files shrink from 33866 to 26291 words without dropping a rule; an independent review of HEAD against the new text found no loss in 60 of 64 skills and three weakenings, now restored (WSJF re-estimates after an objective change, cleanup's volume checklist, essence's absolute dates). The kvb-org-organize Peer protocol sends status that asks nothing to the news board, answers nothing that asks nothing, keeps messages terse, and lets light runs admit themselves in the operator's wording; the skills follow it (operator reads the register, relax and wakeup reply through the news, evening collects them there, wsjf asks only for missing estimates, cleanup and triage batch their questions). Skills that explore code ask the codebase-memory-mcp graph first, subagent briefs scale with the stakes and cap their answers, and the skills that need the full register or council tables call show --wide or list --long. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
2.6 KiB
Python
Executable File
84 lines
2.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Count every first-party skill description against the listing budget, and each SKILL.md against its cap.
|
|
|
|
Args:
|
|
--repo: skills repository root.
|
|
--index: count the staged versions in the git index instead of the files.
|
|
--top: how many of the longest descriptions to list.
|
|
|
|
Exit codes:
|
|
0 within budget, 1 over budget, 2 usage error.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
LISTING_WORDS = 1500
|
|
DESCRIPTION_WORDS = 40
|
|
SKILL_WORDS = 600
|
|
|
|
|
|
def skill_sizes(repo: Path) -> dict[str, int]:
|
|
return {
|
|
p.parent.name: len(p.read_text(encoding="utf-8").split())
|
|
for p in sorted((repo / "skills").glob("*/SKILL.md"))
|
|
}
|
|
|
|
|
|
def words(skill_md: str) -> int:
|
|
frontmatter = skill_md.split("---", 2)[1]
|
|
return len(frontmatter.split("description:", 1)[1].split()) - 1
|
|
|
|
|
|
def git(repo: Path, *args: str) -> str:
|
|
result = subprocess.run(
|
|
["git", *args], cwd=repo, capture_output=True, text=True, check=False
|
|
)
|
|
if result.returncode != 0:
|
|
print(f"budget: {result.stderr.strip()}", file=sys.stderr)
|
|
sys.exit(2)
|
|
return result.stdout
|
|
|
|
|
|
def counts(repo: Path, index: bool) -> dict[str, int]:
|
|
if index:
|
|
paths = git(repo, "ls-files", "skills/*/SKILL.md").split()
|
|
return {Path(p).parent.name: words(git(repo, "show", f":{p}")) for p in paths}
|
|
return {
|
|
p.parent.name: words(p.read_text(encoding="utf-8"))
|
|
for p in sorted((repo / "skills").glob("*/SKILL.md"))
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument("--repo", required=True)
|
|
parser.add_argument("--index", action="store_true")
|
|
parser.add_argument("--top", type=int, required=True)
|
|
args = parser.parse_args()
|
|
|
|
repo = Path(args.repo)
|
|
if not (repo / "skills").is_dir():
|
|
print(f"budget: {repo} has no skills/ directory", file=sys.stderr)
|
|
return 2
|
|
found = counts(repo, args.index)
|
|
total = sum(found.values())
|
|
over = sorted(name for name, n in found.items() if n > DESCRIPTION_WORDS)
|
|
print(f"total {total}/{LISTING_WORDS} words in {len(found)} skills")
|
|
for name, n in sorted(found.items(), key=lambda item: -item[1])[: args.top]:
|
|
print(f"{n}\t{name}")
|
|
for name in over:
|
|
print(f"over {DESCRIPTION_WORDS}: {name}")
|
|
long = sorted(name for name, n in skill_sizes(repo).items() if n > SKILL_WORDS)
|
|
for name in long:
|
|
print(f"SKILL.md over {SKILL_WORDS}: {name}")
|
|
return 1 if total > LISTING_WORDS or over or long else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|