From 10253c2d7a4ca772bf3c0a224fd50e08021509fc Mon Sep 17 00:00:00 2001 From: kryptobaseddev Date: Thu, 21 May 2026 14:31:46 -0700 Subject: [PATCH 1/3] =?UTF-8?q?feat(T9960):=20ct-skill-validator=20becomes?= =?UTF-8?q?=20internal-only=20=E2=80=94=20disable-model-invocation=20+=20s?= =?UTF-8?q?cript=20reshuffle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marks ct-skill-validator as `disable-model-invocation: true` and `allowed-tools: Bash(python *)` so it never auto-triggers from regular agent loops. The validator is internal CLEO development tooling and should not surface in user skill profiles or the published `@cleocode/skills` bundle (npm-ignore wiring lands in T9960 follow-up). Script reshuffle: - Renamed `evals/evals.json` -> `quality_evals.json`, `eval_set.json` -> `trigger_queries.json` (separates A/B quality scoring from trigger-discovery eval queries) - Deleted `scripts/check_manifest.py` and Python __pycache__ artifacts - Added `scripts/_skill_finder.py` (shared skill-locator helper) - Added `scripts/run_quality_eval.py` (separated quality-eval entry point) - Expanded `scripts/check_depth.py` and `scripts/validate.py` Prep work for SG-CLEO-SKILLS-V2 (T9799) — ct-skill-validator becomes the internal backbone for the upcoming Skill Drift Check CI gate (epic T9960). --- .../skills/skills/ct-skill-validator/SKILL.md | 97 +++++--- .../evals/{evals.json => quality_evals.json} | 2 +- .../{eval_set.json => trigger_queries.json} | 0 .../references/validation-rules.md | 103 ++++++--- .../ct-skill-validator/scripts/__init__.py | 0 .../__pycache__/audit_body.cpython-314.pyc | Bin 11180 -> 0 bytes .../check_ecosystem.cpython-314.pyc | Bin 7962 -> 0 bytes ...generate_validation_report.cpython-314.pyc | Bin 32910 -> 0 bytes .../__pycache__/validate.cpython-314.pyc | Bin 20194 -> 0 bytes .../scripts/_skill_finder.py | 211 ++++++++++++++++++ .../ct-skill-validator/scripts/audit_body.py | 15 +- .../ct-skill-validator/scripts/check_depth.py | 115 +++++++++- .../scripts/check_manifest.py | 172 -------------- .../scripts/generate_validation_report.py | 2 +- .../scripts/run_quality_eval.py | 110 +++++++++ .../ct-skill-validator/scripts/validate.py | 96 +++++++- 16 files changed, 674 insertions(+), 249 deletions(-) rename packages/skills/skills/ct-skill-validator/evals/{evals.json => quality_evals.json} (97%) rename packages/skills/skills/ct-skill-validator/evals/{eval_set.json => trigger_queries.json} (100%) delete mode 100644 packages/skills/skills/ct-skill-validator/scripts/__init__.py delete mode 100644 packages/skills/skills/ct-skill-validator/scripts/__pycache__/audit_body.cpython-314.pyc delete mode 100644 packages/skills/skills/ct-skill-validator/scripts/__pycache__/check_ecosystem.cpython-314.pyc delete mode 100644 packages/skills/skills/ct-skill-validator/scripts/__pycache__/generate_validation_report.cpython-314.pyc delete mode 100644 packages/skills/skills/ct-skill-validator/scripts/__pycache__/validate.cpython-314.pyc create mode 100644 packages/skills/skills/ct-skill-validator/scripts/_skill_finder.py delete mode 100644 packages/skills/skills/ct-skill-validator/scripts/check_manifest.py create mode 100644 packages/skills/skills/ct-skill-validator/scripts/run_quality_eval.py diff --git a/packages/skills/skills/ct-skill-validator/SKILL.md b/packages/skills/skills/ct-skill-validator/SKILL.md index f765f6bb6..b5dea4e8c 100644 --- a/packages/skills/skills/ct-skill-validator/SKILL.md +++ b/packages/skills/skills/ct-skill-validator/SKILL.md @@ -34,14 +34,18 @@ python ${CLAUDE_SKILL_DIR}/scripts/validate.py --json # Deep body quality audit (optional, run alongside validate.py): python ${CLAUDE_SKILL_DIR}/scripts/audit_body.py -# Manifest alignment check: -python ${CLAUDE_SKILL_DIR}/scripts/check_manifest.py +# Manifest alignment check: bundled into validate.py Tier 4. Use: +# python validate.py --manifest --dispatch-config # Progressive-disclosure depth check (T9684 — CI gate): python ${CLAUDE_SKILL_DIR}/scripts/check_depth.py # Repo-wide depth sweep: python ${CLAUDE_SKILL_DIR}/scripts/check_depth.py --all + +# Allowlist audit (CI / cron — exit 1 on findings): +python ${CLAUDE_SKILL_DIR}/scripts/check_depth.py --audit-allowlist +python ${CLAUDE_SKILL_DIR}/scripts/check_depth.py --audit-allowlist --json ``` **Depth rule (T9684):** A skill PASSES when ANY of: @@ -54,6 +58,13 @@ Pre-existing stubs are allowlisted with follow-up task IDs in `scripts/check_depth.py::ALLOWLIST`. Gold-standard skills: `ct-orchestrator` (9 refs) and `ct-skill-creator` (7 refs). +**Allowlist hygiene:** every entry carries `last_reviewed: YYYY-MM-DD HH:MM:SS`. +`check_depth.py` runs a silent background audit on every invocation and emits +WARNs to stderr for malformed or stale (> 30 days) entries. Use +`--audit-allowlist` for an explicit pass that exits 1 on any finding — +suitable for a CI cron job. The threshold is tunable via +`ALLOWLIST_STALE_DAYS` at the top of `check_depth.py`. + The depth check runs on every PR touching `packages/skills/skills/**` via `.github/workflows/skills-depth-check.yml`. @@ -119,40 +130,68 @@ Repeat until verdict is `PASS` or `PASS_WITH_WARNINGS`. WARN is acceptable; ERRO ## Phase 3: Quality A/B Eval Tests whether the skill actually improves agent output quality vs. no skill context. -Uses the eval infrastructure from ct-skill-creator. +Phase 3 is **delegated** — `ct-skill-validator` does static analysis; runtime +quality evals live in a dedicated skill (`skill-evaluator` preferred, +`ct-skill-creator` as legacy fallback). + +> **Scope boundary:** `ct-skill-validator` is *static* — it checks structure, +> frontmatter, body, manifest, depth, ecosystem fit. For deep runtime A/B +> benchmarking, regression detection, and auto-improvement, the dispatcher +> below routes to `skill-evaluator`, which owns that workflow end-to-end. + +The two eval files in `evals/` serve different purposes: +- `evals/trigger_queries.json` — trigger queries (does the description activate correctly?) +- `evals/quality_evals.json` — output-quality scenarios (does the validator produce the right report?) + +### Dispatch (no hardcoded cross-skill paths) + +`scripts/run_quality_eval.py` uses `_skill_finder.py` to dynamically locate +the eval skill at runtime. It searches: + +1. `$SKILL_FINDER_PATH` (colon-separated override) +2. Direct sibling of this skill +3. `/../../skills//` (CLEO / awesome-skills layouts) +4. Walk-up ancestors + their project-shaped children (cross-project) +5. `~/.claude/skills//` + +Show what would be used (without running anything): +```bash +python ${CLAUDE_SKILL_DIR}/scripts/run_quality_eval.py --list +``` **Trigger accuracy** — does the skill description trigger correctly? ```bash -python ${CLAUDE_SKILL_DIR}/../ct-skill-creator/scripts/run_eval.py \ - --eval-set ${CLAUDE_SKILL_DIR}/evals/eval_set.json \ - --skill-path ${CLAUDE_SKILL_DIR} +python ${CLAUDE_SKILL_DIR}/scripts/run_quality_eval.py \ + --trigger --evals ${CLAUDE_SKILL_DIR}/evals/trigger_queries.json ``` -**Optimize description** (if trigger accuracy < 80%): +**Quality eval** (with/without skill A/B + grading + blind comparison): ```bash -python ${CLAUDE_SKILL_DIR}/../ct-skill-creator/scripts/run_loop.py \ - --eval-set ${CLAUDE_SKILL_DIR}/evals/eval_set.json \ - --skill-path ${CLAUDE_SKILL_DIR} \ - --model claude-sonnet-4-6 \ - --max-iterations 5 +python ${CLAUDE_SKILL_DIR}/scripts/run_quality_eval.py \ + --runs 3 --executor api \ + --evals ${CLAUDE_SKILL_DIR}/evals/quality_evals.json ``` -`run_loop.py` opens a live HTML accuracy report in the browser automatically. - -**Quality eval** (with/without skill A/B): -1. Spawn two agents in the SAME turn: one WITH skill context loaded, one WITHOUT (baseline) -2. Give both the same task prompt from [evals/evals.json](evals/evals.json) -3. Grade each with the grader agent → `grading.json`: - `${CLAUDE_SKILL_DIR}/../ct-skill-creator/agents/grader.md` -4. Blind A/B comparison with the comparator agent → `comparison.json`: - `${CLAUDE_SKILL_DIR}/../ct-skill-creator/agents/comparator.md` -5. Post-hoc analysis with the analyzer agent → `analysis.json`: - `${CLAUDE_SKILL_DIR}/../ct-skill-creator/agents/analyzer.md` -6. Serve the full eval review: - `python ${CLAUDE_SKILL_DIR}/../ct-skill-creator/eval-viewer/generate_review.py ` - (Opens browser at localhost:3117) - -See [references/validation-rules.md](references/validation-rules.md) and -`${CLAUDE_SKILL_DIR}/../ct-skill-creator/references/schemas.md` for JSON output schemas. + +When `skill-evaluator` is the resolved target, the wrapper drives its full +loop: generate → run → grade → aggregate → analyze → detect-regression → +propose. See `skill-evaluator/SKILL.md` for the workflow it actually +executes. + +When `ct-skill-creator` is the resolved fallback, the wrapper invokes its +`run_eval.py` with the same arguments translated to its CLI shape. + +### Manual A/B (if you want to drive runs yourself) + +If you need direct control of how runs are spawned (e.g. inside a real +Claude Code session with subagent isolation), invoke the resolved eval +skill's scripts directly — locate them with: + +```bash +EVAL_SKILL=$(python ${CLAUDE_SKILL_DIR}/scripts/_skill_finder.py skill-evaluator) +``` + +then drive that skill's documented workflow without any further hardcoded +paths in this file. --- diff --git a/packages/skills/skills/ct-skill-validator/evals/evals.json b/packages/skills/skills/ct-skill-validator/evals/quality_evals.json similarity index 97% rename from packages/skills/skills/ct-skill-validator/evals/evals.json rename to packages/skills/skills/ct-skill-validator/evals/quality_evals.json index 6837a978d..5b329e8e1 100644 --- a/packages/skills/skills/ct-skill-validator/evals/evals.json +++ b/packages/skills/skills/ct-skill-validator/evals/quality_evals.json @@ -42,7 +42,7 @@ "prompt": "Run the manifest alignment check for ct-skill-validator against the CLEO manifest", "expected_output": "Manifest alignment results showing whether ct-skill-validator is registered correctly in manifest.json and dispatch-config.json", "expectations": [ - "Claude passes --manifest to validate.py or check_manifest.py", + "Claude passes --manifest to validate.py (Tier 4 check)", "The manifest.json path is correctly resolved", "The output shows Tier 4 CLEO Integration results", "Claude reports whether the skill is found in manifest.json" diff --git a/packages/skills/skills/ct-skill-validator/evals/eval_set.json b/packages/skills/skills/ct-skill-validator/evals/trigger_queries.json similarity index 100% rename from packages/skills/skills/ct-skill-validator/evals/eval_set.json rename to packages/skills/skills/ct-skill-validator/evals/trigger_queries.json diff --git a/packages/skills/skills/ct-skill-validator/references/validation-rules.md b/packages/skills/skills/ct-skill-validator/references/validation-rules.md index 0aad4a53d..964d6d8e3 100644 --- a/packages/skills/skills/ct-skill-validator/references/validation-rules.md +++ b/packages/skills/skills/ct-skill-validator/references/validation-rules.md @@ -1,10 +1,10 @@ -# CLEO Skill Validator v2 — Validation Rules +# CLEO Skill Validator — Validation Rules Complete rule reference for the 5-tier validation system. ## Overview -The CLEO Skill Validator v2 enforces compliance across five tiers of increasing depth: +The CLEO Skill Validator enforces compliance across five tiers of increasing depth: 1. **Structure** — Does the skill have the required files and valid frontmatter? 2. **Frontmatter Quality** — Are all frontmatter fields correct, well-formed, and non-contradictory? @@ -16,42 +16,79 @@ Tiers 1-3 run on every validation. Tiers 4-5 are opt-in via CLI flags. ## Allowed vs Forbidden Fields -### V2_STANDARD (allowed in SKILL.md frontmatter) +### Allowed in SKILL.md frontmatter + +Two groups: **agentskills.io spec fields** (the open standard) and +**Claude Code harness extensions** (honored by the runtime but not part of +the open spec). + +#### From the agentskills.io spec | Field | Type | Required | Description | |-------|------|----------|-------------| -| `name` | string | Yes | Skill identifier, hyphen-case, max 64 chars | +| `name` | string | Yes | Skill identifier, hyphen-case, max 64 chars, must match parent directory name | | `description` | string | Yes | What the skill does and when to use it, max 1024 chars | +| `license` | string | No | License name or reference to a bundled LICENSE file | +| `compatibility` | string | No | Environment requirements (max 500 chars). Only include when the skill needs specific runtime, packages, or network access | +| `metadata` | dict | No | Map from string keys to string values for additional metadata not defined by the spec | +| `allowed-tools` | string or list | No | Tools pre-approved without per-use prompts (experimental in spec) | + +##### Recommended `metadata` sub-keys (string values per spec) + +The agentskills.io spec defines `metadata` as "a map from string keys to string +values". Use it for authorship and version info that the spec doesn't define +fields for: + +| Sub-key | Convention | Example | +|---------|-----------|---------| +| `author` | Author name or org | `author: example-org` | +| `version` | Skill version (always quoted as string) | `version: "1.0.0"` | +| `last_updated` | ISO timestamp `YYYY-MM-DD HH:MM:SS` (always quoted) | `last_updated: "2026-05-21 14:00:18"` | +| `related` | Related skills | `related: skill-creator, skill-evaluator` | +| `spec` | Spec the skill claims to follow | `spec: https://agentskills.io/specification.md` | + +The validator emits a WARN when `metadata` is present without any of +`author`, `version`, or `last_updated`. Numeric values like `version: 1.0` are +flagged — the spec requires string values, so quote them: `version: "1.0"`. + +#### Claude Code harness extensions + +These are honored by the Claude Code runtime but are NOT part of the +agentskills.io open spec. Skills targeting other agent runtimes should +either omit them or document the dependency in `compatibility`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| | `argument-hint` | string | No | Shown in autocomplete, max 100 chars | | `disable-model-invocation` | boolean | No | Prevent model from auto-invoking | | `user-invocable` | boolean | No | Whether skill appears as slash command | -| `allowed-tools` | string or list | No | Tools pre-approved without per-use prompts | | `model` | string | No | Override model for this skill | | `context` | string | No | Must be "fork" if present | | `agent` | string | No | Subagent type (Explore, Plan, etc.) | | `hooks` | dict | No | Skill-scoped lifecycle hooks | -| `license` | string | No | License identifier | -### CLEO_ONLY (forbidden in SKILL.md, belongs in manifest.json) +### CLEO-only fields (forbidden in SKILL.md; belong in `manifest-entry.json`) + +These fields hold CLEO-specific structured data that the Claude runtime +doesn't read. They live in `manifest-entry.json` so they don't bloat +the SKILL.md frontmatter or violate the agentskills.io spec. | Field | Destination | |-------|-------------| -| `version` | manifest.json | -| `tier` | manifest.json | -| `core` | manifest.json | -| `category` | manifest.json | -| `protocol` | manifest.json | -| `dependencies` | manifest.json | -| `sharedResources` | manifest.json | -| `compatibility` | manifest.json | -| `token_budget` | manifest.json | -| `capabilities` | manifest.json | -| `constraints` | manifest.json | -| `metadata` | manifest.json | -| `tags` | manifest.json | -| `triggers` | manifest.json | -| `mvi_scope` | manifest.json | -| `requires_tiers` | manifest.json | +| `version` | manifest-entry.json (or `metadata.version` in SKILL.md) | +| `tier` | manifest-entry.json | +| `core` | manifest-entry.json | +| `category` | manifest-entry.json | +| `protocol` | manifest-entry.json | +| `dependencies` | manifest-entry.json | +| `sharedResources` | manifest-entry.json | +| `token_budget` | manifest-entry.json | +| `capabilities` | manifest-entry.json | +| `constraints` | manifest-entry.json | +| `tags` | manifest-entry.json | +| `triggers` | manifest-entry.json | +| `mvi_scope` | manifest-entry.json | +| `requires_tiers` | manifest-entry.json | ## Tier 1 — Structure Rules @@ -94,14 +131,21 @@ Tiers 1-3 run on every validation. Tiers 4-5 are opt-in via CLI flags. | T2-024 | `model` is a string if present | ERROR | Use model ID string | | T2-025 | `agent` is a string if present | ERROR | Use agent type string | | T2-026 | `hooks` is a dict if present | ERROR | Use key: value structure | +| T2-027 | `compatibility` is a string if present | ERROR | Use a plain string value | +| T2-028 | `compatibility` is 500 characters or fewer (agentskills.io spec) | ERROR | Shorten or move detail to `references/` | +| T2-029 | `metadata` is a dict if present (agentskills.io spec) | ERROR | Use key: value structure | +| T2-030 | `metadata` keys are all strings (agentskills.io spec) | ERROR | Quote non-string keys | +| T2-031 | `metadata` values are all strings (agentskills.io spec) | WARN | Quote numeric versions: `version: "1.0"` | +| T2-032 | `metadata` includes at least one of: author, version, last_updated | WARN | Add recommended traceability keys | +| T2-033 | `metadata.last_updated` (and `metadata.last_reviewed` if present) match `YYYY-MM-DD HH:MM:SS` | WARN | Use precise timestamp format, e.g. `"2026-05-21 14:00:18"` | ## Tier 3 — Body Quality Rules | Rule ID | Check | Severity | Fix | |---------|-------|----------|-----| | T3-001 | Body is present (non-empty content after frontmatter) | WARN | Add content below the closing `---` | -| T3-002 | Body is under 600 lines | ERROR | Split into sub-documents or trim | -| T3-003 | Body is under 400 lines | WARN | Consider trimming for token efficiency | +| T3-002 | Body is under 600 lines (hard cap) | ERROR | Split into sub-documents or trim | +| T3-003 | Body is under 500 lines (agentskills.io spec recommendation) | WARN | Move detail to `references/` for progressive disclosure | | T3-004 | No placeholder text (`[Required:`, `TODO`, `REPLACE`, `[Add content`, `FIXME`, `TBD`) | WARN (per match) | Replace placeholders with real content | | T3-005 | Bodies over 200 lines have `## ` section headers | WARN | Add section structure for readability | | T3-006 | File references (`references/`, `scripts/`) point to existing files | WARN | Create the referenced file or fix the path | @@ -156,9 +200,16 @@ Tiers 1-3 run on every validation. Tiers 4-5 are opt-in via CLI flags. | T2-024 | 2 | `model` is string | ERROR | | T2-025 | 2 | `agent` is string | ERROR | | T2-026 | 2 | `hooks` is dict | ERROR | +| T2-027 | 2 | `compatibility` is string | ERROR | +| T2-028 | 2 | `compatibility` max 500 chars | ERROR | +| T2-029 | 2 | `metadata` is dict | ERROR | +| T2-030 | 2 | `metadata` keys are strings | ERROR | +| T2-031 | 2 | `metadata` values are strings | WARN | +| T2-032 | 2 | `metadata` has author/version/last_updated | WARN | +| T2-033 | 2 | `metadata` timestamp keys match `YYYY-MM-DD HH:MM:SS` | WARN | | T3-001 | 3 | Body present | WARN | | T3-002 | 3 | Body under 600 lines | ERROR | -| T3-003 | 3 | Body under 400 lines | WARN | +| T3-003 | 3 | Body under 500 lines (spec) | WARN | | T3-004 | 3 | No placeholder text | WARN | | T3-005 | 3 | Section headers in long bodies | WARN | | T3-006 | 3 | File references exist | WARN | diff --git a/packages/skills/skills/ct-skill-validator/scripts/__init__.py b/packages/skills/skills/ct-skill-validator/scripts/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/skills/skills/ct-skill-validator/scripts/__pycache__/audit_body.cpython-314.pyc b/packages/skills/skills/ct-skill-validator/scripts/__pycache__/audit_body.cpython-314.pyc deleted file mode 100644 index 78af56b5435e4c47476a22b42ce342b7c6f480f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11180 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 x1f)BKgrNGc^e-b;PNN`?6}3D{dI4a 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 MJ=7thT_O2@0gEu)oB#j- diff --git a/packages/skills/skills/ct-skill-validator/scripts/__pycache__/generate_validation_report.cpython-314.pyc b/packages/skills/skills/ct-skill-validator/scripts/__pycache__/generate_validation_report.cpython-314.pyc deleted file mode 100644 index 5de0e3932a13c8241232d385d7c1cefd10b745b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32910 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 YXPt~JXxIEiXZ*d+&$X0pEh+Z@2huU#2LJ#7 diff --git a/packages/skills/skills/ct-skill-validator/scripts/__pycache__/validate.cpython-314.pyc b/packages/skills/skills/ct-skill-validator/scripts/__pycache__/validate.cpython-314.pyc deleted file mode 100644 index dafd49459fe1f704e8687721a5163b5e2ef01d0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20194 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/SKILL.md` + OR may itself be the skill directory if its basename matches. + + 2. `~/.claude/skill-finder-paths.txt` — user config; newline-separated root + directories to search. Useful when the install lives in `~/.claude/skills/` + but the project skills live under `/mnt/projects//skills/`. + Lines starting with `#` are ignored. + + 3. Sibling of the calling skill — `/..//SKILL.md`. + Most common in awesome-skills layouts where every skill is a peer. + + 4. Two-up + `skills//` — `/../../skills//SKILL.md`. + Matches cleocode `packages/skills/skills/...` and `repo/skills/...`. + + 5. Walk up from the calling skill looking for a `skills//SKILL.md` + on the ancestor chain AND its project-shaped children (depth-limited). + + 6. `~/.claude/skills//SKILL.md` — installed Claude Code skill. + +The caller is determined from `Path(__file__).resolve()` — so a skill +running its own script can find a peer without knowing absolute paths. + +Use: + from _skill_finder import find_skill + evaluator = find_skill("skill-evaluator") + if evaluator is None: + sys.exit("skill-evaluator not found") + +CLI: + python _skill_finder.py # prints path, exits 1 if not found + python _skill_finder.py --json +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path +from typing import Iterator + + +MAX_WALK_DEPTH = 6 + + +def _candidates(name: str, caller_skill_dir: Path) -> Iterator[Path]: + """Yield candidate paths in priority order. Each may or may not exist.""" + + # 1. Explicit override via env var (colon-separated paths) + env = os.environ.get("SKILL_FINDER_PATH", "") + for entry in (e for e in env.split(":") if e): + p = Path(entry).expanduser() + # If the entry IS the skill directory, accept it; else treat as parent + if p.is_dir() and p.name == name and (p / "SKILL.md").exists(): + yield p + continue + yield p / name + # Also probe common "skills/" subdirs under each user-configured root + yield p / "skills" / name + yield p / "packages" / "skills" / "skills" / name + + # 2. User config file: ~/.claude/skill-finder-paths.txt + cfg = Path.home() / ".claude" / "skill-finder-paths.txt" + if cfg.exists(): + try: + for line in cfg.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + root = Path(line).expanduser() + yield root / name + yield root / "skills" / name + yield root / "packages" / "skills" / "skills" / name + # Also peek into project-shaped children of the root + if root.is_dir(): + try: + for child in root.iterdir(): + if not child.is_dir() or child.name.startswith("."): + continue + if (child / "skills").is_dir(): + yield child / "skills" / name + if (child / "packages" / "skills" / "skills").is_dir(): + yield child / "packages" / "skills" / "skills" / name + except PermissionError: + pass + except (OSError, UnicodeDecodeError): + pass + + # 3. Direct sibling of the caller + yield caller_skill_dir.parent / name + + # 4. Two-up + skills// + yield caller_skill_dir.parent.parent / "skills" / name + + # 4. Walk up looking for skills// on the ancestor chain. + # At each ancestor `cur`, probe: + # - cur/skills/ (standard layout) + # - cur/packages/skills/skills/ (CLEO layout) + # AND iterate the *children* of cur (which are potential project + # roots) and probe: + # - /skills/ + # - /packages/skills/skills/ + # This finds skills under sibling project roots — e.g. when the + # caller is in /mnt/projects/cleocode/.../ct-skill-validator/ and + # the target is in /mnt/projects/proxmox/skills//. The walk + # eventually reaches the common ancestor /mnt/projects/ where both + # cleocode and proxmox are children. Bounded by MAX_SIBLINGS per + # level to keep this fast on populated roots. + cur = caller_skill_dir.parent + for _ in range(MAX_WALK_DEPTH): + yield cur / "skills" / name + yield cur / "packages" / "skills" / "skills" / name + # Iterate children, pre-filtering to project-shaped dirs only. + # A project-shaped dir is one that has a `skills/` or + # `packages/skills/skills/` subdir on disk. The pre-filter is one + # extra stat per child but eliminates the vast majority of irrelevant + # candidates before they enter the search, keeping a fully-populated + # ancestor (e.g. 200+ peer projects) sub-second. + if cur.is_dir(): + try: + for child in cur.iterdir(): + if not child.is_dir() or child.name.startswith("."): + continue + if (child / "skills").is_dir(): + yield child / "skills" / name + if (child / "packages" / "skills" / "skills").is_dir(): + yield child / "packages" / "skills" / "skills" / name + except PermissionError: + pass + parent = cur.parent + if parent == cur: + break + cur = parent + + # 5. Installed Claude Code skill + yield Path.home() / ".claude" / "skills" / name + + +def caller_skill_dir() -> Path: + """Resolve the directory of the skill that invoked this helper. + + Assumes this file lives at `/scripts/_skill_finder.py`. If moved, + the caller can pass `caller_skill_dir` to `find_skill()` explicitly. + """ + return Path(__file__).resolve().parent.parent + + +def find_skill(name: str, *, caller: Path | None = None) -> Path | None: + """Return the resolved path of the named skill, or None if not found. + + A skill is considered found if `/SKILL.md` exists. + """ + caller_dir = (caller or caller_skill_dir()).resolve() + seen: set[Path] = set() + for cand in _candidates(name, caller_dir): + try: + resolved = cand.resolve() + except (OSError, RuntimeError): + continue + if resolved in seen: + continue + seen.add(resolved) + if (resolved / "SKILL.md").exists(): + return resolved + return None + + +def find_first(names: list[str], *, caller: Path | None = None) -> tuple[str, Path] | None: + """Return the (name, path) of the first found skill from a preference list. + + Lets callers say "prefer skill-evaluator, fall back to ct-skill-creator" + without hardcoding either path. + """ + for n in names: + p = find_skill(n, caller=caller) + if p is not None: + return n, p + return None + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("name", help="skill name to resolve") + ap.add_argument("--caller", help="override the calling-skill directory") + ap.add_argument("--json", action="store_true", help="emit JSON instead of bare path") + args = ap.parse_args() + + caller = Path(args.caller).expanduser().resolve() if args.caller else None + path = find_skill(args.name, caller=caller) + + if args.json: + print(json.dumps({"name": args.name, "found": path is not None, + "path": str(path) if path else None}, indent=2)) + else: + if path is None: + print(f"error: skill '{args.name}' not found on search path", file=sys.stderr) + return 1 + print(str(path)) + return 0 if path is not None else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/packages/skills/skills/ct-skill-validator/scripts/audit_body.py b/packages/skills/skills/ct-skill-validator/scripts/audit_body.py index 6b8115938..3bfb43a6b 100644 --- a/packages/skills/skills/ct-skill-validator/scripts/audit_body.py +++ b/packages/skills/skills/ct-skill-validator/scripts/audit_body.py @@ -60,9 +60,13 @@ def ok(section, msg): total_lines = len(body_lines) # ── Section analysis ──────────────────────────────────────────────── - h1_headers = re.findall(r"^# .+", body, re.MULTILINE) - h2_headers = re.findall(r"^## .+", body, re.MULTILINE) - h3_headers = re.findall(r"^### .+", body, re.MULTILINE) + # Strip fenced code blocks first — '#' inside bash blocks are comments, + # not markdown headings, and matching them produces false positives. + body_no_fences = re.sub(r"```[\s\S]*?```", "", body) + body_no_fences_lines = body_no_fences.split("\n") + h1_headers = re.findall(r"^# .+", body_no_fences, re.MULTILINE) + h2_headers = re.findall(r"^## .+", body_no_fences, re.MULTILINE) + h3_headers = re.findall(r"^### .+", body_no_fences, re.MULTILINE) total_sections = len(h2_headers) + len(h3_headers) if len(h1_headers) > 1: @@ -70,7 +74,7 @@ def ok(section, msg): first_h2_line = None first_h3_line = None - for i, line in enumerate(body_lines): + for i, line in enumerate(body_no_fences_lines): if first_h2_line is None and line.startswith("## "): first_h2_line = i if first_h3_line is None and line.startswith("### "): @@ -146,7 +150,8 @@ def ok(section, msg): ok("placeholder-scan", "No placeholder text found") # ── Duplicate headings ────────────────────────────────────────────── - all_headings = re.findall(r"^(#{1,6} .+)", body, re.MULTILINE) + # Reuse code-fence-stripped body for the same reason as section analysis. + all_headings = re.findall(r"^(#{1,6} .+)", body_no_fences, re.MULTILINE) seen: dict[str, bool] = {} dup_found = False for heading in all_headings: diff --git a/packages/skills/skills/ct-skill-validator/scripts/check_depth.py b/packages/skills/skills/ct-skill-validator/scripts/check_depth.py index 18fcb7a4c..e7fb5976d 100644 --- a/packages/skills/skills/ct-skill-validator/scripts/check_depth.py +++ b/packages/skills/skills/ct-skill-validator/scripts/check_depth.py @@ -33,6 +33,7 @@ import re import json import argparse +import datetime from pathlib import Path @@ -42,14 +43,28 @@ MIN_REF_FILES = 3 GOLD_STANDARDS = ("ct-orchestrator", "ct-skill-creator") +# Cadence for the allowlist audit — entries older than this are flagged stale. +ALLOWLIST_STALE_DAYS = 30 +LAST_REVIEWED_RE = re.compile(r"last_reviewed:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})") + # Allowlist — pre-existing stub skills exempted at T9567 (E-SKILLS-DEPTH-BACKFILL). -# Each entry MUST have a follow-up task ID. Remove the entry once that task lands -# a depth backfill. New entries require owner approval — do not add silently. +# Each entry MUST have a follow-up task ID. Remove the entry once that task +# lands a depth backfill. New entries require owner approval — do not add +# silently. +# +# AUDIT CADENCE: review every release cycle (or every 30 days, whichever +# comes first). Each entry below carries `last_reviewed` — if that timestamp +# is older than the cadence, run `python check_depth.py ` for each +# allowlisted skill, decide keep / remove, and bump `last_reviewed` to a +# fresh `date '+%Y-%m-%d %H:%M:%S'` value. See ALLOWLIST_STALE_DAYS above +# for the actual threshold the audit enforces. +# +# Format: skill-name -> "task-id: rationale | last_reviewed: YYYY-MM-DD HH:MM:SS" ALLOWLIST: dict[str, str] = { - "ct-codebase-mapper": "T9567-followup: pre-existing; depth-backfill deferred", - "ct-master-tac": "T9567-followup: pre-existing; depth-backfill deferred", - "ct-memory": "T9567-followup: pre-existing; depth-backfill deferred", - "ct-stickynote": "T9567-followup: ephemeral note skill; minimal-by-design", + "ct-codebase-mapper": "T9567-followup: pre-existing; depth-backfill deferred | last_reviewed: 2026-05-21 14:00:18", + "ct-master-tac": "T9567-followup: pre-existing; depth-backfill deferred | last_reviewed: 2026-05-21 14:00:18", + "ct-memory": "T9567-followup: pre-existing; depth-backfill deferred | last_reviewed: 2026-05-21 14:00:18", + "ct-stickynote": "T9567-followup: ephemeral note skill; minimal-by-design | last_reviewed: 2026-05-21 14:00:18", } @@ -218,6 +233,60 @@ def _print_report(report: dict) -> None: print(f" * {r}") +def audit_allowlist( + *, + now: datetime.datetime | None = None, + stale_days: int = ALLOWLIST_STALE_DAYS, +) -> list[dict]: + """Audit the ALLOWLIST for malformed or stale `last_reviewed` stamps. + + Returns a list of finding dicts: each has `skill`, `severity` (WARN), + `message`, and (when parseable) `age_days`. An empty list means every + allowlist entry has a well-formed, fresh stamp. + + `last_reviewed:` must match `YYYY-MM-DD HH:MM:SS`. Stamps older than + `stale_days` are flagged for re-audit. + """ + now = now or datetime.datetime.now() + findings: list[dict] = [] + for skill, rationale in ALLOWLIST.items(): + m = LAST_REVIEWED_RE.search(rationale) + if not m: + findings.append({ + "skill": skill, "severity": "WARN", + "message": "missing or malformed 'last_reviewed: YYYY-MM-DD HH:MM:SS' stamp", + }) + continue + stamp = m.group(1) + try: + ts = datetime.datetime.strptime(stamp, "%Y-%m-%d %H:%M:%S") + except ValueError as e: + findings.append({ + "skill": skill, "severity": "WARN", + "message": f"invalid timestamp '{stamp}': {e}", + }) + continue + age_days = (now - ts).days + if age_days > stale_days: + findings.append({ + "skill": skill, "severity": "WARN", "age_days": age_days, + "message": ( + f"stale: last_reviewed was {age_days}d ago " + f"(cadence: {stale_days}d). Audit and bump the stamp." + ), + }) + return findings + + +def _print_allowlist_audit(findings: list[dict], *, stream=sys.stderr) -> None: + if not findings: + return + print("=== allowlist audit ===", file=stream) + for f in findings: + print(f" ⚠️ {f['skill']}: {f['message']}", file=stream) + print(file=stream) + + def walk_all_skills(root: Path) -> list[Path]: """Find all skill directories under packages/skills/skills/. Skips manifest.json, _shared/, and any dir without SKILL.md.""" @@ -246,8 +315,42 @@ def main() -> int: help="Walk every skill under packages/skills/skills/", ) parser.add_argument("--json", action="store_true", help="Output JSON instead of text") + parser.add_argument( + "--audit-allowlist", action="store_true", + help=( + "Audit ALLOWLIST entries for malformed or stale `last_reviewed` " + "stamps and exit. Exits 1 if any finding is reported." + ), + ) args = parser.parse_args() + # Standalone audit mode — for CI / cron use. + if args.audit_allowlist: + findings = audit_allowlist() + if args.json: + print(json.dumps({ + "stale_days_cadence": ALLOWLIST_STALE_DAYS, + "findings": findings, + "passed": len(findings) == 0, + }, indent=2)) + else: + if findings: + _print_allowlist_audit(findings, stream=sys.stdout) + print(f"=== SUMMARY ===\nFindings: {len(findings)}\nResult: FAIL", + file=sys.stdout) + else: + print("=== allowlist audit ===", file=sys.stdout) + print(f" ✅ all {len(ALLOWLIST)} entries have fresh stamps " + f"(cadence: {ALLOWLIST_STALE_DAYS}d)", file=sys.stdout) + print(f"\n=== SUMMARY ===\nFindings: 0\nResult: PASS", + file=sys.stdout) + return 1 if findings else 0 + + # Background audit — runs on every invocation, silent when clean, emits + # to stderr so --json output on stdout stays parseable. + if not args.json: + _print_allowlist_audit(audit_allowlist()) + arg_path = Path(args.skill_dir).resolve() manifest = Path(args.manifest).resolve() if args.manifest else None diff --git a/packages/skills/skills/ct-skill-validator/scripts/check_manifest.py b/packages/skills/skills/ct-skill-validator/scripts/check_manifest.py deleted file mode 100644 index 023d2ec12..000000000 --- a/packages/skills/skills/ct-skill-validator/scripts/check_manifest.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -""" -CLEO manifest alignment checker. -Usage: check_manifest.py [--dispatch-config dispatch-config.json] -""" -import sys -import json -import re -import yaml -import argparse -from pathlib import Path - -MANIFEST_REQUIRED_FIELDS = [ - "name", "version", "description", "path", "status", - "tier", "token_budget", "capabilities", "constraints", -] - - -def check_manifest(skill_path, manifest_path, dispatch_config_path=None): - """Check manifest alignment for a skill.""" - skill_dir = Path(skill_path).resolve() - skill_name = skill_dir.name - manifest_file = Path(manifest_path).resolve() - errors = 0 - warnings = 0 - - def error(msg): - nonlocal errors - errors += 1 - print(f" \u274c ERROR: {msg}") - - def warn(msg): - nonlocal warnings - warnings += 1 - print(f" \u26a0\ufe0f WARN: {msg}") - - def ok(msg): - print(f" \u2705 {msg}") - - print(f"\n=== CLEO Manifest Check: {skill_name} ===\n") - - # ── Read SKILL.md frontmatter ─────────────────────────────────────── - print("--- SKILL.md ---") - skill_md = skill_dir / "SKILL.md" - if not skill_md.exists(): - error("SKILL.md does not exist") - _print_summary(errors, warnings) - return errors - - raw_content = skill_md.read_text(encoding="utf-8") - fm_match = re.match(r"^---\n(.*?)\n---", raw_content, re.DOTALL) - if not fm_match: - error("Could not extract frontmatter from SKILL.md") - _print_summary(errors, warnings) - return errors - - try: - frontmatter = yaml.safe_load(fm_match.group(1)) - except yaml.YAMLError as e: - error(f"Frontmatter YAML parse error: {e}") - _print_summary(errors, warnings) - return errors - - if not isinstance(frontmatter, dict): - error("Frontmatter is not a dict") - _print_summary(errors, warnings) - return errors - - fm_name = frontmatter.get("name", skill_name) - ok(f"SKILL.md frontmatter read (name: '{fm_name}')") - - # ── Read manifest.json ────────────────────────────────────────────── - print("\n--- Manifest ---") - if not manifest_file.exists(): - error(f"Manifest file not found: {manifest_path}") - _print_summary(errors, warnings) - return errors - - try: - manifest_data = json.loads(manifest_file.read_text(encoding="utf-8")) - except json.JSONDecodeError as e: - error(f"Manifest is not valid JSON: {e}") - _print_summary(errors, warnings) - return errors - - ok("Manifest parsed successfully") - - skills_list = manifest_data.get("skills", []) - matching = [s for s in skills_list if s.get("name") == fm_name] - - if not matching: - error(f"Skill '{fm_name}' not found in manifest.json skills[] array") - _print_summary(errors, warnings) - return errors - - ok(f"Skill '{fm_name}' found in manifest.json") - entry = matching[0] - - # Check required fields - print("\n--- Required Fields ---") - missing_fields = [] - for field in MANIFEST_REQUIRED_FIELDS: - if field not in entry: - warn(f"Missing required field: '{field}'") - missing_fields.append(field) - else: - ok(f"'{field}' present") - - # ── Dispatch config check ─────────────────────────────────────────── - if dispatch_config_path: - print("\n--- Dispatch Config ---") - dc_file = Path(dispatch_config_path).resolve() - if not dc_file.exists(): - error(f"Dispatch config not found: {dispatch_config_path}") - else: - try: - dc_data = json.loads(dc_file.read_text(encoding="utf-8")) - except json.JSONDecodeError as e: - error(f"Dispatch config is not valid JSON: {e}") - dc_data = None - - if dc_data is not None: - overrides = dc_data.get("skill_overrides", {}) - if fm_name not in overrides: - warn(f"Skill '{fm_name}' not found in dispatch-config.json skill_overrides") - else: - ok(f"Skill '{fm_name}' found in dispatch-config.json") - - _print_summary(errors, warnings) - return errors - - -def _print_summary(errors, warnings): - """Print the check summary.""" - print(f"\n=== SUMMARY ===") - print(f"Errors: {errors}") - print(f"Warnings: {warnings}") - - if errors > 0: - print(f"Result: FAIL") - elif warnings > 0: - print(f"Result: PASS (with warnings)") - else: - print(f"Result: PASS") - - -def main(): - parser = argparse.ArgumentParser( - description="CLEO manifest alignment checker" - ) - parser.add_argument("skill_dir", help="Path to the skill directory") - parser.add_argument("manifest", help="Path to manifest.json") - parser.add_argument("--dispatch-config", help="Path to dispatch-config.json") - - args = parser.parse_args() - - skill_path = Path(args.skill_dir).resolve() - if not skill_path.is_dir(): - print(f"Error: '{args.skill_dir}' is not a directory", file=sys.stderr) - sys.exit(1) - - error_count = check_manifest( - skill_path, - args.manifest, - dispatch_config_path=args.dispatch_config, - ) - - sys.exit(1 if error_count > 0 else 0) - - -if __name__ == "__main__": - main() diff --git a/packages/skills/skills/ct-skill-validator/scripts/generate_validation_report.py b/packages/skills/skills/ct-skill-validator/scripts/generate_validation_report.py index c29b623d2..fa6e30961 100644 --- a/packages/skills/skills/ct-skill-validator/scripts/generate_validation_report.py +++ b/packages/skills/skills/ct-skill-validator/scripts/generate_validation_report.py @@ -254,7 +254,7 @@ def generate_html( eco_html = _ecosystem_section(ecosystem) if ecosystem else '
CLEO Ecosystem Compliance — Not yet run
Run: python check_ecosystem.py <skill-dir> | ecosystem-checker agent | save to ecosystem-check.json
' - grading_html = _grading_section(grading) if grading else '
Quality Eval — Grading not yet run
Run A/B eval using ct-skill-creator agents/grader.md then pass --grading grading.json
' + grading_html = _grading_section(grading) if grading else '
Quality Eval — Grading not yet run
Run: python scripts/run_quality_eval.py <skill-dir> (dispatches dynamically to skill-evaluator), then pass --grading grading.json
' comparison_html = _comparison_section(comparison) if comparison else "" diff --git a/packages/skills/skills/ct-skill-validator/scripts/run_quality_eval.py b/packages/skills/skills/ct-skill-validator/scripts/run_quality_eval.py new file mode 100644 index 000000000..7343363a3 --- /dev/null +++ b/packages/skills/skills/ct-skill-validator/scripts/run_quality_eval.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Phase 3 dispatcher — delegates runtime quality eval to a dedicated skill. + +Prefers `skill-evaluator` (the dedicated quality-eval skill) and falls back +to `ct-skill-creator` (legacy eval infrastructure) when skill-evaluator +isn't found. Uses `_skill_finder.py` to resolve the target dynamically — +no hardcoded cross-skill paths. + +Usage: + run_quality_eval.py # full quality eval + run_quality_eval.py --trigger # trigger-accuracy only + run_quality_eval.py --runs 3 --executor api + run_quality_eval.py --list # show what's reachable + +Exit codes: + 0 — eval ran (or was prepared, in --executor print mode) + 1 — target eval skill not found on the search path + 2 — eval script inside the target skill exited non-zero +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from _skill_finder import find_first # noqa: E402 + + +# Preference order — first found wins. Lets the user opt-in to a different +# eval skill via $SKILL_FINDER_PATH without code changes. +PREFERENCE = ["skill-evaluator", "ct-skill-creator"] + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("skill_dir", nargs="?", help="path to the skill being evaluated") + ap.add_argument("--trigger", action="store_true", + help="run trigger-accuracy eval only (description_eval.py)") + ap.add_argument("--runs", type=int, default=3, help="repeated runs per case") + ap.add_argument("--executor", default=None, + help="executor for the eval (passes through to the target script)") + ap.add_argument("--list", action="store_true", + help="show what eval skill would be used and exit") + ap.add_argument("--evals", default=None, help="explicit evals.json path") + args, extra = ap.parse_known_args() + + resolved = find_first(PREFERENCE) + if resolved is None: + print( + f"error: none of {PREFERENCE} were found on the search path. " + f"Set SKILL_FINDER_PATH or install one of them.", + file=sys.stderr, + ) + return 1 + eval_skill_name, eval_skill_path = resolved + + if args.list: + print(f"will use: {eval_skill_name} at {eval_skill_path}") + return 0 + + if not args.skill_dir: + ap.error("skill_dir is required unless --list is given") + + target_skill = Path(args.skill_dir).expanduser().resolve() + if not (target_skill / "SKILL.md").exists(): + print(f"error: '{args.skill_dir}' is not a skill directory (no SKILL.md)", + file=sys.stderr) + return 1 + + # Pick the right script per target eval skill + if eval_skill_name == "skill-evaluator": + if args.trigger: + script = eval_skill_path / "scripts" / "description_eval.py" + cmd = ["python3", str(script), "--skill", str(target_skill), "--runs", str(args.runs)] + else: + script = eval_skill_path / "scripts" / "run_eval.py" + cmd = ["python3", str(script), "--skill", str(target_skill), "--runs", str(args.runs)] + if args.evals: + cmd += ["--evals", args.evals] + if args.executor: + cmd += ["--executor", args.executor] + else: + # ct-skill-creator legacy paths + if args.trigger: + script = eval_skill_path / "scripts" / "run_eval.py" + cmd = ["python3", str(script), "--skill-path", str(target_skill)] + if args.evals: + cmd += ["--eval-set", args.evals] + else: + script = eval_skill_path / "scripts" / "run_eval.py" + cmd = ["python3", str(script), "--skill-path", str(target_skill)] + if args.evals: + cmd += ["--eval-set", args.evals] + + if not script.exists(): + print(f"error: expected script not found: {script}", file=sys.stderr) + return 1 + + cmd += extra # pass any additional flags straight through + print(f"[run_quality_eval] dispatching to {eval_skill_name}: {' '.join(cmd)}", + file=sys.stderr) + rc = subprocess.run(cmd).returncode + return 0 if rc == 0 else 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/packages/skills/skills/ct-skill-validator/scripts/validate.py b/packages/skills/skills/ct-skill-validator/scripts/validate.py index 461ec0a2b..c009d9b18 100644 --- a/packages/skills/skills/ct-skill-validator/scripts/validate.py +++ b/packages/skills/skills/ct-skill-validator/scripts/validate.py @@ -17,16 +17,44 @@ import argparse from pathlib import Path -V2_STANDARD = { - "name", "description", "argument-hint", "disable-model-invocation", - "user-invocable", "allowed-tools", "model", "context", "agent", "hooks", - "license", +# Frontmatter fields allowed directly in SKILL.md. +# +# Sources (in order of authority): +# 1. agentskills.io spec — name, description, license, compatibility, +# metadata, allowed-tools +# https://agentskills.io/specification.md +# 2. Claude Code harness extensions — argument-hint, disable-model-invocation, +# user-invocable, model, context, agent, hooks +# (honored by the runtime but not part of the open spec) +# +# Anything in CLEO_ONLY is reserved for manifest-entry.json — the validator +# rejects those at SKILL.md top level. +# +# Per-spec author conventions for `metadata` (sub-keys, all strings): +# author, version, last_updated, related, spec +RECOMMENDED_METADATA_KEYS = {"author", "version", "last_updated"} + +# Timestamp keys inside `metadata` whose value should match the precision +# convention. The agentskills.io spec doesn't pin a format, but we enforce +# YYYY-MM-DD HH:MM:SS for both `last_updated` (metadata convention) and +# `last_reviewed` (audit/allowlist convention) so audit trails stay precise. +TIMESTAMP_METADATA_KEYS = ("last_updated", "last_reviewed") +TIMESTAMP_FORMAT_RE = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$") + +SPEC_FRONTMATTER = { + "name", "description", "license", "compatibility", "metadata", "allowed-tools", } +HARNESS_EXTENSIONS = { + "argument-hint", "disable-model-invocation", "user-invocable", + "model", "context", "agent", "hooks", +} +ALLOWED_FRONTMATTER = SPEC_FRONTMATTER | HARNESS_EXTENSIONS + CLEO_ONLY = { "version", "tier", "core", "category", "protocol", - "dependencies", "sharedResources", "compatibility", + "dependencies", "sharedResources", "token_budget", "capabilities", "constraints", - "metadata", "tags", "triggers", "mvi_scope", "requires_tiers", + "tags", "triggers", "mvi_scope", "requires_tiers", } MANIFEST_REQUIRED_FIELDS = [ @@ -213,6 +241,53 @@ def ok(tier, msg): if hooks_val is not None and not isinstance(hooks_val, dict): error(tier, "'hooks' must be a dict") + # compatibility checks (agentskills.io spec: max 500 chars) + compat_val = frontmatter.get("compatibility") + if compat_val is not None: + if not isinstance(compat_val, str): + error(tier, "'compatibility' must be a string") + elif len(compat_val) > 500: + error(tier, f"'compatibility' exceeds 500 characters (got: {len(compat_val)})") + else: + ok(tier, "'compatibility' is valid") + + # metadata checks (agentskills.io spec: map from string keys to string values) + metadata_val = frontmatter.get("metadata") + if metadata_val is not None: + if not isinstance(metadata_val, dict): + error(tier, "'metadata' must be a dict (map from string keys to string values)") + else: + non_string_keys = [k for k in metadata_val if not isinstance(k, str)] + if non_string_keys: + error(tier, f"'metadata' keys must all be strings (got non-string: {non_string_keys[:3]})") + non_string_vals = [k for k, v in metadata_val.items() if not isinstance(v, str)] + if non_string_vals: + warn(tier, ( + f"'metadata' values should be strings per agentskills.io spec; " + f"non-string keys: {non_string_vals[:3]} (quote numeric versions: \"1.0\" not 1.0)" + )) + present_recommended = RECOMMENDED_METADATA_KEYS & set(metadata_val.keys()) + if not present_recommended: + warn(tier, ( + "'metadata' present but contains none of the recommended keys " + f"({', '.join(sorted(RECOMMENDED_METADATA_KEYS))}); consider adding for traceability" + )) + else: + ok(tier, f"'metadata' has recommended key(s): {', '.join(sorted(present_recommended))}") + # Timestamp format check on convention keys (precision: YYYY-MM-DD HH:MM:SS). + # The spec is silent on format; this is our audit-precision convention. + for ts_key in TIMESTAMP_METADATA_KEYS: + ts_val = metadata_val.get(ts_key) + if ts_val is None or not isinstance(ts_val, str): + continue + if not TIMESTAMP_FORMAT_RE.match(ts_val): + warn(tier, ( + f"'metadata.{ts_key}' should match 'YYYY-MM-DD HH:MM:SS' " + f"(got: {ts_val[:40]!r}); use a value like \"2026-05-21 14:00:18\"" + )) + else: + ok(tier, f"'metadata.{ts_key}' has valid timestamp format") + # ── Tier 3 — Body Quality ─────────────────────────────────────────── tier = 3 @@ -228,10 +303,13 @@ def ok(tier, msg): body_lines = body.split("\n") line_count = len(body_lines) + # Thresholds aligned with agentskills.io spec recommendation + # ("Keep your main SKILL.md under 500 lines"). 600 is the hard cap; + # 500 is the soft cap from the spec. if line_count >= 600: - error(tier, f"Body is too long: {line_count} lines (max 600)") - elif line_count >= 400: - warn(tier, f"Body is getting long: {line_count} lines (warn threshold: 400)") + error(tier, f"Body is too long: {line_count} lines (hard cap 600)") + elif line_count >= 500: + warn(tier, f"Body exceeds spec recommendation: {line_count} lines (keep under 500)") else: ok(tier, f"Body length OK ({line_count} lines)") From 8e599b49763e1f7f78afe2bb24c35e459c764ee7 Mon Sep 17 00:00:00 2001 From: kryptobaseddev Date: Thu, 21 May 2026 14:42:26 -0700 Subject: [PATCH 2/3] docs(T9960): add 'Skill Maintenance Discipline' section to AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the drift gap that allowed ct-release-orchestrator to describe the deleted 'cleo release ship' monolith for weeks after T9540 shipped the 4-verb plan/open/reconcile model. New mandatory section in AGENTS.md after "Canonical Docs Routing": - Coverage map at packages/skills/internal/skill-coverage.yml (internal-only, .npmignore'd, NEVER ships with @cleocode/skills) - Per-skill SKILL.md frontmatter metadata block (version, lastReviewed, stability) — DOES ship as documentation - Git pre-commit hook (auto-regenerates skills.json from frontmatter) - Git pre-push hook (runs drift-check.mjs against staged diff) - CI gate 'Skill Drift Check' fails PRs touching covered paths without matching SKILL.md update - Trailer 'Skill-Drift-Acknowledged: ' override works for tier-1 skills; REJECTED for tier-0 (ct-cleo, ct-orchestrator, ct-task-executor, ct-dev-workflow, ct-documentor, CLEO-INJECTION.md) - ct-skill-validator stays internal-only (disable-model-invocation: true, excluded from npm bundle via .npmignore) This commit only adds the discipline section. Tooling (skill-coverage.yml, drift-check.mjs, git hooks, CI workflow) lands in subsequent T9960 tasks per the saga T9799 plan. --- AGENTS.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 8fab5fee7..212e964ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,6 +61,80 @@ If you genuinely need a doc-kind not yet listed: 2. Add a routing entry to `.cleo/canon.yml`. 3. Re-run `pnpm --filter @cleocode/cleo run build` and the gate stays green. +## Skill Maintenance Discipline (Saga T9799 · Epic T9960) + +Canonical `ct-*` skills under `packages/skills/skills/` describe how CLEO +works to every spawned agent. When core systems change but the skill text +does not, agents act on stale instructions. The T9540 release-system +rewrite is the canonical example — `ct-release-orchestrator` still +described the deleted `cleo release ship` monolith for weeks. + +**Rule**: when you edit a path declared in the coverage map, you MUST +update the corresponding skill in the same PR — or acknowledge the +deferral explicitly. + +### Coverage map (internal-only — never ships) + +`packages/skills/internal/skill-coverage.yml` maps each canonical skill +to the code paths it documents. The file is listed in +`packages/skills/.npmignore` so it never lands in the published +`@cleocode/skills` bundle. Sibling tooling under `packages/skills/internal/` +(drift-check.mjs, the git-hook runners) is excluded the same way. + +### Shipped per-skill metadata (in SKILL.md frontmatter) + +Every canonical SKILL.md MUST carry a `metadata:` block. These fields +DO ship — they are documentation, not enforcement, and they let +consumers and the curator daemon reason about freshness: + +```yaml +metadata: + version: 2.0.0 # bump on every material change + lastReviewed: 2026-05-21 # ISO date — set by the human/agent who reviewed + stability: stable # experimental | stable | deprecated +``` + +### Enforcement (T9960 — in progress) + +- **Pre-commit hook**: regenerates `packages/skills/skills.json` from + SKILL.md frontmatter. Drift between frontmatter and `skills.json` fails + the hook. +- **CI gate `Skill Drift Check`**: scans the PR diff against the coverage + map. If a covered path is touched but the matching SKILL.md is not, the + PR fails with `E_SKILL_DRIFT_UNACKNOWLEDGED`. +- **Trailer override**: a commit trailer + `Skill-Drift-Acknowledged: ` bypasses the gate AND auto-files a + sentient follow-up task for retroactive skill update. +- **Tier-0 skills get NO override** — `ct-cleo`, `ct-orchestrator`, + `ct-task-executor`, `ct-dev-workflow`, `ct-documentor`, and + `CLEO-INJECTION.md` must be kept current in the same PR. The trailer + is rejected for these. + +### Tier-0 core skills (strict — no drift tolerated) + +These define the agent protocol surface. Edit the matching code path, +edit the skill in the same PR. Period. + +- `ct-cleo` — CLI protocol + session lifecycle +- `ct-orchestrator` — spawn/delegation contract +- `ct-task-executor` — worker contract +- `ct-dev-workflow` — commit / branch / release flow +- `ct-documentor` — docs SSoT routing +- `CLEO-INJECTION.md` (template, not a skill folder) — protocol injected + into every spawn prompt + +### Tier-1 LOOM-stage skills (trailer override permitted) + +One per LOOM stage in `packages/core/src/validation/protocols/`. Same +rule applies; trailer override is allowed for non-blocking deferrals. + +### Internal-only validator + +`ct-skill-validator` ships with `disable-model-invocation: true` and is +listed in `packages/skills/.npmignore` so it never reaches consumers. +It is the developer-side toolchain that drives the drift check, depth +audit, and quality evals. + ## Worktree Location (ADR-055 · Saga T9800 · Decision D009) ALL git worktrees provisioned for agent tasks MUST live under the canonical From d0deaf1a78bd12fd2058c764e37cc9ffbe03f1cb Mon Sep 17 00:00:00 2001 From: kryptobaseddev Date: Thu, 21 May 2026 16:14:18 -0700 Subject: [PATCH 3/3] fix(T9965): add regression test for docs fetch returning populated slug/type/kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guards against regression where `cleo docs fetch ` and `cleo docs fetch ` returned all-null payload ({slug:null, type:null, kind:null}). Two new vitest round-trip cases added to docs-slug-type-project.test.ts: - T9965 RT-1: add → list → fetch by slug → asserts metadata.slug/type/kind non-null - T9965 RT-2: add → fetch by uuid → asserts metadata.slug/type/kind non-null Also adds worktree-sparse-install aliases in packages/cleo/vitest.config.ts for @a2a-js/sdk, @iarna/toml, and js-yaml which are not hoisted to root node_modules in sparse pnpm worktree installs. Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/docs-slug-type-project.test.ts | 108 ++++++++++++++++++ packages/cleo/vitest.config.ts | 16 +++ 2 files changed, 124 insertions(+) diff --git a/packages/cleo/src/dispatch/domains/__tests__/docs-slug-type-project.test.ts b/packages/cleo/src/dispatch/domains/__tests__/docs-slug-type-project.test.ts index d7f3523bb..baebbae3c 100644 --- a/packages/cleo/src/dispatch/domains/__tests__/docs-slug-type-project.test.ts +++ b/packages/cleo/src/dispatch/domains/__tests__/docs-slug-type-project.test.ts @@ -290,4 +290,112 @@ describe('docs dispatch — slug/type/project (T9636/T9637/T9638)', () => { const fetched = await handler.query('fetch', { attachmentRef: prefix }); expect(fetched.success).toBe(true); }); + + // ──────────────────────────────────────────────────────────────────────── + // T9965 — docs.fetch round-trip: slug + type + kind non-null regression + // + // Guards against regression where docs.fetch returned all-null payload + // ({slug:null, type:null, kind:null}) when resolving by slug or uuid. + // ──────────────────────────────────────────────────────────────────────── + + it('T9965 RT-1: add → list → fetch by slug: slug/type/kind all non-null in metadata', async () => { + const handler = new DocsHandler(); + + // AC1/AC2: docs add with slug + type + const add = await handler.mutate('add', { + ownerId: 'T9965', + file: fixtureA, + slug: 'sg-arch-solid-session-1-handoff', + type: 'handoff', + }); + expect(add.success, `docs.add failed: ${JSON.stringify(add.error)}`).toBe(true); + const addData = add.data as { attachmentId: string; slug: string; type: string }; + expect(addData.slug).toBe('sg-arch-solid-session-1-handoff'); + expect(addData.type).toBe('handoff'); + + // Verify list shows slug + type + const list = await handler.query('list', { task: 'T9965' }); + expect(list.success).toBe(true); + const listData = list.data as { + attachments: Array<{ id: string; slug?: string; type?: string; kind: string }>; + }; + expect(listData.attachments).toHaveLength(1); + const listedRow = listData.attachments[0]; + expect(listedRow?.slug).toBe('sg-arch-solid-session-1-handoff'); + expect(listedRow?.type).toBe('handoff'); + expect(listedRow?.kind).toBe('local-file'); + + // AC1: fetch by slug returns populated slug/type/kind + const fetchBySlug = await handler.query('fetch', { + attachmentRef: 'sg-arch-solid-session-1-handoff', + }); + expect( + fetchBySlug.success, + `docs.fetch by slug failed: ${JSON.stringify(fetchBySlug.error)}`, + ).toBe(true); + const slugData = fetchBySlug.data as { + metadata: { id: string; slug?: string; type?: string; kind: string }; + sizeBytes: number; + inlined: boolean; + }; + expect(slugData.metadata.id).toBe(addData.attachmentId); + // Regression assertions: these were null before T9965 fix + expect(slugData.metadata.slug, 'metadata.slug must not be null after fetch by slug').toBe( + 'sg-arch-solid-session-1-handoff', + ); + expect(slugData.metadata.type, 'metadata.type must not be null after fetch by slug').toBe( + 'handoff', + ); + expect(slugData.metadata.kind, 'metadata.kind must not be null after fetch by slug').toBe( + 'local-file', + ); + expect(slugData.sizeBytes).toBeGreaterThan(0); + }); + + it('T9965 RT-2: add → fetch by uuid: slug/type/kind all non-null in metadata', async () => { + const handler = new DocsHandler(); + + // AC2: docs add with slug + type, then fetch by UUID + const add = await handler.mutate('add', { + ownerId: 'T9965-uuid', + file: fixtureB, + slug: 'handoff-for-uuid-test', + type: 'handoff', + }); + expect(add.success, `docs.add failed: ${JSON.stringify(add.error)}`).toBe(true); + const addData = add.data as { + attachmentId: string; + sha256: string; + slug: string; + type: string; + }; + expect(addData.slug).toBe('handoff-for-uuid-test'); + expect(addData.type).toBe('handoff'); + + // AC2: fetch by UUID returns populated slug/type/kind + const fetchByUuid = await handler.query('fetch', { + attachmentRef: addData.attachmentId, + }); + expect( + fetchByUuid.success, + `docs.fetch by uuid failed: ${JSON.stringify(fetchByUuid.error)}`, + ).toBe(true); + const uuidData = fetchByUuid.data as { + metadata: { id: string; slug?: string; type?: string; kind: string }; + sizeBytes: number; + inlined: boolean; + }; + expect(uuidData.metadata.id).toBe(addData.attachmentId); + // Regression assertions: these were null before T9965 fix + expect(uuidData.metadata.slug, 'metadata.slug must not be null after fetch by uuid').toBe( + 'handoff-for-uuid-test', + ); + expect(uuidData.metadata.type, 'metadata.type must not be null after fetch by uuid').toBe( + 'handoff', + ); + expect(uuidData.metadata.kind, 'metadata.kind must not be null after fetch by uuid').toBe( + 'local-file', + ); + expect(uuidData.sizeBytes).toBeGreaterThan(0); + }); }); diff --git a/packages/cleo/vitest.config.ts b/packages/cleo/vitest.config.ts index 5dc9f6864..6e51ae213 100644 --- a/packages/cleo/vitest.config.ts +++ b/packages/cleo/vitest.config.ts @@ -262,6 +262,22 @@ export default defineConfig({ ).pathname, '@cleocode/core': new URL('../../packages/core/src/index.ts', import.meta.url).pathname, '@cleocode/lafs': new URL('../../packages/lafs/src/index.ts', import.meta.url).pathname, + // T9965: @a2a-js/sdk is a dep of @cleocode/lafs; in worktrees it resolves + // through lafs/node_modules rather than root node_modules. + '@a2a-js/sdk': new URL( + '../../packages/lafs/node_modules/@a2a-js/sdk/dist/index.js', + import.meta.url, + ).pathname, + // T9965: js-yaml + @iarna/toml are deps of @cleocode/caamp; in worktrees + // they resolve through caamp/node_modules rather than root node_modules. + '@iarna/toml': new URL( + '../../packages/caamp/node_modules/@iarna/toml/toml.js', + import.meta.url, + ).pathname, + 'js-yaml': new URL( + '../../packages/caamp/node_modules/js-yaml/index.js', + import.meta.url, + ).pathname, // T1113: nexus code sub-path exports — legacy dist-path imports used in nexus.ts '@cleocode/nexus/dist/src/code/unfold.js': new URL( '../../packages/nexus/src/code/unfold.ts',