@tw93/waza 3.25.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.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/package.json +35 -0
- package/rules/anti-patterns.md +38 -0
- package/rules/chinese.md +18 -0
- package/rules/durable-context.md +27 -0
- package/rules/english.md +14 -0
- package/scripts/build_metadata.py +360 -0
- package/scripts/check_routing_drift.py +82 -0
- package/scripts/dispatcher-template.md +43 -0
- package/scripts/dispatcher.md +53 -0
- package/scripts/package-skill.sh +71 -0
- package/scripts/packaging_filter.py +55 -0
- package/scripts/setup-rule.sh +109 -0
- package/scripts/setup-statusline.sh +127 -0
- package/scripts/skill_checks.py +483 -0
- package/scripts/skill_frontmatter.py +110 -0
- package/scripts/statusline.sh +321 -0
- package/scripts/validate_package.py +66 -0
- package/scripts/verify_skills.py +100 -0
- package/skills/RESOLVER.md +91 -0
- package/skills/check/SKILL.md +338 -0
- package/skills/check/agents/reviewer-architecture.md +39 -0
- package/skills/check/agents/reviewer-security.md +39 -0
- package/skills/check/references/persona-catalog.md +56 -0
- package/skills/check/references/project-context.md +107 -0
- package/skills/check/references/public-reply.md +14 -0
- package/skills/check/scripts/audit_signals.py +485 -0
- package/skills/check/scripts/run-tests.sh +19 -0
- package/skills/design/SKILL.md +134 -0
- package/skills/design/references/design-aesthetic-quality.md +67 -0
- package/skills/design/references/design-data-viz.md +34 -0
- package/skills/design/references/design-reference.md +278 -0
- package/skills/design/references/design-tokens.md +53 -0
- package/skills/design/references/design-traps.md +43 -0
- package/skills/health/SKILL.md +231 -0
- package/skills/health/agents/inspector-context.md +119 -0
- package/skills/health/agents/inspector-control.md +84 -0
- package/skills/health/agents/inspector-maintainability.md +55 -0
- package/skills/health/scripts/check-agent-context.sh +5 -0
- package/skills/health/scripts/check-doc-refs.sh +8 -0
- package/skills/health/scripts/check-maintainability.sh +8 -0
- package/skills/health/scripts/check-verifier-output.sh +5 -0
- package/skills/health/scripts/check_agent_context.py +407 -0
- package/skills/health/scripts/check_doc_refs.py +110 -0
- package/skills/health/scripts/check_maintainability.py +629 -0
- package/skills/health/scripts/check_verifier_output.py +116 -0
- package/skills/health/scripts/collect-data.sh +760 -0
- package/skills/hunt/SKILL.md +197 -0
- package/skills/hunt/references/failure-patterns.md +75 -0
- package/skills/hunt/references/ime-unicode.md +58 -0
- package/skills/hunt/references/logging-techniques.md +72 -0
- package/skills/hunt/references/rendering-debug.md +34 -0
- package/skills/learn/SKILL.md +128 -0
- package/skills/read/SKILL.md +108 -0
- package/skills/read/references/read-methods.md +110 -0
- package/skills/read/references/save-paths.md +33 -0
- package/skills/read/scripts/fetch.sh +105 -0
- package/skills/read/scripts/fetch_feishu.py +246 -0
- package/skills/read/scripts/fetch_local.py +218 -0
- package/skills/read/scripts/fetch_weixin.py +107 -0
- package/skills/think/SKILL.md +155 -0
- package/skills/write/SKILL.md +129 -0
- package/skills/write/references/write-en.md +197 -0
- package/skills/write/references/write-zh-bilingual.md +60 -0
- package/skills/write/references/write-zh-prose.md +48 -0
- package/skills/write/references/write-zh-release-notes.md +38 -0
- package/skills/write/references/write-zh.md +645 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate Waza distribution metadata from a single source of truth.
|
|
3
|
+
|
|
4
|
+
Source of truth:
|
|
5
|
+
- VERSION top-level version string (single source)
|
|
6
|
+
- skills/<name>/SKILL.md frontmatter name, description, dispatch_intent
|
|
7
|
+
|
|
8
|
+
Generated files:
|
|
9
|
+
- .claude-plugin/marketplace.json full plugin manifest
|
|
10
|
+
- README.md install URLs pinned to VERSION
|
|
11
|
+
- package.json npm/Pi package metadata pinned to VERSION
|
|
12
|
+
- scripts/setup-rule.sh default WAZA_REF pinned to VERSION
|
|
13
|
+
- scripts/setup-statusline.sh default WAZA_REF pinned to VERSION
|
|
14
|
+
|
|
15
|
+
Modes:
|
|
16
|
+
--write (default) regenerate every file from source
|
|
17
|
+
--check compare generated bytes against committed files;
|
|
18
|
+
exit non-zero with a diff on mismatch
|
|
19
|
+
|
|
20
|
+
Run as: python3 scripts/build_metadata.py [--check] [--root PATH]
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import difflib
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
33
|
+
sys.path.insert(0, str(ROOT / "scripts"))
|
|
34
|
+
|
|
35
|
+
from skill_frontmatter import parse_frontmatter # noqa: E402
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Hand-maintained marketplace constants. Kept here (not in frontmatter) because
|
|
39
|
+
# they describe the Waza project itself, not any single skill.
|
|
40
|
+
MARKETPLACE_TOP = {
|
|
41
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
42
|
+
"name": "waza",
|
|
43
|
+
"description": (
|
|
44
|
+
"Personal skill collection for Claude Code and Codex: think, check, "
|
|
45
|
+
"hunt, design, read, write, learn, and health for agent config and "
|
|
46
|
+
"AI maintainability audits."
|
|
47
|
+
),
|
|
48
|
+
"owner": {
|
|
49
|
+
"name": "Tw93",
|
|
50
|
+
"email": "hitw93@gmail.com",
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
BUNDLE_DESCRIPTION = (
|
|
55
|
+
"Installs the full Waza toolkit. Registers all eight skills under the "
|
|
56
|
+
"waza namespace, callable as /waza:think, /waza:check, /waza:hunt, "
|
|
57
|
+
"/waza:design, /waza:read, /waza:write, /waza:learn, and /waza:health. "
|
|
58
|
+
"For one skill on Claude Code v2.1.143 or newer, use /plugin install "
|
|
59
|
+
"waza-<name>@waza."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
CATEGORY = "development"
|
|
63
|
+
HOMEPAGE = "https://github.com/tw93/Waza"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_version(root: Path) -> str:
|
|
67
|
+
version_file = root / "VERSION"
|
|
68
|
+
if not version_file.exists():
|
|
69
|
+
raise SystemExit(f"ERROR: missing VERSION file at {version_file}")
|
|
70
|
+
version = version_file.read_text().strip()
|
|
71
|
+
if not version:
|
|
72
|
+
raise SystemExit("ERROR: VERSION file is empty")
|
|
73
|
+
return version
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def collect_skill_metadata(root: Path) -> list[dict]:
|
|
77
|
+
skill_files = sorted((root / "skills").glob("*/SKILL.md"))
|
|
78
|
+
if not skill_files:
|
|
79
|
+
raise SystemExit("ERROR: no SKILL.md files found under skills/")
|
|
80
|
+
skills: list[dict] = []
|
|
81
|
+
for path in skill_files:
|
|
82
|
+
fields = parse_frontmatter(path)
|
|
83
|
+
if fields["name"] != path.parent.name:
|
|
84
|
+
raise SystemExit(
|
|
85
|
+
f"ERROR: {path} frontmatter name={fields['name']!r} "
|
|
86
|
+
f"!= directory {path.parent.name!r}"
|
|
87
|
+
)
|
|
88
|
+
skills.append(fields)
|
|
89
|
+
return skills
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_marketplace(version: str, skills: list[dict]) -> dict:
|
|
93
|
+
plugins = [
|
|
94
|
+
{
|
|
95
|
+
"name": "waza",
|
|
96
|
+
"description": BUNDLE_DESCRIPTION,
|
|
97
|
+
"version": version,
|
|
98
|
+
"category": CATEGORY,
|
|
99
|
+
"source": "./",
|
|
100
|
+
"homepage": HOMEPAGE,
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
for skill in sorted(skills, key=lambda s: s["name"]):
|
|
104
|
+
plugins.append(
|
|
105
|
+
{
|
|
106
|
+
"name": f"waza-{skill['name']}",
|
|
107
|
+
"description": skill["description"],
|
|
108
|
+
"version": version,
|
|
109
|
+
"category": CATEGORY,
|
|
110
|
+
"source": f"./skills/{skill['name']}",
|
|
111
|
+
"homepage": HOMEPAGE,
|
|
112
|
+
# `skills` field declares which subdirectories under `source`
|
|
113
|
+
# contain SKILL.md entrypoints. "./" means the source dir itself
|
|
114
|
+
# is the skill root. Present on every per-skill entry; absent
|
|
115
|
+
# on the bundle (the bundle uses Claude Code's auto-discovery).
|
|
116
|
+
"skills": ["./"],
|
|
117
|
+
"strict": False,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
return {**MARKETPLACE_TOP, "plugins": plugins}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def render_marketplace(marketplace: dict) -> str:
|
|
124
|
+
return json.dumps(marketplace, indent=2, ensure_ascii=False) + "\n"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_package_json(version: str) -> str:
|
|
128
|
+
package = {
|
|
129
|
+
"name": "@tw93/waza",
|
|
130
|
+
"version": version,
|
|
131
|
+
"description": (
|
|
132
|
+
"Waza engineering skills for Claude Code, Codex, Pi, and "
|
|
133
|
+
"compatible coding agents."
|
|
134
|
+
),
|
|
135
|
+
"license": "MIT",
|
|
136
|
+
"repository": {
|
|
137
|
+
"type": "git",
|
|
138
|
+
"url": "git+https://github.com/tw93/Waza.git",
|
|
139
|
+
},
|
|
140
|
+
"homepage": "https://github.com/tw93/Waza#readme",
|
|
141
|
+
"keywords": [
|
|
142
|
+
"pi-package",
|
|
143
|
+
"agent-skills",
|
|
144
|
+
"waza",
|
|
145
|
+
"claude-code",
|
|
146
|
+
"codex",
|
|
147
|
+
],
|
|
148
|
+
"files": [
|
|
149
|
+
"LICENSE",
|
|
150
|
+
"README.md",
|
|
151
|
+
"rules",
|
|
152
|
+
"scripts",
|
|
153
|
+
"skills",
|
|
154
|
+
"!**/__pycache__/**",
|
|
155
|
+
"!**/*.pyc",
|
|
156
|
+
],
|
|
157
|
+
"publishConfig": {
|
|
158
|
+
"access": "public",
|
|
159
|
+
},
|
|
160
|
+
"pi": {
|
|
161
|
+
"skills": [
|
|
162
|
+
"./skills",
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
return json.dumps(package, indent=2, ensure_ascii=False) + "\n"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
ROUTING_TABLE_START = "<!-- routing-table:start -->"
|
|
170
|
+
ROUTING_TABLE_END = "<!-- routing-table:end -->"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def render_dispatcher(template: str, skills: list[dict]) -> str:
|
|
174
|
+
rows = ["| Intent | Skill | File |", "|--------|-------|------|"]
|
|
175
|
+
for skill in sorted(skills, key=lambda s: s["name"]):
|
|
176
|
+
intent = skill.get("dispatch_intent") or ""
|
|
177
|
+
if not intent:
|
|
178
|
+
raise SystemExit(
|
|
179
|
+
f"ERROR: skill {skill['name']} missing dispatch_intent in frontmatter"
|
|
180
|
+
)
|
|
181
|
+
rows.append(f"| {intent} | {skill['name']} | `skills/{skill['name']}/SKILL.md` |")
|
|
182
|
+
table = "\n".join(rows)
|
|
183
|
+
block = f"{ROUTING_TABLE_START}\n{table}\n{ROUTING_TABLE_END}"
|
|
184
|
+
if ROUTING_TABLE_START not in template or ROUTING_TABLE_END not in template:
|
|
185
|
+
raise SystemExit(
|
|
186
|
+
"ERROR: dispatcher template is missing routing-table markers"
|
|
187
|
+
)
|
|
188
|
+
pattern = re.compile(
|
|
189
|
+
re.escape(ROUTING_TABLE_START) + r".*?" + re.escape(ROUTING_TABLE_END),
|
|
190
|
+
re.DOTALL,
|
|
191
|
+
)
|
|
192
|
+
return pattern.sub(block, template)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# Matches both pinned (v3.24.0) and unpinned (main) install URLs so the
|
|
196
|
+
# generator can rewrite either form to the current VERSION.
|
|
197
|
+
README_INSTALL_URL_RE = re.compile(
|
|
198
|
+
r"raw\.githubusercontent\.com/tw93/Waza/(?:main|v\d+\.\d+\.\d+)/scripts/"
|
|
199
|
+
)
|
|
200
|
+
README_SWAP_TAG_RE = re.compile(r"swap `v\d+\.\d+\.\d+` for `main`")
|
|
201
|
+
WAZA_REF_RE = re.compile(r'WAZA_REF="\$\{WAZA_REF:-(?:main|v\d+\.\d+\.\d+)\}"')
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def render_readme(current: str, version: str) -> str:
|
|
205
|
+
pinned = f"raw.githubusercontent.com/tw93/Waza/v{version}/scripts/"
|
|
206
|
+
current = README_INSTALL_URL_RE.sub(pinned, current)
|
|
207
|
+
return README_SWAP_TAG_RE.sub(f"swap `v{version}` for `main`", current)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def render_script_ref(current: str, version: str) -> str:
|
|
211
|
+
return WAZA_REF_RE.sub(f'WAZA_REF="${{WAZA_REF:-v{version}}}"', current)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def diff(label: str, expected: str, actual: str) -> str:
|
|
215
|
+
return "".join(
|
|
216
|
+
difflib.unified_diff(
|
|
217
|
+
actual.splitlines(keepends=True),
|
|
218
|
+
expected.splitlines(keepends=True),
|
|
219
|
+
fromfile=f"committed:{label}",
|
|
220
|
+
tofile=f"generated:{label}",
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def main() -> int:
|
|
226
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"--root",
|
|
229
|
+
type=Path,
|
|
230
|
+
default=ROOT,
|
|
231
|
+
help="Repository root (default: parent of scripts/)",
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"--check",
|
|
235
|
+
action="store_true",
|
|
236
|
+
help="Compare generated bytes to committed files; exit non-zero on drift.",
|
|
237
|
+
)
|
|
238
|
+
args = parser.parse_args()
|
|
239
|
+
root = args.root.resolve()
|
|
240
|
+
|
|
241
|
+
version = read_version(root)
|
|
242
|
+
skills = collect_skill_metadata(root)
|
|
243
|
+
marketplace = build_marketplace(version, skills)
|
|
244
|
+
rendered = render_marketplace(marketplace)
|
|
245
|
+
package_rendered = build_package_json(version)
|
|
246
|
+
|
|
247
|
+
target = root / ".claude-plugin" / "marketplace.json"
|
|
248
|
+
package_json = root / "package.json"
|
|
249
|
+
package_actual = package_json.read_text() if package_json.exists() else ""
|
|
250
|
+
readme = root / "README.md"
|
|
251
|
+
readme_actual = readme.read_text() if readme.exists() else ""
|
|
252
|
+
readme_rendered = render_readme(readme_actual, version)
|
|
253
|
+
ref_scripts = [
|
|
254
|
+
root / "scripts" / "setup-rule.sh",
|
|
255
|
+
root / "scripts" / "setup-statusline.sh",
|
|
256
|
+
]
|
|
257
|
+
script_pairs = []
|
|
258
|
+
for script in ref_scripts:
|
|
259
|
+
actual = script.read_text() if script.exists() else ""
|
|
260
|
+
script_pairs.append((script, actual, render_script_ref(actual, version)))
|
|
261
|
+
|
|
262
|
+
dispatcher_template = root / "scripts" / "dispatcher-template.md"
|
|
263
|
+
dispatcher_target = root / "scripts" / "dispatcher.md"
|
|
264
|
+
if not dispatcher_template.exists():
|
|
265
|
+
raise SystemExit(f"ERROR: missing dispatcher template at {dispatcher_template}")
|
|
266
|
+
dispatcher_actual = (
|
|
267
|
+
dispatcher_target.read_text() if dispatcher_target.exists() else ""
|
|
268
|
+
)
|
|
269
|
+
dispatcher_rendered = render_dispatcher(dispatcher_template.read_text(), skills)
|
|
270
|
+
|
|
271
|
+
if args.check:
|
|
272
|
+
actual = target.read_text() if target.exists() else ""
|
|
273
|
+
drift = False
|
|
274
|
+
if actual != rendered:
|
|
275
|
+
print(
|
|
276
|
+
f"DRIFT: {target.relative_to(root)} is out of sync with "
|
|
277
|
+
f"VERSION + SKILL.md frontmatter.\n"
|
|
278
|
+
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
279
|
+
file=sys.stderr,
|
|
280
|
+
)
|
|
281
|
+
sys.stderr.write(diff("marketplace.json", rendered, actual))
|
|
282
|
+
drift = True
|
|
283
|
+
if readme_actual != readme_rendered:
|
|
284
|
+
print(
|
|
285
|
+
f"DRIFT: README.md install URLs are not pinned to v{version}.\n"
|
|
286
|
+
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
287
|
+
file=sys.stderr,
|
|
288
|
+
)
|
|
289
|
+
sys.stderr.write(diff("README.md", readme_rendered, readme_actual))
|
|
290
|
+
drift = True
|
|
291
|
+
if package_actual != package_rendered:
|
|
292
|
+
print(
|
|
293
|
+
f"DRIFT: package.json is out of sync with VERSION v{version} "
|
|
294
|
+
f"and Pi package metadata.\n"
|
|
295
|
+
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
296
|
+
file=sys.stderr,
|
|
297
|
+
)
|
|
298
|
+
sys.stderr.write(diff("package.json", package_rendered, package_actual))
|
|
299
|
+
drift = True
|
|
300
|
+
for script, actual, rendered_script in script_pairs:
|
|
301
|
+
if actual != rendered_script:
|
|
302
|
+
label = script.relative_to(root).as_posix()
|
|
303
|
+
print(
|
|
304
|
+
f"DRIFT: {label} default WAZA_REF is not pinned to v{version}.\n"
|
|
305
|
+
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
306
|
+
file=sys.stderr,
|
|
307
|
+
)
|
|
308
|
+
sys.stderr.write(diff(label, rendered_script, actual))
|
|
309
|
+
drift = True
|
|
310
|
+
if dispatcher_actual != dispatcher_rendered:
|
|
311
|
+
label = dispatcher_target.relative_to(root).as_posix()
|
|
312
|
+
print(
|
|
313
|
+
f"DRIFT: {label} routing table is out of sync with "
|
|
314
|
+
f"SKILL.md dispatch_intent frontmatter.\n"
|
|
315
|
+
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
316
|
+
file=sys.stderr,
|
|
317
|
+
)
|
|
318
|
+
sys.stderr.write(diff(label, dispatcher_rendered, dispatcher_actual))
|
|
319
|
+
drift = True
|
|
320
|
+
if drift:
|
|
321
|
+
return 1
|
|
322
|
+
print(f"ok: {target.relative_to(root)} matches generator")
|
|
323
|
+
print(f"ok: README.md install URLs pinned to v{version}")
|
|
324
|
+
print(f"ok: package.json pinned to v{version}")
|
|
325
|
+
print(f"ok: installer defaults pinned to v{version}")
|
|
326
|
+
print(f"ok: {dispatcher_target.relative_to(root)} matches generator")
|
|
327
|
+
return 0
|
|
328
|
+
|
|
329
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
330
|
+
target.write_text(rendered)
|
|
331
|
+
print(f"wrote: {target.relative_to(root)} ({len(rendered)} bytes)")
|
|
332
|
+
if package_actual != package_rendered:
|
|
333
|
+
package_json.write_text(package_rendered)
|
|
334
|
+
print(f"wrote: package.json (pinned version to v{version})")
|
|
335
|
+
else:
|
|
336
|
+
print(f"ok: package.json already pinned to v{version}")
|
|
337
|
+
if readme_actual != readme_rendered:
|
|
338
|
+
readme.write_text(readme_rendered)
|
|
339
|
+
print(f"wrote: README.md (pinned install URLs to v{version})")
|
|
340
|
+
else:
|
|
341
|
+
print(f"ok: README.md install URLs already pinned to v{version}")
|
|
342
|
+
for script, actual, rendered_script in script_pairs:
|
|
343
|
+
if actual != rendered_script:
|
|
344
|
+
script.write_text(rendered_script)
|
|
345
|
+
print(f"wrote: {script.relative_to(root)} (default WAZA_REF=v{version})")
|
|
346
|
+
else:
|
|
347
|
+
print(f"ok: {script.relative_to(root)} default WAZA_REF already pinned")
|
|
348
|
+
if dispatcher_actual != dispatcher_rendered:
|
|
349
|
+
dispatcher_target.write_text(dispatcher_rendered)
|
|
350
|
+
print(
|
|
351
|
+
f"wrote: {dispatcher_target.relative_to(root)} "
|
|
352
|
+
f"({len(dispatcher_rendered)} bytes)"
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
print(f"ok: {dispatcher_target.relative_to(root)} already matches generator")
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
if __name__ == "__main__":
|
|
360
|
+
sys.exit(main())
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify dispatcher routing table and RESOLVER.md cover every skill.
|
|
3
|
+
|
|
4
|
+
Both files must reference the same set of skill names found under
|
|
5
|
+
`skills/*/SKILL.md`. Treated as a sanity tripwire alongside the codegen in
|
|
6
|
+
`build_metadata.py`; cheap enough to keep even after routing tables become
|
|
7
|
+
generated.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
SKILL_REF_RE = re.compile(r"skills/([a-z][a-z0-9_-]*)/SKILL\.md")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> int:
|
|
21
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--root",
|
|
24
|
+
type=Path,
|
|
25
|
+
default=Path(__file__).resolve().parent.parent,
|
|
26
|
+
help="Repository root (default: parent of scripts/)",
|
|
27
|
+
)
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
root = args.root.resolve()
|
|
30
|
+
|
|
31
|
+
expected = {
|
|
32
|
+
p.parent.name for p in (root / "skills").glob("*/SKILL.md")
|
|
33
|
+
}
|
|
34
|
+
if not expected:
|
|
35
|
+
print("ERROR: no skills found under skills/*/SKILL.md", file=sys.stderr)
|
|
36
|
+
return 1
|
|
37
|
+
|
|
38
|
+
dispatcher = set(
|
|
39
|
+
SKILL_REF_RE.findall((root / "scripts" / "dispatcher.md").read_text())
|
|
40
|
+
)
|
|
41
|
+
resolver = set(
|
|
42
|
+
SKILL_REF_RE.findall((root / "skills" / "RESOLVER.md").read_text())
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
drift = False
|
|
46
|
+
|
|
47
|
+
missing_dispatcher = expected - dispatcher
|
|
48
|
+
if missing_dispatcher:
|
|
49
|
+
print(
|
|
50
|
+
"ROUTING DRIFT: skills missing from scripts/dispatcher.md: "
|
|
51
|
+
f"{sorted(missing_dispatcher)}",
|
|
52
|
+
file=sys.stderr,
|
|
53
|
+
)
|
|
54
|
+
drift = True
|
|
55
|
+
|
|
56
|
+
missing_resolver = expected - resolver
|
|
57
|
+
if missing_resolver:
|
|
58
|
+
print(
|
|
59
|
+
f"ROUTING DRIFT: skills missing from RESOLVER.md: {sorted(missing_resolver)}",
|
|
60
|
+
file=sys.stderr,
|
|
61
|
+
)
|
|
62
|
+
drift = True
|
|
63
|
+
|
|
64
|
+
stale_dispatcher = dispatcher - expected
|
|
65
|
+
if stale_dispatcher:
|
|
66
|
+
print(
|
|
67
|
+
f"ROUTING DRIFT: stale skill refs in scripts/dispatcher.md: {sorted(stale_dispatcher)}",
|
|
68
|
+
file=sys.stderr,
|
|
69
|
+
)
|
|
70
|
+
drift = True
|
|
71
|
+
|
|
72
|
+
if drift:
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
print(
|
|
76
|
+
f"ok: routing consistent across {len(expected)} skills (scripts/dispatcher.md + RESOLVER.md)"
|
|
77
|
+
)
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
sys.exit(main())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: waza
|
|
3
|
+
description: 'Dispatcher for Waza engineering skills: think (architecture/handoff), design (artifact-grounded UI), check (review/release gates), hunt (runtime debugging/regression), write (prose/release copy), learn (research), read (URL/PDF fetch), health (agent config and AI maintainability audit).'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Waza: Engineering Skills Dispatcher
|
|
7
|
+
|
|
8
|
+
Prefix your first line with 🥷 inline, not as its own paragraph.
|
|
9
|
+
|
|
10
|
+
You have eight skills available. Match the user's intent to the right skill, read the matching section below, and execute it.
|
|
11
|
+
|
|
12
|
+
## Routing Table
|
|
13
|
+
|
|
14
|
+
<!-- routing-table:start -->
|
|
15
|
+
<!-- routing-table:end -->
|
|
16
|
+
|
|
17
|
+
## How This Works
|
|
18
|
+
|
|
19
|
+
1. Read the user's message and match it to a skill from the table above.
|
|
20
|
+
2. Read the matched skill section in full.
|
|
21
|
+
3. Execute that skill's instructions exactly.
|
|
22
|
+
|
|
23
|
+
If the message could match multiple skills, use these disambiguation rules:
|
|
24
|
+
|
|
25
|
+
1. Most specific wins: `/design` is more specific than `/think` for UI decisions.
|
|
26
|
+
2. URL in message: start with `/read`. If the content is research material, chain to `/learn`.
|
|
27
|
+
3. Code already done vs. code broken: done/PR -> `/check`; error/broken -> `/hunt`.
|
|
28
|
+
4. Config/maintainability vs. code: Codex/Claude misbehaving, hooks/MCP, `/health` token usage, AI coding code rot, unclear context, missing verification, or stale verifier output -> `/health`; user code errors -> `/hunt`.
|
|
29
|
+
5. Release action vs. release prose: commit/tag/publish/push/release reactions/close issue -> `/check`; write release notes/changelog text -> `/write`.
|
|
30
|
+
6. Screenshot taste vs. screenshot regression: visual taste complaint -> `/design`; broken render/state/generated output or used-to-work evidence -> `/hunt`.
|
|
31
|
+
7. From scratch vs. editing: new long-form output -> `/learn`; existing draft to polish -> `/write`.
|
|
32
|
+
8. "Judge this" + error -> `/hunt`; "judge this" + should we keep it -> `/think`.
|
|
33
|
+
9. Still ambiguous: read both skills' "Not for" sections; use exclusion. If still unclear, ask the user.
|
|
34
|
+
|
|
35
|
+
## Path Resolution
|
|
36
|
+
|
|
37
|
+
In this distribution, sub-skill scripts live at `skills/{name}/scripts/`. Resolve all relative paths from this file's directory, not from a personal home-directory skill cache.
|
|
38
|
+
|
|
39
|
+
## Chaining
|
|
40
|
+
|
|
41
|
+
Skills chain manually, not automatically. Each skill completes and waits for the user's next action.
|
|
42
|
+
|
|
43
|
+
Common chains: `/think` -> implement approved plan -> `/check` | `/hunt` -> fix -> `/check` -> release/push/issue follow-through | `/read` -> `/learn` -> `/write`
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: waza
|
|
3
|
+
description: 'Dispatcher for Waza engineering skills: think (architecture/handoff), design (artifact-grounded UI), check (review/release gates), hunt (runtime debugging/regression), write (prose/release copy), learn (research), read (URL/PDF fetch), health (agent config and AI maintainability audit).'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Waza: Engineering Skills Dispatcher
|
|
7
|
+
|
|
8
|
+
Prefix your first line with 🥷 inline, not as its own paragraph.
|
|
9
|
+
|
|
10
|
+
You have eight skills available. Match the user's intent to the right skill, read the matching section below, and execute it.
|
|
11
|
+
|
|
12
|
+
## Routing Table
|
|
13
|
+
|
|
14
|
+
<!-- routing-table:start -->
|
|
15
|
+
| Intent | Skill | File |
|
|
16
|
+
|--------|-------|------|
|
|
17
|
+
| Code review, before merge, release gates, generated artifacts, safety sinks, publish/push/reaction follow-through, triage issues/PRs, project-wide code-quality audit scorecard | check | `skills/check/SKILL.md` |
|
|
18
|
+
| UI, component, page, visual interface, frontend, artifact-grounded screenshot aesthetic complaint | design | `skills/design/SKILL.md` |
|
|
19
|
+
| Codex/Claude/Pi ignoring instructions, agent config audit, hooks/MCP broken, health token usage, AI coding code rot, hotspot ownership, unclear context, missing verification, stale verifier output | health | `skills/health/SKILL.md` |
|
|
20
|
+
| Error, crash, regression, screenshot-reported defect, test failure, stale cache, runtime boundary, why broken | hunt | `skills/hunt/SKILL.md` |
|
|
21
|
+
| Deep research, unfamiliar domain, compile sources into output | learn | `skills/learn/SKILL.md` |
|
|
22
|
+
| Any URL or PDF to fetch, read this, fetch this page | read | `skills/read/SKILL.md` |
|
|
23
|
+
| New feature, architecture, how should I design this, value judgment, executable plan, handoff | think | `skills/think/SKILL.md` |
|
|
24
|
+
| Writing, editing prose, polish, release notes, launch/social copy, remove AI tone | write | `skills/write/SKILL.md` |
|
|
25
|
+
<!-- routing-table:end -->
|
|
26
|
+
|
|
27
|
+
## How This Works
|
|
28
|
+
|
|
29
|
+
1. Read the user's message and match it to a skill from the table above.
|
|
30
|
+
2. Read the matched skill section in full.
|
|
31
|
+
3. Execute that skill's instructions exactly.
|
|
32
|
+
|
|
33
|
+
If the message could match multiple skills, use these disambiguation rules:
|
|
34
|
+
|
|
35
|
+
1. Most specific wins: `/design` is more specific than `/think` for UI decisions.
|
|
36
|
+
2. URL in message: start with `/read`. If the content is research material, chain to `/learn`.
|
|
37
|
+
3. Code already done vs. code broken: done/PR -> `/check`; error/broken -> `/hunt`.
|
|
38
|
+
4. Config/maintainability vs. code: Codex/Claude misbehaving, hooks/MCP, `/health` token usage, AI coding code rot, unclear context, missing verification, or stale verifier output -> `/health`; user code errors -> `/hunt`.
|
|
39
|
+
5. Release action vs. release prose: commit/tag/publish/push/release reactions/close issue -> `/check`; write release notes/changelog text -> `/write`.
|
|
40
|
+
6. Screenshot taste vs. screenshot regression: visual taste complaint -> `/design`; broken render/state/generated output or used-to-work evidence -> `/hunt`.
|
|
41
|
+
7. From scratch vs. editing: new long-form output -> `/learn`; existing draft to polish -> `/write`.
|
|
42
|
+
8. "Judge this" + error -> `/hunt`; "judge this" + should we keep it -> `/think`.
|
|
43
|
+
9. Still ambiguous: read both skills' "Not for" sections; use exclusion. If still unclear, ask the user.
|
|
44
|
+
|
|
45
|
+
## Path Resolution
|
|
46
|
+
|
|
47
|
+
In this distribution, sub-skill scripts live at `skills/{name}/scripts/`. Resolve all relative paths from this file's directory, not from a personal home-directory skill cache.
|
|
48
|
+
|
|
49
|
+
## Chaining
|
|
50
|
+
|
|
51
|
+
Skills chain manually, not automatically. Each skill completes and waits for the user's next action.
|
|
52
|
+
|
|
53
|
+
Common chains: `/think` -> implement approved plan -> `/check` | `/hunt` -> fix -> `/check` -> release/push/issue follow-through | `/read` -> `/learn` -> `/write`
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
OUT="${1:-"$ROOT/dist/waza.zip"}"
|
|
6
|
+
case "$OUT" in
|
|
7
|
+
/*) ;;
|
|
8
|
+
*) OUT="$ROOT/$OUT" ;;
|
|
9
|
+
esac
|
|
10
|
+
|
|
11
|
+
mkdir -p "$(dirname "$OUT")"
|
|
12
|
+
rm -f "$OUT"
|
|
13
|
+
|
|
14
|
+
cd "$ROOT"
|
|
15
|
+
|
|
16
|
+
MANIFEST="$(mktemp)"
|
|
17
|
+
FILTERED_MANIFEST="$(mktemp)"
|
|
18
|
+
STAGE="$(mktemp -d)"
|
|
19
|
+
trap 'rm -f "$MANIFEST" "$FILTERED_MANIFEST"; rm -rf "$STAGE"' EXIT
|
|
20
|
+
|
|
21
|
+
git ls-files --cached --others --exclude-standard > "$MANIFEST"
|
|
22
|
+
|
|
23
|
+
# Default-deny: only paths matching packaging.allowlist ship. Structural
|
|
24
|
+
# excludes (per-skill SKILL.md, __pycache__, etc.) live inside the filter.
|
|
25
|
+
python3 "$ROOT/scripts/packaging_filter.py" "$ROOT/packaging.allowlist" \
|
|
26
|
+
< "$MANIFEST" > "$FILTERED_MANIFEST"
|
|
27
|
+
|
|
28
|
+
tar -cf - -T "$FILTERED_MANIFEST" | (cd "$STAGE" && tar -xf -)
|
|
29
|
+
|
|
30
|
+
# Dispatcher body is codegen output: scripts/dispatcher.md. Source of truth is
|
|
31
|
+
# scripts/dispatcher-template.md plus SKILL.md frontmatter; regenerate with
|
|
32
|
+
# `make regenerate` if the template or any dispatch_intent changes.
|
|
33
|
+
if [ ! -f "$ROOT/scripts/dispatcher.md" ]; then
|
|
34
|
+
echo "ERROR: scripts/dispatcher.md missing; run 'make regenerate' first" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
cp "$ROOT/scripts/dispatcher.md" "$STAGE/SKILL.md"
|
|
38
|
+
|
|
39
|
+
find skills -mindepth 2 -maxdepth 2 -name SKILL.md | sort | while IFS= read -r path; do
|
|
40
|
+
skill="$(basename "$(dirname "$path")")"
|
|
41
|
+
{
|
|
42
|
+
printf '\n---\n\n# SKILL: %s\n\n' "$skill"
|
|
43
|
+
awk 'BEGIN{skip=0} /^---$/{if(NR==1){skip=1;next} if(skip){skip=0;next}} !skip' "$path"
|
|
44
|
+
} >> "$STAGE/SKILL.md"
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
perl -0pi -e 's#`skills/([a-z][a-z0-9_-]*)/SKILL\.md`#the **$1** section below#g' "$STAGE/SKILL.md"
|
|
48
|
+
find "$STAGE/skills" -type d -empty -delete 2>/dev/null || true
|
|
49
|
+
|
|
50
|
+
(cd "$STAGE" && find . -type f | sed 's#^\./##' | sort | zip -q "$OUT" -@)
|
|
51
|
+
|
|
52
|
+
if ! zipinfo -1 "$OUT" | awk '$0 == "SKILL.md" { found = 1 } END { exit found ? 0 : 1 }'; then
|
|
53
|
+
echo "ERROR: root SKILL.md missing from $OUT" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
SKILL_COUNT="$(zipinfo -1 "$OUT" | awk '$0 ~ /(^|\/)SKILL\.md$/ { count++ } END { print count + 0 }')"
|
|
58
|
+
if [ "$SKILL_COUNT" -ne 1 ]; then
|
|
59
|
+
echo "ERROR: expected exactly one SKILL.md in $OUT, found $SKILL_COUNT" >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
SIZE=$(wc -c < "$OUT" | tr -d ' ')
|
|
64
|
+
echo "OK: wrote $OUT (${SIZE} bytes)"
|
|
65
|
+
|
|
66
|
+
# Post-package validation lives in scripts/validate_package.py so it's
|
|
67
|
+
# py_compile-checked in CI and unit-testable.
|
|
68
|
+
VALIDATE_DIR="$(mktemp -d)"
|
|
69
|
+
trap 'rm -rf "$VALIDATE_DIR"' EXIT
|
|
70
|
+
unzip -q "$OUT" -d "$VALIDATE_DIR"
|
|
71
|
+
python3 "$ROOT/scripts/validate_package.py" "$VALIDATE_DIR"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Filter stdin paths through packaging.allowlist. Default-deny."""
|
|
3
|
+
|
|
4
|
+
import fnmatch
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
EXCLUDE_RE = re.compile(
|
|
10
|
+
r"(^skills/[^/]+/SKILL\.md$"
|
|
11
|
+
r"|(^|/)__pycache__/"
|
|
12
|
+
r"|\.pyc$"
|
|
13
|
+
r"|(^|/)\.DS_Store$)"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_patterns(path: Path) -> list[str]:
|
|
18
|
+
patterns = []
|
|
19
|
+
for raw in path.read_text().splitlines():
|
|
20
|
+
line = raw.strip()
|
|
21
|
+
if not line or line.startswith("#"):
|
|
22
|
+
continue
|
|
23
|
+
patterns.append(line)
|
|
24
|
+
return patterns
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def allowed(path: str, patterns: list[str]) -> bool:
|
|
28
|
+
for pat in patterns:
|
|
29
|
+
if pat.endswith("/**"):
|
|
30
|
+
prefix = pat[:-3]
|
|
31
|
+
if path == prefix or path.startswith(prefix + "/"):
|
|
32
|
+
return True
|
|
33
|
+
elif fnmatch.fnmatch(path, pat):
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> int:
|
|
39
|
+
if len(sys.argv) != 2:
|
|
40
|
+
print("usage: packaging_filter.py <allowlist-path>", file=sys.stderr)
|
|
41
|
+
return 2
|
|
42
|
+
patterns = load_patterns(Path(sys.argv[1]))
|
|
43
|
+
for line in sys.stdin:
|
|
44
|
+
path = line.rstrip("\n")
|
|
45
|
+
if not path:
|
|
46
|
+
continue
|
|
47
|
+
if EXCLUDE_RE.search(path):
|
|
48
|
+
continue
|
|
49
|
+
if allowed(path, patterns):
|
|
50
|
+
print(path)
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
raise SystemExit(main())
|