master-skill 0.4.0

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.
Files changed (107) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/.codex/INSTALL.md +45 -0
  4. package/.cursor-plugin/plugin.json +25 -0
  5. package/.opencode/INSTALL.md +46 -0
  6. package/ETHICS.md +198 -0
  7. package/GEMINI.md +2 -0
  8. package/LICENSE +21 -0
  9. package/README.md +425 -0
  10. package/README_EN.md +398 -0
  11. package/SKILL.md +393 -0
  12. package/bin/cli.mjs +148 -0
  13. package/gemini-extension.json +6 -0
  14. package/hooks/hooks-cursor.json +10 -0
  15. package/hooks/hooks.json +16 -0
  16. package/hooks/run-hook.cmd +29 -0
  17. package/hooks/session-start +47 -0
  18. package/package.json +74 -0
  19. package/prebuilt/ajahn-chah/SKILL.md +126 -0
  20. package/prebuilt/ajahn-chah/meta.json +100 -0
  21. package/prebuilt/ajahn-chah/references/teaching.md +164 -0
  22. package/prebuilt/ajahn-chah/references/voice.md +98 -0
  23. package/prebuilt/ajahn-chah/sources/INDEX.md +24 -0
  24. package/prebuilt/ajahn-chah/sources/sutta-excerpts.md +128 -0
  25. package/prebuilt/ajahn-chah/sources/teachings-excerpts.md +204 -0
  26. package/prebuilt/ajahn-chah/tests/fidelity.jsonl +13 -0
  27. package/prebuilt/compare/SKILL.md +277 -0
  28. package/prebuilt/compare/tests/fidelity.jsonl +5 -0
  29. package/prebuilt/fazang/SKILL.md +132 -0
  30. package/prebuilt/fazang/meta.json +101 -0
  31. package/prebuilt/fazang/references/teaching.md +121 -0
  32. package/prebuilt/fazang/references/voice.md +95 -0
  33. package/prebuilt/fazang/sources/INDEX.md +22 -0
  34. package/prebuilt/fazang/sources/jinshizi-excerpts.md +35 -0
  35. package/prebuilt/fazang/sources/wujiao-zhang-excerpts.md +68 -0
  36. package/prebuilt/fazang/tests/fidelity.jsonl +10 -0
  37. package/prebuilt/huineng/SKILL.md +124 -0
  38. package/prebuilt/huineng/meta.json +91 -0
  39. package/prebuilt/huineng/references/teaching.md +122 -0
  40. package/prebuilt/huineng/references/voice.md +96 -0
  41. package/prebuilt/huineng/sources/INDEX.md +22 -0
  42. package/prebuilt/huineng/sources/jingang-excerpts.md +47 -0
  43. package/prebuilt/huineng/sources/tanjing-excerpts.md +81 -0
  44. package/prebuilt/huineng/tests/fidelity.jsonl +10 -0
  45. package/prebuilt/kumarajiva/SKILL.md +135 -0
  46. package/prebuilt/kumarajiva/meta.json +113 -0
  47. package/prebuilt/kumarajiva/references/teaching.md +131 -0
  48. package/prebuilt/kumarajiva/references/voice.md +99 -0
  49. package/prebuilt/kumarajiva/sources/INDEX.md +23 -0
  50. package/prebuilt/kumarajiva/sources/fahua-excerpts.md +33 -0
  51. package/prebuilt/kumarajiva/sources/jingang-bore-excerpts.md +46 -0
  52. package/prebuilt/kumarajiva/sources/zhonglun-excerpts.md +35 -0
  53. package/prebuilt/kumarajiva/tests/fidelity.jsonl +10 -0
  54. package/prebuilt/milarepa/SKILL.md +122 -0
  55. package/prebuilt/milarepa/meta.json +83 -0
  56. package/prebuilt/milarepa/references/teaching.md +135 -0
  57. package/prebuilt/milarepa/references/voice.md +99 -0
  58. package/prebuilt/milarepa/sources/INDEX.md +24 -0
  59. package/prebuilt/milarepa/sources/grubum-excerpts.md +124 -0
  60. package/prebuilt/milarepa/sources/namthar-excerpts.md +134 -0
  61. package/prebuilt/milarepa/tests/fidelity.jsonl +12 -0
  62. package/prebuilt/ouyi/SKILL.md +130 -0
  63. package/prebuilt/ouyi/meta.json +108 -0
  64. package/prebuilt/ouyi/references/teaching.md +118 -0
  65. package/prebuilt/ouyi/references/voice.md +97 -0
  66. package/prebuilt/ouyi/sources/INDEX.md +22 -0
  67. package/prebuilt/ouyi/sources/jiaoguan-gangzong-excerpts.md +34 -0
  68. package/prebuilt/ouyi/sources/mituo-yaojie-excerpts.md +50 -0
  69. package/prebuilt/ouyi/tests/fidelity.jsonl +10 -0
  70. package/prebuilt/xuanzang/SKILL.md +135 -0
  71. package/prebuilt/xuanzang/meta.json +110 -0
  72. package/prebuilt/xuanzang/references/teaching.md +162 -0
  73. package/prebuilt/xuanzang/references/voice.md +99 -0
  74. package/prebuilt/xuanzang/sources/INDEX.md +22 -0
  75. package/prebuilt/xuanzang/sources/chengweishi-excerpts.md +67 -0
  76. package/prebuilt/xuanzang/sources/xinjing-excerpts.md +34 -0
  77. package/prebuilt/xuanzang/tests/fidelity.jsonl +10 -0
  78. package/prebuilt/xuyun/SKILL.md +127 -0
  79. package/prebuilt/xuyun/meta.json +96 -0
  80. package/prebuilt/xuyun/references/teaching.md +106 -0
  81. package/prebuilt/xuyun/references/voice.md +96 -0
  82. package/prebuilt/xuyun/sources/INDEX.md +22 -0
  83. package/prebuilt/xuyun/sources/lengyanjing-excerpts.md +33 -0
  84. package/prebuilt/xuyun/sources/tanjing-excerpts.md +32 -0
  85. package/prebuilt/xuyun/tests/fidelity.jsonl +10 -0
  86. package/prebuilt/yinguang/SKILL.md +130 -0
  87. package/prebuilt/yinguang/meta.json +109 -0
  88. package/prebuilt/yinguang/references/teaching.md +103 -0
  89. package/prebuilt/yinguang/references/voice.md +96 -0
  90. package/prebuilt/yinguang/sources/INDEX.md +23 -0
  91. package/prebuilt/yinguang/sources/jingtu-sanjing-excerpts.md +32 -0
  92. package/prebuilt/yinguang/sources/wenchao-excerpts.md +50 -0
  93. package/prebuilt/yinguang/sources/yihanbianfu-excerpts.md +33 -0
  94. package/prebuilt/yinguang/tests/fidelity.jsonl +10 -0
  95. package/prebuilt/zhiyi/SKILL.md +130 -0
  96. package/prebuilt/zhiyi/meta.json +104 -0
  97. package/prebuilt/zhiyi/references/teaching.md +105 -0
  98. package/prebuilt/zhiyi/references/voice.md +96 -0
  99. package/prebuilt/zhiyi/sources/INDEX.md +22 -0
  100. package/prebuilt/zhiyi/sources/fahua-xuanyi-excerpts.md +103 -0
  101. package/prebuilt/zhiyi/sources/mohezhiguan-excerpts.md +117 -0
  102. package/prebuilt/zhiyi/tests/fidelity.jsonl +10 -0
  103. package/scripts/cite.py +74 -0
  104. package/scripts/query.py +84 -0
  105. package/scripts/test-fidelity.py +282 -0
  106. package/scripts/validate-fidelity.py +146 -0
  107. package/scripts/validate.py +207 -0
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """Master-skill SKILL.md frontmatter linter.
3
+
4
+ Walks prebuilt/<master>/SKILL.md, validates required fields and conventions
5
+ per the Anthropic Agent Skills spec + Master-skill provenance extensions.
6
+
7
+ Usage:
8
+ python scripts/validate.py # lint all masters
9
+ python scripts/validate.py --master zhiyi # lint one master
10
+ python scripts/validate.py --strict # fail on warnings too
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import re
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ PREBUILT_DIR = Path(__file__).resolve().parent.parent / "prebuilt"
22
+
23
+ # --- Required and recommended fields ---
24
+
25
+ REQUIRED_FIELDS = {"name", "description"}
26
+ RECOMMENDED_FIELDS = {"version", "license", "lineage", "dates", "sources", "citation_format"}
27
+ # Fields not applicable to meta-skills (aggregate/comparison skills with no single lineage)
28
+ META_SKILL_EXCLUDED = {"lineage", "dates", "sources", "citation_format"}
29
+ MAX_DESCRIPTION_CHARS = 500
30
+ MAX_SKILL_LINES = 500
31
+
32
+
33
+ def parse_frontmatter(path: Path) -> tuple[dict, str, list[str]]:
34
+ """Parse YAML frontmatter from a SKILL.md file.
35
+
36
+ Returns (frontmatter_dict, body, raw_lines).
37
+ """
38
+ text = path.read_text(encoding="utf-8")
39
+ lines = text.splitlines()
40
+ if not lines or lines[0].strip() != "---":
41
+ return {}, text, lines
42
+
43
+ end = None
44
+ for i, line in enumerate(lines[1:], start=1):
45
+ if line.strip() == "---":
46
+ end = i
47
+ break
48
+ if end is None:
49
+ return {}, text, lines
50
+
51
+ # Minimal YAML parse (no pyyaml dependency)
52
+ fm: dict = {}
53
+ current_key = None
54
+ current_list: list | None = None
55
+ for line in lines[1:end]:
56
+ # list item
57
+ if line.startswith(" - ") and current_key:
58
+ if current_list is None:
59
+ current_list = []
60
+ item = line.strip().lstrip("- ").strip()
61
+ # Try inline dict (title: xxx)
62
+ if ":" in item:
63
+ parts = item.split(":", 1)
64
+ if current_list and isinstance(current_list[-1], dict):
65
+ current_list[-1][parts[0].strip()] = parts[1].strip()
66
+ else:
67
+ current_list.append({parts[0].strip(): parts[1].strip()})
68
+ else:
69
+ current_list.append(item)
70
+ continue
71
+ # Save accumulated list
72
+ if current_list is not None and current_key:
73
+ fm[current_key] = current_list
74
+ current_list = None
75
+ # key: value
76
+ match = re.match(r"^(\w[\w_-]*):\s*(.*)", line)
77
+ if match:
78
+ current_key = match.group(1)
79
+ value = match.group(2).strip().strip('"').strip("'")
80
+ if value:
81
+ fm[current_key] = value
82
+ # If empty value, might be a list starting next line
83
+ # Flush last list
84
+ if current_list is not None and current_key:
85
+ fm[current_key] = current_list
86
+
87
+ body = "\n".join(lines[end + 1 :])
88
+ return fm, body, lines
89
+
90
+
91
+ def lint_master(master_dir: Path, strict: bool = False) -> list[str]:
92
+ """Lint a single master directory. Returns list of issues."""
93
+ issues: list[str] = []
94
+ name = master_dir.name
95
+ skill_path = master_dir / "SKILL.md"
96
+
97
+ if not skill_path.exists():
98
+ issues.append(f"[ERROR] {name}: missing SKILL.md")
99
+ return issues
100
+
101
+ fm, body, lines = parse_frontmatter(skill_path)
102
+
103
+ # --- Required fields ---
104
+ for field in REQUIRED_FIELDS:
105
+ if field not in fm:
106
+ issues.append(f"[ERROR] {name}: missing required field '{field}'")
107
+
108
+ # --- Recommended fields ---
109
+ kind = fm.get("kind", "master")
110
+ for field in RECOMMENDED_FIELDS:
111
+ if kind == "meta-skill" and field in META_SKILL_EXCLUDED:
112
+ continue
113
+ if field not in fm:
114
+ issues.append(f"[WARN] {name}: missing recommended field '{field}'")
115
+
116
+ # --- Description length ---
117
+ desc = fm.get("description", "")
118
+ if isinstance(desc, str) and len(desc) > MAX_DESCRIPTION_CHARS:
119
+ issues.append(f"[WARN] {name}: description exceeds {MAX_DESCRIPTION_CHARS} chars ({len(desc)})")
120
+
121
+ # --- SKILL.md line count ---
122
+ if len(lines) > MAX_SKILL_LINES:
123
+ issues.append(f"[WARN] {name}: SKILL.md exceeds {MAX_SKILL_LINES} lines ({len(lines)})")
124
+
125
+ # --- Sources validation ---
126
+ sources = fm.get("sources")
127
+ if isinstance(sources, list):
128
+ for i, src in enumerate(sources):
129
+ if isinstance(src, dict):
130
+ if "title" not in src and "cbeta_id" not in src:
131
+ issues.append(f"[WARN] {name}: sources[{i}] missing 'title' or 'cbeta_id'")
132
+
133
+ # --- Directory structure checks ---
134
+ # Meta-skills (e.g. compare-masters) borrow from other masters and have no own corpus
135
+ if kind != "meta-skill":
136
+ refs_dir = master_dir / "references"
137
+ sources_dir = master_dir / "sources"
138
+
139
+ if not refs_dir.exists():
140
+ issues.append(f"[WARN] {name}: missing references/ directory")
141
+ else:
142
+ if not (refs_dir / "voice.md").exists():
143
+ issues.append(f"[WARN] {name}: missing references/voice.md")
144
+ if not (refs_dir / "teaching.md").exists():
145
+ issues.append(f"[WARN] {name}: missing references/teaching.md")
146
+
147
+ if not sources_dir.exists():
148
+ issues.append(f"[WARN] {name}: missing sources/ directory")
149
+ elif not list(sources_dir.glob("*.md")):
150
+ issues.append(f"[WARN] {name}: sources/ directory is empty")
151
+
152
+ # --- Check for tests ---
153
+ tests_dir = master_dir / "tests"
154
+ if not tests_dir.exists() or not (tests_dir / "fidelity.jsonl").exists():
155
+ issues.append(f"[WARN] {name}: missing tests/fidelity.jsonl")
156
+
157
+ # --- Strict mode: treat warnings as errors ---
158
+ if strict:
159
+ issues = [i.replace("[WARN] ", "[ERROR]") for i in issues]
160
+
161
+ return issues
162
+
163
+
164
+ def main():
165
+ parser = argparse.ArgumentParser(description="Master-skill SKILL.md linter")
166
+ parser.add_argument("--master", type=str, help="Lint a specific master only")
167
+ parser.add_argument("--strict", action="store_true", help="Treat warnings as errors")
168
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
169
+ args = parser.parse_args()
170
+
171
+ if args.master:
172
+ dirs = [PREBUILT_DIR / args.master]
173
+ if not dirs[0].exists():
174
+ print(f"Master '{args.master}' not found in {PREBUILT_DIR}")
175
+ sys.exit(1)
176
+ else:
177
+ dirs = sorted(d for d in PREBUILT_DIR.iterdir() if d.is_dir())
178
+
179
+ all_issues: dict[str, list[str]] = {}
180
+ has_errors = False
181
+
182
+ for d in dirs:
183
+ issues = lint_master(d, strict=args.strict)
184
+ if issues:
185
+ all_issues[d.name] = issues
186
+ if any("[ERROR]" in i for i in issues):
187
+ has_errors = True
188
+
189
+ if args.json:
190
+ print(json.dumps(all_issues, indent=2, ensure_ascii=False))
191
+ else:
192
+ if not all_issues:
193
+ print(f"✅ All {len(dirs)} masters pass validation.")
194
+ else:
195
+ for name, issues in all_issues.items():
196
+ for issue in issues:
197
+ print(issue)
198
+ print()
199
+ total_errors = sum(1 for issues in all_issues.values() for i in issues if "[ERROR]" in i)
200
+ total_warns = sum(1 for issues in all_issues.values() for i in issues if "[WARN]" in i)
201
+ print(f"Summary: {total_errors} error(s), {total_warns} warning(s) across {len(all_issues)} master(s)")
202
+
203
+ sys.exit(1 if has_errors else 0)
204
+
205
+
206
+ if __name__ == "__main__":
207
+ main()