Skip to content

Commit ce8817d

Browse files
geledekclaude
andcommitted
scripts: add live golden-prompt runner; plugin.json: fix invalid manifest schema
- run_golden_prompts.py executes tests/golden-prompts.md through headless claude -p --plugin-dir: activation, routing, dormancy, and (--full) verdict vocabulary. Distinguishes errored runs from dormant skills; paces runs for rate-limit headroom. - plugin.json failed current Claude Code schema validation (author must be an object; skills paths must be ./-prefixed), silently dropping all 16 skills on --plugin-dir loads. Found by the runner's first live run. - .github/ISSUE_TEMPLATE/wrong-verdict.yml: structured field-report intake — every confirmed report becomes a regression case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d021490 commit ce8817d

5 files changed

Lines changed: 301 additions & 18 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"displayName": "Enterprise AI Transformation Skills",
44
"version": "0.5.0",
55
"description": "Sixteen installable skills for diagnosing, designing, governing, and scaling enterprise AI transformation. Distilled from 21 flagship sources (Stanford, MIT, McKinsey, BCG, Deloitte, PwC, Accenture, NIST, EU AI Act, IMDA, WEF).",
6-
"author": "Ray Han",
6+
"author": {
7+
"name": "Ray Han",
8+
"email": "ray@rayhan.ai"
9+
},
710
"license": "MIT",
811
"homepage": "https://github.com/geledek/enterprise-ai-transformation-skills",
912
"repository": "https://github.com/geledek/enterprise-ai-transformation-skills",
@@ -19,21 +22,21 @@
1922
"decision-frameworks"
2023
],
2124
"skills": [
22-
"skills/general-idea-diagnostic",
23-
"skills/general-use-case-discovery",
24-
"skills/general-maturity-assessment",
25-
"skills/general-peer-cases",
26-
"skills/general-roi-gate",
27-
"skills/process-pilot-design",
28-
"skills/process-productionization",
29-
"skills/process-portfolio-observability",
30-
"skills/tech-stack-diagnostic",
31-
"skills/tech-buy-vs-build",
32-
"skills/tech-data-deployment",
33-
"skills/tech-agent-guardrail",
34-
"skills/people-readiness-conversation",
35-
"skills/people-literacy-curriculum",
36-
"skills/people-frontline-engagement",
37-
"skills/people-tool-selection"
25+
"./skills/general-idea-diagnostic",
26+
"./skills/general-use-case-discovery",
27+
"./skills/general-maturity-assessment",
28+
"./skills/general-peer-cases",
29+
"./skills/general-roi-gate",
30+
"./skills/process-pilot-design",
31+
"./skills/process-productionization",
32+
"./skills/process-portfolio-observability",
33+
"./skills/tech-stack-diagnostic",
34+
"./skills/tech-buy-vs-build",
35+
"./skills/tech-data-deployment",
36+
"./skills/tech-agent-guardrail",
37+
"./skills/people-readiness-conversation",
38+
"./skills/people-literacy-curriculum",
39+
"./skills/people-frontline-engagement",
40+
"./skills/people-tool-selection"
3841
]
3942
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Wrong verdict report
2+
description: A skill produced a verdict that didn't match reality. This is the most valuable feedback this repo can receive — every confirmed report becomes a regression case.
3+
title: "[wrong-verdict] <skill>: <one-line summary>"
4+
labels: ["wrong-verdict"]
5+
body:
6+
- type: dropdown
7+
id: skill
8+
attributes:
9+
label: Which skill?
10+
options:
11+
- general-idea-diagnostic
12+
- general-use-case-discovery
13+
- general-maturity-assessment
14+
- general-peer-cases
15+
- general-roi-gate
16+
- process-pilot-design
17+
- process-productionization
18+
- process-portfolio-observability
19+
- tech-stack-diagnostic
20+
- tech-buy-vs-build
21+
- tech-data-deployment
22+
- tech-agent-guardrail
23+
- people-readiness-conversation
24+
- people-literacy-curriculum
25+
- people-frontline-engagement
26+
- people-tool-selection
27+
validations:
28+
required: true
29+
- type: textarea
30+
id: situation
31+
attributes:
32+
label: The situation you ran it on
33+
description: Anonymized — pattern over personality (no company or person names needed). Enough detail that someone else could reproduce the run.
34+
placeholder: "Training cohort of ~40 nurses, hospital on Microsoft 365, no budget, patient data in scope…"
35+
validations:
36+
required: true
37+
- type: input
38+
id: verdict-given
39+
attributes:
40+
label: Verdict the skill gave
41+
placeholder: "Adopt-now"
42+
validations:
43+
required: true
44+
- type: textarea
45+
id: what-happened
46+
attributes:
47+
label: What actually happened (or what you expected and why)
48+
description: If you followed the verdict — what was the outcome? If you overrode it — what did the skill miss?
49+
validations:
50+
required: true
51+
- type: input
52+
id: model
53+
attributes:
54+
label: Model and host you ran it on
55+
placeholder: "Claude Opus 4.8 via Claude Code plugin / pasted into ChatGPT / …"
56+
- type: checkboxes
57+
id: confirm
58+
attributes:
59+
label: Confirmation
60+
options:
61+
- label: The situation above is anonymized — no confidential or personal data.
62+
required: true

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
All notable changes to this plugin are documented here.
44

5+
## [Unreleased]
6+
7+
### Added
8+
- `scripts/run_golden_prompts.py` — live golden-prompt runner. Executes every activation and routing prompt from `tests/golden-prompts.md` through headless `claude -p --plugin-dir`, asserting the right skill activates, adjacent skills stay dormant, and (in `--full` mode) the output uses the contracted verdict vocabulary. Costs API tokens — run before releases, not in CI. Supports `--model` for cross-model checks.
9+
- "Wrong verdict report" issue template (`.github/ISSUE_TEMPLATE/wrong-verdict.yml`) — structured field-report intake; every confirmed report becomes a regression case.
10+
11+
### Fixed
12+
- **`plugin.json` rejected by current Claude Code.** The manifest failed schema validation (`author` must be an object, not a string; `skills` paths must be `./`-prefixed), so `--plugin-dir` loads — and potentially fresh plugin-manager installs — silently dropped all 16 skills. Found by the first live run of the golden-prompt runner.
13+
514
## [0.5.0] — 2026-07-02
615

716
### Added

CONTRIBUTING.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,16 @@ It checks that every reference pointer resolves, `references/_index.md`
7272
matches actual citations, the plugin manifests match the `skills/`
7373
directories, and frontmatter is well-formed. If you changed a skill's
7474
behavior, re-run its row from `tests/golden-prompts.md` and confirm the
75-
verdict vocabulary is unchanged.
75+
verdict vocabulary is unchanged — or run it live:
76+
77+
```bash
78+
python3 scripts/run_golden_prompts.py --only <your-skill> # activation
79+
python3 scripts/run_golden_prompts.py --routing --only <your-skill> # routing
80+
```
81+
82+
(This calls the API through headless `claude -p`, so it costs tokens and is
83+
not run by CI. If you changed a frontmatter `description`, run the routing
84+
mode — descriptions are what Claude routes on.)
7685

7786
## Commit style
7887

scripts/run_golden_prompts.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
#!/usr/bin/env python3
2+
"""Golden-prompt runner. Executes tests/golden-prompts.md against a live model.
3+
4+
Runs each prompt through `claude -p` with this repo loaded via --plugin-dir,
5+
then asserts:
6+
- activation rows (the per-bucket tables): the named skill activates, and in
7+
--full mode the output uses the contracted verdict vocabulary;
8+
- routing rows ("must NOT misfire" table): the intended skill activates and
9+
the adjacent skill does not; the dormancy row activates nothing.
10+
11+
This costs API tokens and takes minutes — it is NOT run by CI on push.
12+
Run it before cutting a release, or after editing any frontmatter description
13+
(routing) or role structure (contracts).
14+
15+
Usage:
16+
python3 scripts/run_golden_prompts.py --list # parse and show cases, no API calls
17+
python3 scripts/run_golden_prompts.py --routing # routing table only (cheap: capped turns)
18+
python3 scripts/run_golden_prompts.py --full # everything, uncapped, checks verdict vocab
19+
python3 scripts/run_golden_prompts.py --only people-tool-selection,general-roi-gate
20+
python3 scripts/run_golden_prompts.py --model sonnet # cross-model run
21+
"""
22+
23+
from __future__ import annotations
24+
25+
import argparse
26+
import json
27+
import os
28+
import re
29+
import subprocess
30+
import sys
31+
import tempfile
32+
import time
33+
34+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
35+
GOLDEN = os.path.join(ROOT, "tests", "golden-prompts.md")
36+
37+
# Read-only tools only: skills may consult references/, but must not mutate.
38+
ALLOWED_TOOLS = "Skill Read Glob Grep"
39+
40+
41+
def parse_cases():
42+
"""Return (activation_cases, routing_cases) from golden-prompts.md."""
43+
activation, routing = [], []
44+
in_routing = False
45+
for line in open(GOLDEN):
46+
if line.startswith("## Routing prompts"):
47+
in_routing = True
48+
continue
49+
if not line.startswith("|") or line.startswith("|-") or line.startswith("|---"):
50+
continue
51+
cells = [c.strip() for c in line.strip().strip("|").split("|")]
52+
if in_routing:
53+
if len(cells) < 3 or cells[0] in ("Prompt", "---"):
54+
continue
55+
prompt = cells[0].strip('"')
56+
must = m.group(1) if (m := re.search(r"`([a-z0-9-]+)`", cells[1])) else None
57+
must_not = m.group(1) if (m := re.search(r"`([a-z0-9-]+)`", cells[2])) else "ANY"
58+
routing.append({"prompt": prompt, "must": must, "must_not": must_not})
59+
else:
60+
m = re.match(r"`([a-z0-9-]+)`", cells[0])
61+
if not m or len(cells) < 3:
62+
continue
63+
prompt = cells[1].strip('"')
64+
vocab_groups = [
65+
[opt.strip() for opt in grp.split(" / ")]
66+
for grp in re.findall(r"\*\*(.+?)\*\*", cells[2])
67+
]
68+
activation.append({"skill": m.group(1), "prompt": prompt, "vocab": vocab_groups})
69+
return activation, routing
70+
71+
72+
def run_claude(prompt: str, model: str | None, max_turns: int | None, timeout: int):
73+
"""Run one headless prompt. Returns (activated_skills, result_text, error)."""
74+
cmd = [
75+
"claude", "-p", prompt,
76+
"--plugin-dir", ROOT,
77+
"--output-format", "stream-json", "--verbose",
78+
"--allowedTools", ALLOWED_TOOLS,
79+
]
80+
if model:
81+
cmd += ["--model", model]
82+
if max_turns:
83+
cmd += ["--max-turns", str(max_turns)]
84+
# Neutral cwd: don't let this repo's CLAUDE.md leak into the routing test.
85+
with tempfile.TemporaryDirectory() as neutral:
86+
try:
87+
proc = subprocess.run(
88+
cmd, capture_output=True, text=True, timeout=timeout, cwd=neutral
89+
)
90+
except subprocess.TimeoutExpired:
91+
return set(), "", "timeout"
92+
if proc.returncode != 0 and not proc.stdout:
93+
return set(), "", (proc.stderr or "claude exited non-zero").strip()[:300]
94+
95+
activated, result_text, result_error = set(), "", None
96+
for raw in proc.stdout.splitlines():
97+
try:
98+
event = json.loads(raw)
99+
except json.JSONDecodeError:
100+
continue
101+
if event.get("type") == "assistant":
102+
for block in event.get("message", {}).get("content", []):
103+
if block.get("type") == "tool_use" and block.get("name") == "Skill":
104+
activated.add(block.get("input", {}).get("skill", "").split(":")[-1])
105+
elif event.get("type") == "result":
106+
result_text = event.get("result") or ""
107+
if event.get("is_error") or event.get("subtype") != "success":
108+
# A failed run must not masquerade as "skill stayed dormant".
109+
result_error = f"{event.get('subtype')}: {result_text[:200]}"
110+
return activated, result_text, result_error
111+
112+
113+
def vocab_hit(options: list[str], text: str) -> bool:
114+
"""True if any option appears in text (hyphen/space interchangeable)."""
115+
for opt in options:
116+
pattern = re.escape(opt).replace(r"\-", "[-\\s]").replace(r"\ ", "[-\\s]")
117+
if re.search(rf"\b{pattern}\b", text):
118+
return True
119+
return False
120+
121+
122+
def main() -> int:
123+
ap = argparse.ArgumentParser(description=__doc__)
124+
ap.add_argument("--list", action="store_true", help="show parsed cases, no API calls")
125+
ap.add_argument("--routing", action="store_true", help="routing table only")
126+
ap.add_argument("--full", action="store_true", help="activation + verdict vocabulary, uncapped")
127+
ap.add_argument("--only", help="comma-separated skill slugs to test")
128+
ap.add_argument("--model", help="model alias to run against (default: your configured model)")
129+
ap.add_argument("--sleep", type=int, default=15, help="seconds between runs (rate-limit headroom)")
130+
args = ap.parse_args()
131+
132+
activation, routing = parse_cases()
133+
if args.only:
134+
keep = set(args.only.split(","))
135+
activation = [c for c in activation if c["skill"] in keep]
136+
routing = [c for c in routing if c["must"] in keep or c["must_not"] in keep]
137+
138+
if args.list:
139+
for c in activation:
140+
print(f"activate {c['skill']:35s} vocab groups: {len(c['vocab'])}")
141+
for c in routing:
142+
print(f"route {str(c['must']):35s} not: {c['must_not']}")
143+
print(f"\n{len(activation)} activation + {len(routing)} routing cases")
144+
return 0
145+
146+
failures = []
147+
run_activation = not args.routing
148+
149+
if run_activation:
150+
for c in activation:
151+
max_turns = None if args.full else 3
152+
timeout = 600 if args.full else 180
153+
activated, text, error = run_claude(c["prompt"], args.model, max_turns, timeout)
154+
time.sleep(args.sleep)
155+
if error:
156+
failures.append(f"{c['skill']}: run failed ({error})")
157+
print(f"ERROR {c['skill']}: {error}")
158+
continue
159+
ok = c["skill"] in activated
160+
if not ok:
161+
failures.append(f"{c['skill']}: did not activate (activated: {sorted(activated) or 'none'})")
162+
if args.full and ok:
163+
for group in c["vocab"]:
164+
if not vocab_hit(group, text):
165+
ok = False
166+
failures.append(f"{c['skill']}: output missing vocabulary {group}")
167+
print(f"{'PASS' if ok else 'FAIL'} activate {c['skill']}")
168+
169+
for c in routing:
170+
activated, _, error = run_claude(c["prompt"], args.model, 3, 180)
171+
time.sleep(args.sleep)
172+
if error:
173+
failures.append(f"routing '{c['prompt'][:40]}…': run failed ({error})")
174+
print(f"ERROR routing '{c['prompt'][:40]}…': {error}")
175+
continue
176+
ok = True
177+
if c["must"] and c["must"] not in activated:
178+
ok = False
179+
failures.append(f"routing '{c['prompt'][:40]}…': {c['must']} did not activate ({sorted(activated) or 'none'})")
180+
if c["must_not"] == "ANY":
181+
if activated:
182+
ok = False
183+
failures.append(f"dormancy '{c['prompt'][:40]}…': activated {sorted(activated)}")
184+
elif c["must_not"] in activated:
185+
ok = False
186+
failures.append(f"routing '{c['prompt'][:40]}…': misfired {c['must_not']}")
187+
print(f"{'PASS' if ok else 'FAIL'} route {c['must'] or '(dormancy)'}")
188+
189+
print()
190+
if failures:
191+
print(f"FAIL — {len(failures)} issue(s):")
192+
for f in failures:
193+
print(f" - {f}")
194+
return 1
195+
print("OK — all golden prompts passed")
196+
return 0
197+
198+
199+
if __name__ == "__main__":
200+
sys.exit(main())

0 commit comments

Comments
 (0)