@cleocode/skills 2026.5.84 → 2026.5.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/ct-adr-recorder/SKILL.md +74 -0
- package/skills/ct-adr-recorder/__tests__/skill-adr-recorder.test.ts +65 -0
- package/skills/ct-docs-lookup/SKILL.md +116 -1
- package/skills/ct-docs-lookup/references/ctx7-workflow.md +198 -0
- package/skills/ct-docs-lookup/references/library-id-resolution.md +217 -0
- package/skills/ct-docs-lookup/references/version-specific-docs.md +220 -0
- package/skills/ct-docs-review/SKILL.md +133 -1
- package/skills/ct-docs-review/__tests__/skill-docs-review.test.ts +53 -0
- package/skills/ct-docs-review/references/inline-comment-patterns.md +268 -0
- package/skills/ct-docs-review/references/pr-review-mode.md +270 -0
- package/skills/ct-docs-review/references/style-violations.md +341 -0
- package/skills/ct-docs-write/SKILL.md +157 -1
- package/skills/ct-docs-write/__tests__/skill-docs-write.test.ts +55 -0
- package/skills/ct-docs-write/references/audience-targeting.md +305 -0
- package/skills/ct-docs-write/references/cleo-style-guide.md +234 -0
- package/skills/ct-docs-write/references/markdown-patterns.md +329 -0
- package/skills/ct-documentor/SKILL.md +11 -0
- package/skills/ct-documentor/references/anti-patterns.md +216 -0
- package/skills/ct-documentor/references/chain-orchestration.md +194 -0
- package/skills/ct-documentor/references/doc-types-and-templates.md +301 -0
- package/skills/ct-documentor/references/style-coordination.md +195 -0
- package/skills/ct-research-agent/SKILL.md +9 -0
- package/skills/ct-research-agent/references/anti-patterns.md +154 -0
- package/skills/ct-research-agent/references/citation-and-evidence.md +140 -0
- package/skills/ct-research-agent/references/source-strategy.md +116 -0
- package/skills/ct-research-agent/references/triggers-and-routing.md +93 -0
- package/skills/ct-skill-validator/SKILL.md +19 -0
- package/skills/ct-skill-validator/scripts/check_depth.py +306 -0
- package/skills/ct-spec-writer/SKILL.md +71 -1
- package/skills/ct-spec-writer/__tests__/skill-spec-writer.test.ts +60 -0
- package/skills/ct-spec-writer/references/anti-patterns.md +176 -0
- package/skills/ct-spec-writer/references/rfc2119-language.md +138 -0
- package/skills/ct-spec-writer/references/spec-templates.md +233 -0
- package/skills/ct-spec-writer/references/traceability-matrix.md +145 -0
- package/skills/ct-task-executor/SKILL.md +10 -0
- package/skills/ct-task-executor/references/acceptance-criteria-mapping.md +163 -0
- package/skills/ct-task-executor/references/anti-patterns.md +201 -0
- package/skills/ct-task-executor/references/common-failures.md +193 -0
- package/skills/ct-task-executor/references/evidence-and-gates.md +179 -0
- package/skills/ct-task-executor/references/implementation-patterns.md +160 -0
- package/skills/ct-validator/SKILL.md +9 -0
- package/skills/ct-validator/references/anti-patterns.md +194 -0
- package/skills/ct-validator/references/compliance-reports.md +199 -0
- package/skills/ct-validator/references/schema-checking.md +191 -0
- package/skills/ct-validator/references/validation-modes.md +185 -0
- package/skills/manifest.json +46 -8
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Triggers and Routing
|
|
2
|
+
|
|
3
|
+
When to load `ct-research-agent`, what tasks it owns, and how it hands off
|
|
4
|
+
to neighboring skills in the RCASD-IVTR+C pipeline. The skill operates as the
|
|
5
|
+
`research` protocol within the orchestrator's stage taxonomy and is the first
|
|
6
|
+
skill invoked when an Epic enters its research lifecycle stage.
|
|
7
|
+
|
|
8
|
+
## Primary Triggers
|
|
9
|
+
|
|
10
|
+
Load this skill when the orchestrator (or the user) requests any of the
|
|
11
|
+
following — the dispatch matrix in `manifest.json` enumerates the canonical
|
|
12
|
+
keyword set.
|
|
13
|
+
|
|
14
|
+
| Trigger phrase | Routing decision |
|
|
15
|
+
|----------------|------------------|
|
|
16
|
+
| "research <topic>" | Direct invocation — proceed with multi-source pull |
|
|
17
|
+
| "investigate <area>" | Same as research; emphasize codebase + web blend |
|
|
18
|
+
| "explore options for <decision>" | Treat as research → ct-consensus-voter |
|
|
19
|
+
| "what does the literature say about X" | Web + Context7 lookup, no codebase |
|
|
20
|
+
| "audit current <subsystem> implementation" | Codebase-only research; skip web |
|
|
21
|
+
| "compare libraries for <use-case>" | Web + Context7 with `--research` flag |
|
|
22
|
+
| "due-diligence on <vendor/tool>" | Web + reputation + license scan |
|
|
23
|
+
|
|
24
|
+
The orchestrator's `cleo orchestrate spawn` prompt always carries a
|
|
25
|
+
`stage: research` hint when the task's `pipelineStage` field equals
|
|
26
|
+
`research`. The skill SHOULD honor that hint and SHOULD NOT advance the
|
|
27
|
+
pipeline stage on its own — that is the orchestrator's responsibility once
|
|
28
|
+
the research manifest entry is appended.
|
|
29
|
+
|
|
30
|
+
## Anti-Triggers (Do NOT Load)
|
|
31
|
+
|
|
32
|
+
The skill MUST NOT be loaded for the following requests because they belong
|
|
33
|
+
to sibling skills that have narrower context budgets and stricter contracts.
|
|
34
|
+
|
|
35
|
+
| Request | Correct skill |
|
|
36
|
+
|---------|---------------|
|
|
37
|
+
| "fix this failing test" | `ct-task-executor` |
|
|
38
|
+
| "write the spec for X" | `ct-spec-writer` |
|
|
39
|
+
| "validate this implementation against the spec" | `ct-validator` |
|
|
40
|
+
| "decompose this epic into tasks" | `ct-epic-architect` |
|
|
41
|
+
| "look up Next.js 15 middleware API" | `ct-docs-lookup` (single-library fetch) |
|
|
42
|
+
| "decide between A and B" (no investigation needed) | `ct-consensus-voter` |
|
|
43
|
+
| "explain this function" | (no skill — direct read suffices) |
|
|
44
|
+
|
|
45
|
+
A useful heuristic: if the user already knows the answer and just wants it
|
|
46
|
+
written down, route to `ct-spec-writer` or `ct-docs-write`. Research is for
|
|
47
|
+
when the answer is not yet known.
|
|
48
|
+
|
|
49
|
+
## Routing to Downstream Skills
|
|
50
|
+
|
|
51
|
+
Research outputs typically feed one of these next stages. The manifest
|
|
52
|
+
`chains_to` array enumerates the legal handoffs.
|
|
53
|
+
|
|
54
|
+
1. **`ct-spec-writer`** — when findings produce testable requirements.
|
|
55
|
+
Pass the research file path and the synthesized recommendations as
|
|
56
|
+
spec input. Use this when the task description contains "spec",
|
|
57
|
+
"contract", "RFC", or "protocol".
|
|
58
|
+
2. **`ct-epic-architect`** — when findings change the scope estimate of
|
|
59
|
+
an Epic. The architect re-decomposes based on the new evidence.
|
|
60
|
+
3. **`ct-consensus-voter`** — when research surfaces two-or-more viable
|
|
61
|
+
options with comparable trade-offs. The voter resolves the choice
|
|
62
|
+
under HITL when confidence < 0.5.
|
|
63
|
+
4. **`ct-task-executor`** — only when the research itself contains an
|
|
64
|
+
actionable next step that does not require a spec (e.g. "bump the
|
|
65
|
+
library version" or "delete deprecated path"). Rare.
|
|
66
|
+
|
|
67
|
+
## Decision Tree
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Is the answer already known and just needs documentation?
|
|
71
|
+
├── YES → ct-docs-write
|
|
72
|
+
└── NO → continue
|
|
73
|
+
│
|
|
74
|
+
Is this a single-library API question?
|
|
75
|
+
├── YES → ct-docs-lookup
|
|
76
|
+
└── NO → ct-research-agent (this skill)
|
|
77
|
+
│
|
|
78
|
+
After research, do findings produce requirements?
|
|
79
|
+
├── YES → ct-spec-writer next
|
|
80
|
+
└── NO → continue
|
|
81
|
+
│
|
|
82
|
+
Do findings produce a decision between options?
|
|
83
|
+
├── YES → ct-consensus-voter next
|
|
84
|
+
└── NO → return manifest only, no chain
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Stage Hint Compliance
|
|
88
|
+
|
|
89
|
+
The orchestrator's `pipeline_manifest` table tracks per-task stage
|
|
90
|
+
progression. The research skill MUST append exactly one entry with
|
|
91
|
+
`agent_type: "research"` and MUST NOT mutate stage fields directly. If
|
|
92
|
+
findings indicate a stage advance is warranted, the skill SHOULD include
|
|
93
|
+
that suggestion in `needs_followup` so the orchestrator can act on it.
|
|
@@ -36,8 +36,27 @@ python ${CLAUDE_SKILL_DIR}/scripts/audit_body.py <skill-dir>
|
|
|
36
36
|
|
|
37
37
|
# Manifest alignment check:
|
|
38
38
|
python ${CLAUDE_SKILL_DIR}/scripts/check_manifest.py <skill-dir> <manifest.json>
|
|
39
|
+
|
|
40
|
+
# Progressive-disclosure depth check (T9684 — CI gate):
|
|
41
|
+
python ${CLAUDE_SKILL_DIR}/scripts/check_depth.py <skill-dir>
|
|
42
|
+
|
|
43
|
+
# Repo-wide depth sweep:
|
|
44
|
+
python ${CLAUDE_SKILL_DIR}/scripts/check_depth.py <repo-root> --all
|
|
39
45
|
```
|
|
40
46
|
|
|
47
|
+
**Depth rule (T9684):** A skill PASSES when ANY of:
|
|
48
|
+
|
|
49
|
+
- SKILL.md body has ≥ 100 content lines, OR
|
|
50
|
+
- `references/` subdir has ≥ 3 markdown files, OR
|
|
51
|
+
- `manifest.json` `references[]` array enumerates ≥ 3 files (all on disk)
|
|
52
|
+
|
|
53
|
+
Pre-existing stubs are allowlisted with follow-up task IDs in
|
|
54
|
+
`scripts/check_depth.py::ALLOWLIST`. Gold-standard skills:
|
|
55
|
+
`ct-orchestrator` (9 refs) and `ct-skill-creator` (7 refs).
|
|
56
|
+
|
|
57
|
+
The depth check runs on every PR touching `packages/skills/skills/**`
|
|
58
|
+
via `.github/workflows/skills-depth-check.yml`.
|
|
59
|
+
|
|
41
60
|
**Iteration rule**: If errors > 0, fix them in the skill's SKILL.md, re-run `validate.py`.
|
|
42
61
|
Repeat until errors = 0. Do not proceed to Phase 2 while errors remain.
|
|
43
62
|
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CLEO Skill Depth Check — progressive-disclosure-depth rule (T9684).
|
|
4
|
+
|
|
5
|
+
Fails when a skill's SKILL.md is below the depth threshold AND it has
|
|
6
|
+
no references/ subdir with at least the manifest-declared reference
|
|
7
|
+
files. The gold standard is ct-orchestrator (9 references) and
|
|
8
|
+
ct-skill-creator (7 references); the rule is calibrated to flag
|
|
9
|
+
stubs without forcing every skill to that depth.
|
|
10
|
+
|
|
11
|
+
Rule logic:
|
|
12
|
+
PASS when ANY of:
|
|
13
|
+
- SKILL.md body has >= MIN_BODY_LINES content lines
|
|
14
|
+
- references/ subdir exists with >= MIN_REF_FILES files
|
|
15
|
+
- manifest.json references[] array populated with file paths that
|
|
16
|
+
all exist on disk
|
|
17
|
+
|
|
18
|
+
FAIL when:
|
|
19
|
+
- SKILL.md body < MIN_BODY_LINES lines AND
|
|
20
|
+
- references/ missing or has < MIN_REF_FILES files AND
|
|
21
|
+
- manifest.json references[] empty or files missing
|
|
22
|
+
|
|
23
|
+
Error message points at the gold standard and lists expected files
|
|
24
|
+
from the manifest entry (when present) so the fix is obvious.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
check_depth.py <skill-directory>
|
|
28
|
+
check_depth.py <skill-directory> --manifest path/to/manifest.json
|
|
29
|
+
check_depth.py <skill-directory> --all # walk all skills under a root
|
|
30
|
+
check_depth.py <skill-directory> --json
|
|
31
|
+
"""
|
|
32
|
+
import sys
|
|
33
|
+
import re
|
|
34
|
+
import json
|
|
35
|
+
import argparse
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Calibration knobs — set against the post-T9567 state.
|
|
40
|
+
# Adjust here, then re-run the audit to verify all 19 active skills pass.
|
|
41
|
+
MIN_BODY_LINES = 100
|
|
42
|
+
MIN_REF_FILES = 3
|
|
43
|
+
GOLD_STANDARDS = ("ct-orchestrator", "ct-skill-creator")
|
|
44
|
+
|
|
45
|
+
# Allowlist — pre-existing stub skills exempted at T9567 (E-SKILLS-DEPTH-BACKFILL).
|
|
46
|
+
# Each entry MUST have a follow-up task ID. Remove the entry once that task lands
|
|
47
|
+
# a depth backfill. New entries require owner approval — do not add silently.
|
|
48
|
+
ALLOWLIST: dict[str, str] = {
|
|
49
|
+
"ct-codebase-mapper": "T9567-followup: pre-existing; depth-backfill deferred",
|
|
50
|
+
"ct-master-tac": "T9567-followup: pre-existing; depth-backfill deferred",
|
|
51
|
+
"ct-memory": "T9567-followup: pre-existing; depth-backfill deferred",
|
|
52
|
+
"ct-stickynote": "T9567-followup: ephemeral note skill; minimal-by-design",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def count_body_lines(skill_md_path: Path) -> int:
|
|
57
|
+
"""Count content lines in the SKILL.md body (excluding frontmatter)."""
|
|
58
|
+
if not skill_md_path.exists():
|
|
59
|
+
return 0
|
|
60
|
+
raw = skill_md_path.read_text(encoding="utf-8")
|
|
61
|
+
if not raw.startswith("---"):
|
|
62
|
+
# No frontmatter — count whole file
|
|
63
|
+
return len(raw.split("\n"))
|
|
64
|
+
parts = raw.split("---", 2)
|
|
65
|
+
if len(parts) < 3:
|
|
66
|
+
return 0
|
|
67
|
+
body = parts[2].strip()
|
|
68
|
+
if not body:
|
|
69
|
+
return 0
|
|
70
|
+
return len(body.split("\n"))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def manifest_references_for(skill_name: str, manifest_path: Path) -> list[str]:
|
|
74
|
+
"""Return the references array from manifest.json for the given skill,
|
|
75
|
+
or empty list if not present."""
|
|
76
|
+
if not manifest_path.exists():
|
|
77
|
+
return []
|
|
78
|
+
try:
|
|
79
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
80
|
+
except json.JSONDecodeError:
|
|
81
|
+
return []
|
|
82
|
+
for entry in data.get("skills", []):
|
|
83
|
+
if entry.get("name") == skill_name:
|
|
84
|
+
return entry.get("references", []) or []
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def repo_root_of(skill_dir: Path) -> Path:
|
|
89
|
+
"""Walk up from skill_dir to the repo root (heuristic: contains
|
|
90
|
+
`packages/skills/skills/manifest.json`)."""
|
|
91
|
+
cur = skill_dir.resolve()
|
|
92
|
+
for _ in range(10):
|
|
93
|
+
if (cur / "packages" / "skills" / "skills" / "manifest.json").exists():
|
|
94
|
+
return cur
|
|
95
|
+
if cur.parent == cur:
|
|
96
|
+
break
|
|
97
|
+
cur = cur.parent
|
|
98
|
+
return skill_dir # fallback
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_depth(skill_path: Path, manifest_path: Path | None = None) -> tuple[bool, dict]:
|
|
102
|
+
"""Run the depth check on a single skill directory.
|
|
103
|
+
|
|
104
|
+
Returns (passed, report_dict).
|
|
105
|
+
"""
|
|
106
|
+
skill_dir = Path(skill_path).resolve()
|
|
107
|
+
skill_name = skill_dir.name
|
|
108
|
+
skill_md = skill_dir / "SKILL.md"
|
|
109
|
+
refs_dir = skill_dir / "references"
|
|
110
|
+
|
|
111
|
+
report: dict = {
|
|
112
|
+
"skill_name": skill_name,
|
|
113
|
+
"path": str(skill_dir),
|
|
114
|
+
"body_lines": 0,
|
|
115
|
+
"ref_files_on_disk": 0,
|
|
116
|
+
"manifest_references": [],
|
|
117
|
+
"manifest_references_missing": [],
|
|
118
|
+
"thresholds": {
|
|
119
|
+
"min_body_lines": MIN_BODY_LINES,
|
|
120
|
+
"min_ref_files": MIN_REF_FILES,
|
|
121
|
+
},
|
|
122
|
+
"passed": False,
|
|
123
|
+
"reasons": [],
|
|
124
|
+
"remediation": [],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Threshold A: body length
|
|
128
|
+
body_lines = count_body_lines(skill_md)
|
|
129
|
+
report["body_lines"] = body_lines
|
|
130
|
+
body_passes = body_lines >= MIN_BODY_LINES
|
|
131
|
+
|
|
132
|
+
# Threshold B: references/ subdir
|
|
133
|
+
ref_files_on_disk = 0
|
|
134
|
+
if refs_dir.is_dir():
|
|
135
|
+
ref_files_on_disk = len([p for p in refs_dir.iterdir() if p.is_file() and p.suffix == ".md"])
|
|
136
|
+
report["ref_files_on_disk"] = ref_files_on_disk
|
|
137
|
+
refs_dir_passes = ref_files_on_disk >= MIN_REF_FILES
|
|
138
|
+
|
|
139
|
+
# Threshold C: manifest.json references populated and on disk
|
|
140
|
+
manifest_passes = False
|
|
141
|
+
if manifest_path is None:
|
|
142
|
+
# Auto-locate from repo root
|
|
143
|
+
root = repo_root_of(skill_dir)
|
|
144
|
+
manifest_path = root / "packages" / "skills" / "skills" / "manifest.json"
|
|
145
|
+
|
|
146
|
+
if manifest_path.exists():
|
|
147
|
+
manifest_refs = manifest_references_for(skill_name, manifest_path)
|
|
148
|
+
report["manifest_references"] = manifest_refs
|
|
149
|
+
if manifest_refs:
|
|
150
|
+
root = repo_root_of(skill_dir)
|
|
151
|
+
base = root / "packages" / "skills"
|
|
152
|
+
missing = []
|
|
153
|
+
for rel in manifest_refs:
|
|
154
|
+
ref_abs = base / rel
|
|
155
|
+
if not ref_abs.exists():
|
|
156
|
+
# Try relative to skill_dir as fallback
|
|
157
|
+
fallback = skill_dir / Path(rel).relative_to(Path(rel).parts[0]) \
|
|
158
|
+
if Path(rel).parts else None
|
|
159
|
+
if fallback is None or not fallback.exists():
|
|
160
|
+
missing.append(rel)
|
|
161
|
+
report["manifest_references_missing"] = missing
|
|
162
|
+
manifest_passes = (len(manifest_refs) >= MIN_REF_FILES and not missing)
|
|
163
|
+
|
|
164
|
+
# Decide pass/fail — ANY threshold passes ⇒ depth check passes.
|
|
165
|
+
passed = body_passes or refs_dir_passes or manifest_passes
|
|
166
|
+
report["passed"] = passed
|
|
167
|
+
|
|
168
|
+
if body_passes:
|
|
169
|
+
report["reasons"].append(f"body_lines={body_lines} >= {MIN_BODY_LINES}")
|
|
170
|
+
if refs_dir_passes:
|
|
171
|
+
report["reasons"].append(f"references/ has {ref_files_on_disk} files >= {MIN_REF_FILES}")
|
|
172
|
+
if manifest_passes:
|
|
173
|
+
report["reasons"].append(
|
|
174
|
+
f"manifest references[] populated with {len(report['manifest_references'])} files (all on disk)"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Allowlist override — exempted skills pass with a reason captured.
|
|
178
|
+
if not passed and skill_name in ALLOWLIST:
|
|
179
|
+
passed = True
|
|
180
|
+
report["passed"] = True
|
|
181
|
+
report["allowlisted"] = True
|
|
182
|
+
report["allowlist_reason"] = ALLOWLIST[skill_name]
|
|
183
|
+
report["reasons"].append(f"allowlisted: {ALLOWLIST[skill_name]}")
|
|
184
|
+
|
|
185
|
+
if not passed:
|
|
186
|
+
report["reasons"].append("none of the three thresholds met")
|
|
187
|
+
report["remediation"] = [
|
|
188
|
+
f"Expand SKILL.md body to >= {MIN_BODY_LINES} content lines (currently {body_lines}), OR",
|
|
189
|
+
f"Add references/ subdir with >= {MIN_REF_FILES} markdown files (currently {ref_files_on_disk}), OR",
|
|
190
|
+
"Populate manifest.json references[] array for this skill with file paths.",
|
|
191
|
+
f"Gold-standard examples: {', '.join(GOLD_STANDARDS)}.",
|
|
192
|
+
]
|
|
193
|
+
if report["manifest_references_missing"]:
|
|
194
|
+
report["remediation"].append(
|
|
195
|
+
"Manifest references[] lists files that do not exist on disk: "
|
|
196
|
+
+ ", ".join(report["manifest_references_missing"])
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return passed, report
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _print_report(report: dict) -> None:
|
|
203
|
+
"""Print a single skill's depth report."""
|
|
204
|
+
name = report["skill_name"]
|
|
205
|
+
status = "PASS" if report["passed"] else "FAIL"
|
|
206
|
+
icon = "✅" if report["passed"] else "❌"
|
|
207
|
+
print(f"\n{icon} {status} {name}")
|
|
208
|
+
print(f" body_lines={report['body_lines']} (min {MIN_BODY_LINES})")
|
|
209
|
+
print(f" ref_files_on_disk={report['ref_files_on_disk']} (min {MIN_REF_FILES})")
|
|
210
|
+
print(f" manifest_references={len(report['manifest_references'])} files")
|
|
211
|
+
if report["manifest_references_missing"]:
|
|
212
|
+
print(f" manifest_references_missing={report['manifest_references_missing']}")
|
|
213
|
+
for r in report["reasons"]:
|
|
214
|
+
print(f" - {r}")
|
|
215
|
+
if not report["passed"]:
|
|
216
|
+
print(" remediation:")
|
|
217
|
+
for r in report["remediation"]:
|
|
218
|
+
print(f" * {r}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def walk_all_skills(root: Path) -> list[Path]:
|
|
222
|
+
"""Find all skill directories under packages/skills/skills/.
|
|
223
|
+
Skips manifest.json, _shared/, and any dir without SKILL.md."""
|
|
224
|
+
base = root if (root / "SKILL.md").exists() else (root / "packages" / "skills" / "skills")
|
|
225
|
+
if not base.is_dir():
|
|
226
|
+
return []
|
|
227
|
+
skills = []
|
|
228
|
+
for entry in sorted(base.iterdir()):
|
|
229
|
+
if not entry.is_dir():
|
|
230
|
+
continue
|
|
231
|
+
if entry.name.startswith("_") or entry.name.startswith("."):
|
|
232
|
+
continue
|
|
233
|
+
if (entry / "SKILL.md").exists():
|
|
234
|
+
skills.append(entry)
|
|
235
|
+
return skills
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main() -> int:
|
|
239
|
+
parser = argparse.ArgumentParser(
|
|
240
|
+
description="CLEO Skill Depth Check (T9684) — progressive-disclosure-depth"
|
|
241
|
+
)
|
|
242
|
+
parser.add_argument("skill_dir", help="Path to the skill directory (or repo root if --all)")
|
|
243
|
+
parser.add_argument("--manifest", help="Path to manifest.json (auto-located if omitted)")
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
"--all", action="store_true",
|
|
246
|
+
help="Walk every skill under packages/skills/skills/",
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument("--json", action="store_true", help="Output JSON instead of text")
|
|
249
|
+
args = parser.parse_args()
|
|
250
|
+
|
|
251
|
+
arg_path = Path(args.skill_dir).resolve()
|
|
252
|
+
manifest = Path(args.manifest).resolve() if args.manifest else None
|
|
253
|
+
|
|
254
|
+
if args.all:
|
|
255
|
+
skills = walk_all_skills(arg_path)
|
|
256
|
+
if not skills:
|
|
257
|
+
print(f"Error: no skill directories found under {arg_path}", file=sys.stderr)
|
|
258
|
+
return 1
|
|
259
|
+
all_reports = []
|
|
260
|
+
total_fail = 0
|
|
261
|
+
for s in skills:
|
|
262
|
+
passed, report = check_depth(s, manifest)
|
|
263
|
+
all_reports.append(report)
|
|
264
|
+
if not passed:
|
|
265
|
+
total_fail += 1
|
|
266
|
+
if args.json:
|
|
267
|
+
print(json.dumps({
|
|
268
|
+
"summary": {
|
|
269
|
+
"total": len(all_reports),
|
|
270
|
+
"passed": len(all_reports) - total_fail,
|
|
271
|
+
"failed": total_fail,
|
|
272
|
+
"thresholds": {
|
|
273
|
+
"min_body_lines": MIN_BODY_LINES,
|
|
274
|
+
"min_ref_files": MIN_REF_FILES,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
"skills": all_reports,
|
|
278
|
+
}, indent=2))
|
|
279
|
+
else:
|
|
280
|
+
print(f"=== CLEO Skill Depth Check (all skills under {arg_path}) ===")
|
|
281
|
+
for r in all_reports:
|
|
282
|
+
_print_report(r)
|
|
283
|
+
print(f"\n=== SUMMARY ===")
|
|
284
|
+
print(f"Total skills: {len(all_reports)}")
|
|
285
|
+
print(f"Passed: {len(all_reports) - total_fail}")
|
|
286
|
+
print(f"Failed: {total_fail}")
|
|
287
|
+
return 1 if total_fail > 0 else 0
|
|
288
|
+
|
|
289
|
+
# Single-skill mode
|
|
290
|
+
if not arg_path.is_dir():
|
|
291
|
+
print(f"Error: '{args.skill_dir}' is not a directory", file=sys.stderr)
|
|
292
|
+
return 1
|
|
293
|
+
if not (arg_path / "SKILL.md").exists():
|
|
294
|
+
print(f"Error: '{args.skill_dir}' has no SKILL.md", file=sys.stderr)
|
|
295
|
+
return 1
|
|
296
|
+
|
|
297
|
+
passed, report = check_depth(arg_path, manifest)
|
|
298
|
+
if args.json:
|
|
299
|
+
print(json.dumps(report, indent=2))
|
|
300
|
+
else:
|
|
301
|
+
_print_report(report)
|
|
302
|
+
return 0 if passed else 1
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
if __name__ == "__main__":
|
|
306
|
+
sys.exit(main())
|
|
@@ -140,9 +140,70 @@ Non-compliant implementations SHOULD {remediation}.
|
|
|
140
140
|
|
|
141
141
|
---
|
|
142
142
|
|
|
143
|
+
## Through SDK (preferred)
|
|
144
|
+
|
|
145
|
+
Specifications are first-class docs SSoT records — created via
|
|
146
|
+
`cleo docs add --type spec`, auto-attached to the parent task, and
|
|
147
|
+
addressable by a stable slug. This is the canonical write path; the
|
|
148
|
+
legacy "write to `docs/specs/<NAME>.md` and commit" pattern is
|
|
149
|
+
deprecated below.
|
|
150
|
+
|
|
151
|
+
### Write the spec attached to its parent task
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cleo docs add T1234 docs/specs/auth-protocol.md \
|
|
155
|
+
--type spec \
|
|
156
|
+
--slug auth-protocol-v2 \
|
|
157
|
+
--desc "Auth protocol v2 — RFC 2119 requirements"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- `--type spec` is the canonical taxonomy value for a specification.
|
|
161
|
+
Other allowed values: `adr | research | handoff | note | llm-readme`.
|
|
162
|
+
- `--slug` is the kebab-case retrieval handle. Use the spec topic +
|
|
163
|
+
version (e.g. `auth-protocol-v2`, `release-pipeline-v3`). The CLI
|
|
164
|
+
returns `E_SLUG_TAKEN` with 3 alternatives on collision — pick one
|
|
165
|
+
rather than silently overwriting.
|
|
166
|
+
- The owner ID (`T1234`) auto-attaches the spec to its parent task so
|
|
167
|
+
downstream stages (`ct-validator`, decomposition, implementation)
|
|
168
|
+
can discover the spec via `cleo docs list --task T1234 --type spec`.
|
|
169
|
+
|
|
170
|
+
### Publish the spec to a git-tracked path (when the spec must ship on disk)
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
cleo docs publish --for T1234 --to docs/specs/auth-protocol.md
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Atomic tmp-then-rename. The published file lands in the next commit;
|
|
177
|
+
the SSoT blob remains canonical and continues to track future versions.
|
|
178
|
+
|
|
179
|
+
### Fetch the spec back by slug
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
cleo docs fetch auth-protocol-v2 # latest version
|
|
183
|
+
cleo docs versions --for T1234 # every SHA version
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Discover sibling specs in this project
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
cleo docs list --type spec --project # every spec in the project
|
|
190
|
+
cleo docs list --task T1234 --type spec # specs attached to T1234
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Deprecated: Direct filesystem write
|
|
194
|
+
|
|
195
|
+
The legacy "write to `docs/specs/{{SPEC_NAME}}.md` and commit" pattern
|
|
196
|
+
is deprecated. The on-disk file drifts from the SSoT, the spec has no
|
|
197
|
+
slug for downstream skills to retrieve it by, and the task↔spec
|
|
198
|
+
linkage exists only as a path convention. Migrate to
|
|
199
|
+
`cleo docs add --type spec --slug <name>` for every new spec — and use
|
|
200
|
+
`cleo docs sync --from docs/specs/<name>.md --for <taskId>` to
|
|
201
|
+
back-fill existing on-disk specs into the SSoT.
|
|
202
|
+
|
|
143
203
|
## Output Location
|
|
144
204
|
|
|
145
|
-
|
|
205
|
+
Spec blobs live in the docs SSoT; published copies on disk go in
|
|
206
|
+
`docs/specs/{{SPEC_NAME}}.md`.
|
|
146
207
|
|
|
147
208
|
---
|
|
148
209
|
|
|
@@ -202,3 +263,12 @@ This skill binds to the **specification** LOOM lifecycle stage. Governing ADRs:
|
|
|
202
263
|
- [ADR-023 — protocol validation dispatch](../../../../.cleo/adrs/ADR-023-protocol-validation-dispatch.md) — defines how specifications are validated before decomposition.
|
|
203
264
|
|
|
204
265
|
LOOM coverage matrix: [docs/skills/loom-coverage-matrix.md](../../../../docs/skills/loom-coverage-matrix.md).
|
|
266
|
+
|
|
267
|
+
## See references/
|
|
268
|
+
|
|
269
|
+
Progressive disclosure — load on demand only:
|
|
270
|
+
|
|
271
|
+
- `references/rfc2119-language.md` — keyword semantics, positive/negative examples, decision rubric
|
|
272
|
+
- `references/spec-templates.md` — protocol, API, architecture, requirements scaffolds + naming + versioning
|
|
273
|
+
- `references/traceability-matrix.md` — three-way trace from source to REQ to test, drift detection
|
|
274
|
+
- `references/anti-patterns.md` — ten failure modes seen in past spec drafts
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for ct-spec-writer/SKILL.md (T9643 / Epic T9629 / Saga T9625).
|
|
3
|
+
*
|
|
4
|
+
* Pins the SDK-first spec contract: specs MUST be created via
|
|
5
|
+
* `cleo docs add --type spec --slug <name>` so they auto-attach to the
|
|
6
|
+
* parent task and are retrievable by slug. The Output Location section
|
|
7
|
+
* may still mention `docs/specs/{{SPEC_NAME}}.md` as the published path,
|
|
8
|
+
* but the canonical write surface is the SDK.
|
|
9
|
+
*
|
|
10
|
+
* @task T9643
|
|
11
|
+
* @epic T9629
|
|
12
|
+
* @saga T9625
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { describe, expect, it } from 'vitest';
|
|
19
|
+
|
|
20
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
21
|
+
const skillRoot = resolve(dirname(thisFile), '..');
|
|
22
|
+
const skillPath = join(skillRoot, 'SKILL.md');
|
|
23
|
+
const skillContent = readFileSync(skillPath, 'utf-8');
|
|
24
|
+
|
|
25
|
+
describe('ct-spec-writer SKILL.md — SDK-first contract (T9643)', () => {
|
|
26
|
+
it('teaches `cleo docs add --type spec` as the canonical write path', () => {
|
|
27
|
+
expect(skillContent).toMatch(/cleo docs add[\s\S]+--type spec/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('shows `--slug` as the kebab-case retrieval handle', () => {
|
|
31
|
+
expect(skillContent).toContain('--slug');
|
|
32
|
+
expect(skillContent).toMatch(/kebab-case/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('attaches the spec to a parent task via the owner ID', () => {
|
|
36
|
+
// Owner ID example must use a T### prefix to demonstrate task linkage
|
|
37
|
+
expect(skillContent).toMatch(/cleo docs add\s+T\d+\s/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('shows `cleo docs publish --for ... --to docs/specs/...` for git publication', () => {
|
|
41
|
+
expect(skillContent).toMatch(/cleo docs publish[\s\S]+--for[\s\S]+--to[\s\S]+docs\/specs/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('shows `cleo docs fetch <slug>` for downstream retrieval', () => {
|
|
45
|
+
expect(skillContent).toContain('cleo docs fetch');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('shows `cleo docs list --type spec` for sibling spec discovery', () => {
|
|
49
|
+
expect(skillContent).toMatch(/cleo docs list[\s\S]+--type spec/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('marks the old direct-filesystem write as deprecated with a migration note', () => {
|
|
53
|
+
expect(skillContent).toContain('Deprecated: Direct filesystem write');
|
|
54
|
+
expect(skillContent).toMatch(/cleo docs (add|sync)/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('references E_SLUG_TAKEN for collision handling', () => {
|
|
58
|
+
expect(skillContent).toContain('E_SLUG_TAKEN');
|
|
59
|
+
});
|
|
60
|
+
});
|