master-skill 0.10.1 → 0.12.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 (77) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/GEMINI.md +1 -1
  5. package/README.md +84 -336
  6. package/README_EN.md +108 -321
  7. package/bin/cli.mjs +249 -9
  8. package/gemini-extension.json +1 -1
  9. package/hooks/session-start +68 -74
  10. package/hooks/session_start.py +152 -0
  11. package/package.json +6 -2
  12. package/prebuilt/{compare → compare-masters}/SKILL.md +21 -2
  13. package/prebuilt/master-ajahn-chah/meta.json +6 -0
  14. package/prebuilt/master-ajahn-chah/tests/fidelity.jsonl +6 -6
  15. package/prebuilt/master-atisha/tests/fidelity.jsonl +4 -4
  16. package/prebuilt/master-curriculum/SKILL.md +1 -1
  17. package/prebuilt/master-curriculum/references/tiantai.md +1 -1
  18. package/prebuilt/master-debate/SKILL.md +15 -3
  19. package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
  20. package/prebuilt/master-help/SKILL.md +94 -0
  21. package/prebuilt/master-help/tests/fidelity.jsonl +10 -0
  22. package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
  23. package/prebuilt/master-kumarajiva/meta.json +14 -3
  24. package/prebuilt/master-kumarajiva/tests/fidelity.jsonl +3 -3
  25. package/prebuilt/master-mahasi-sayadaw/tests/fidelity.jsonl +4 -4
  26. package/prebuilt/master-milarepa/tests/fidelity.jsonl +3 -3
  27. package/prebuilt/master-nagarjuna/meta.json +19 -4
  28. package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +6 -6
  29. package/prebuilt/master-ouyi/meta.json +5 -0
  30. package/prebuilt/master-ouyi/references/teaching.md +3 -3
  31. package/prebuilt/master-ouyi/tests/fidelity.jsonl +3 -3
  32. package/prebuilt/master-tsongkhapa/meta.json +32 -5
  33. package/prebuilt/master-tsongkhapa/tests/fidelity.jsonl +2 -2
  34. package/prebuilt/master-xuanzang/tests/fidelity.jsonl +3 -3
  35. package/prebuilt/master-xuyun/tests/fidelity.jsonl +6 -6
  36. package/prebuilt/master-zhiyi/meta.json +2 -2
  37. package/prebuilt/master-zhiyi/tests/fidelity.jsonl +2 -2
  38. package/references/teaching-modes.md +8 -1
  39. package/routing.json +209 -0
  40. package/scripts/check-audit-ignores.py +105 -0
  41. package/scripts/check-eval-sdk-surface.py +142 -0
  42. package/scripts/check-gate-liveness.py +421 -0
  43. package/scripts/reaudit-report.py +163 -0
  44. package/scripts/regrade-report.py +157 -0
  45. package/scripts/smoke-eval-sdk.py +174 -0
  46. package/scripts/test-fidelity.py +992 -89
  47. package/scripts/validate-citation-references.py +150 -0
  48. package/scripts/validate-citation-templates.py +176 -0
  49. package/scripts/validate-fidelity.py +6 -1
  50. package/scripts/validate-fixture-terms.py +127 -0
  51. package/scripts/validate-routing.py +254 -0
  52. package/scripts/validate.py +63 -36
  53. package/scripts/verify-adjudication.py +316 -0
  54. package/scripts/verify_citations.py +739 -39
  55. package/skill-catalog.json +83 -20
  56. package/tools/cross_reference.py +44 -10
  57. package/tools/fojin-known-absent.json +14 -0
  58. package/tools/fojin_bridge.py +138 -8
  59. package/tools/rag_query.py +45 -2
  60. package/tools/skill_writer.py +50 -7
  61. package/tools/verify_sources.py +240 -15
  62. package/hooks/tests/test_run_hook.sh +0 -114
  63. package/hooks/tests/test_run_hook_cmd.sh +0 -94
  64. package/hooks/tests/test_session_start.sh +0 -149
  65. package/scripts/tests/test_check_manifest_versions.py +0 -217
  66. package/scripts/tests/test_debate_protocol.py +0 -159
  67. package/scripts/tests/test_injection_hardening.py +0 -174
  68. package/scripts/tests/test_select_fidelity_smoke.py +0 -142
  69. package/scripts/tests/test_validate_citation_contract.py +0 -408
  70. package/scripts/tests/test_validate_cross_critique.py +0 -149
  71. package/scripts/tests/test_validate_curriculum_sources.py +0 -144
  72. package/scripts/tests/test_validate_fidelity.py +0 -59
  73. package/scripts/tests/test_validate_lore_triggers_content.py +0 -372
  74. package/scripts/tests/test_validate_persona_fidelity.py +0 -317
  75. package/scripts/tests/test_validate_promptfoo_configs.py +0 -386
  76. package/scripts/tests/test_validate_workflow.py +0 -265
  77. /package/prebuilt/{compare → compare-masters}/tests/fidelity.jsonl +0 -0
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env python3
2
+ """Re-grade a committed run against the current judge and fixtures. Offline, free.
3
+
4
+ `check_response` is deterministic, and every answer has been stored since PR
5
+ #142. So any change to the judge or to the fixtures can be measured against an
6
+ already-paid-for run for nothing, instead of claimed.
7
+
8
+ That matters most for changes that *relax* something. Moving a requirement into
9
+ `must_convey` — "the substring matcher cannot decide this" — is honest when the
10
+ adjudication says so and is laundering when it does not. Re-grading the ¥3.89
11
+ sweep shows exactly which cases move and whether anything moved the wrong way.
12
+
13
+ python3 scripts/regrade-report.py eval/reports/0.11.0-06b8142-deepseek.json
14
+
15
+ Results are joined to fixtures **by question text**, never by position: a
16
+ fixture added or removed shifts every index after it, and grading an answer
17
+ against someone else's question would produce a confident, meaningless number.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
26
+
27
+ import importlib.util
28
+
29
+ ROOT = Path(__file__).resolve().parent.parent
30
+ _spec = importlib.util.spec_from_file_location(
31
+ "_fidelity", ROOT / "scripts" / "test-fidelity.py"
32
+ )
33
+ _fidelity = importlib.util.module_from_spec(_spec)
34
+ sys.modules["_fidelity"] = _fidelity
35
+ _spec.loader.exec_module(_fidelity)
36
+
37
+ from verify_citations import load_declared_ids, load_member_aliases # noqa: E402
38
+
39
+
40
+ def load_fixtures() -> dict[str, list[dict]]:
41
+ fixtures: dict[str, list[dict]] = {}
42
+ for path in sorted((ROOT / "prebuilt").glob("*/tests/fidelity.jsonl")):
43
+ fixtures[path.parent.parent.name] = [
44
+ json.loads(line) for line in path.read_text().splitlines() if line.strip()
45
+ ]
46
+ return fixtures
47
+
48
+
49
+ def regrade(report: dict, fixtures: dict[str, list[dict]]) -> dict:
50
+ """Re-run the judge over every stored answer. Raises if the run stored none."""
51
+ if not any(
52
+ "response" in result
53
+ for suite in report["suites"]
54
+ for result in suite["results"]
55
+ ):
56
+ raise ValueError("no stored answers in this report — nothing to re-grade")
57
+
58
+ cases: list[dict] = []
59
+ graded_results: list[dict] = []
60
+ by_test_type: dict[str, dict[str, int]] = {}
61
+
62
+ for suite in report["suites"]:
63
+ master = suite["master"]
64
+ cases_for_master = fixtures.get(master, [])
65
+ try:
66
+ declared = load_declared_ids(master) or None
67
+ aliases = load_member_aliases(master) or None
68
+ except (FileNotFoundError, ValueError):
69
+ declared = None
70
+ aliases = None
71
+
72
+ for result in suite["results"]:
73
+ if result.get("status") in ("truncated", "api_error"):
74
+ continue
75
+ index = result["index"]
76
+ fixture = cases_for_master[index] if index < len(cases_for_master) else None
77
+ if fixture is None or fixture.get("q") != result["question"]:
78
+ raise ValueError(
79
+ f"{master} #{index}: the fixture at this position does not match "
80
+ f"the question that was graded. Fixtures moved; re-grading by "
81
+ f"position would compare an answer with someone else's question."
82
+ )
83
+ check = _fidelity.check_response(
84
+ result.get("response") or "",
85
+ fixture,
86
+ declared_ids=declared,
87
+ member_aliases=aliases,
88
+ )
89
+ entry = _fidelity.result_entry(index, fixture, check, result.get("response") or "")
90
+ graded_results.append(entry)
91
+ was = result["status"]
92
+ now = "PASS" if check["passed"] else "FAIL"
93
+ bucket = by_test_type.setdefault(
94
+ result["test_type"], {"graded": 0, "was": 0, "now": 0}
95
+ )
96
+ bucket["graded"] += 1
97
+ bucket["was"] += was == "PASS"
98
+ bucket["now"] += now == "PASS"
99
+ cases.append(
100
+ {
101
+ "master": master,
102
+ "index": index,
103
+ "test_type": result["test_type"],
104
+ "was": was,
105
+ "now": now,
106
+ "needs_review": check["needs_review"],
107
+ }
108
+ )
109
+
110
+ return {
111
+ "meta": report.get("meta", {}),
112
+ "cases": cases,
113
+ "by_test_type": by_test_type,
114
+ "mentions": _fidelity.summarize_mentions(graded_results),
115
+ "needs_review": sum(1 for c in cases if c["needs_review"]),
116
+ }
117
+
118
+
119
+ def main(argv: list[str]) -> int:
120
+ if len(argv) != 2:
121
+ print(f"usage: {Path(argv[0]).name} <eval/reports/*.json>")
122
+ return 2
123
+ path = Path(argv[1])
124
+ out = regrade(json.loads(path.read_text()), load_fixtures())
125
+ meta = out["meta"]
126
+ print(f"{path.name} — commit {meta.get('commit_short')}, model {meta.get('model')}")
127
+ print(f"\n{'test_type':12} {'graded':>7} {'as graded':>11} {'re-graded':>11}")
128
+ total = {"graded": 0, "was": 0, "now": 0}
129
+ for test_type, b in sorted(out["by_test_type"].items()):
130
+ for key in total:
131
+ total[key] += b[key]
132
+ print(f"{test_type:12} {b['graded']:>7} "
133
+ f"{b['was']:>6} {b['was']/b['graded']:>4.0%} "
134
+ f"{b['now']:>6} {b['now']/b['graded']:>4.0%}")
135
+ print(f"{'TOTAL':12} {total['graded']:>7} "
136
+ f"{total['was']:>6} {total['was']/total['graded']:>4.0%} "
137
+ f"{total['now']:>6} {total['now']/total['graded']:>4.0%}")
138
+
139
+ m = out["mentions"]
140
+ print(
141
+ f"\nmention coverage {m['mentions_decided']}/{m['mention_requirements']} "
142
+ f"= {m['mention_coverage']} "
143
+ f"({m['mentions_unverified']} undecidable, "
144
+ f"{m['script_mismatches']} answers in the wrong script)"
145
+ )
146
+ print(f"cases needing adjudication: {out['needs_review']}")
147
+
148
+ moved_wrong = [c for c in out["cases"] if c["was"] == "PASS" and c["now"] == "FAIL"]
149
+ if moved_wrong:
150
+ print(f"\nWARNING: {len(moved_wrong)} case(s) that passed now fail:")
151
+ for c in moved_wrong:
152
+ print(f" {c['master']} #{c['index']} ({c['test_type']})")
153
+ return 0
154
+
155
+
156
+ if __name__ == "__main__":
157
+ sys.exit(main(sys.argv))
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env python3
2
+ """Keyless end-to-end smoke for the pinned eval SDKs.
3
+
4
+ `check-eval-sdk-surface.py` reads signatures. That catches a renamed keyword,
5
+ not a changed wire format, a response model that parses differently, or a
6
+ client that stops honouring its base URL. And the real path —
7
+ `scripts/test-fidelity.py` — never runs in CI, because grading needs an API
8
+ key and there is none. So every Dependabot bump of `anthropic` or `openai`
9
+ arrived green having exercised nothing, which `requirements-eval.txt` says in
10
+ as many words.
11
+
12
+ This runs that real path without a key or a network. A local HTTP server
13
+ answers in each provider's own wire format; `test-fidelity.py` builds its
14
+ request, the pinned SDK sends and parses it, and the answer goes through
15
+ grading and the citation audit exactly as in a sweep. Then it checks what
16
+ arrived at the server and what came back.
17
+
18
+ python3 scripts/smoke-eval-sdk.py # must exit 0
19
+ python3 scripts/smoke-eval-sdk.py --break # must exit 1
20
+
21
+ `--break` makes the server return a reply with no answer text while every
22
+ assertion stays the same. CI runs both: a smoke that cannot fail is a green
23
+ tick, not a check.
24
+
25
+ First written 2026-09-14 to verify anthropic 1.4.0 → 1.5.0 and openai
26
+ 3.10.0 → 3.13.0 (#174, #175).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import contextlib
32
+ import importlib.util
33
+ import io
34
+ import json
35
+ import os
36
+ import sys
37
+ import threading
38
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
39
+ from pathlib import Path
40
+
41
+ REPO = Path(__file__).resolve().parent.parent
42
+ ANSWER = "菩提自性,本来清净,但用此心,直了成佛。【《六祖大师法宝坛经》行由品,T48n2008】"
43
+ CACHE_CREATED = 6700
44
+
45
+
46
+ def _server(broken: bool, received: list) -> ThreadingHTTPServer:
47
+ class Handler(BaseHTTPRequestHandler):
48
+ def log_message(self, *args): # keep CI logs quiet
49
+ pass
50
+
51
+ def do_POST(self):
52
+ length = int(self.headers.get("content-length", 0))
53
+ body = json.loads(self.rfile.read(length) or b"{}")
54
+ headers = {k.lower(): v for k, v in self.headers.items()}
55
+ received.append((self.path, body, headers))
56
+ if self.path.endswith("/messages"):
57
+ reply = {
58
+ "id": "msg_smoke", "type": "message", "role": "assistant",
59
+ "model": body.get("model"),
60
+ "content": [] if broken else [{"type": "text", "text": ANSWER}],
61
+ "stop_reason": "end_turn", "stop_sequence": None,
62
+ "usage": {
63
+ "input_tokens": 21, "output_tokens": 40,
64
+ "cache_creation_input_tokens": CACHE_CREATED,
65
+ "cache_read_input_tokens": 0,
66
+ },
67
+ }
68
+ else:
69
+ reply = {
70
+ "id": "chatcmpl-smoke", "object": "chat.completion", "created": 1,
71
+ "model": body.get("model"),
72
+ "choices": [{
73
+ "index": 0, "finish_reason": "stop",
74
+ "message": {"role": "assistant", "content": None if broken else ANSWER},
75
+ }],
76
+ "usage": {"prompt_tokens": 21, "completion_tokens": 40, "total_tokens": 61},
77
+ }
78
+ data = json.dumps(reply).encode()
79
+ self.send_response(200)
80
+ self.send_header("content-type", "application/json")
81
+ self.send_header("content-length", str(len(data)))
82
+ self.end_headers()
83
+ self.wfile.write(data)
84
+
85
+ return ThreadingHTTPServer(("127.0.0.1", 0), Handler)
86
+
87
+
88
+ def main(argv: list[str]) -> int:
89
+ broken = "--break" in argv
90
+ try:
91
+ import anthropic
92
+ import openai
93
+ except ImportError as error:
94
+ # Missing is a failure, not a skip: this only runs where
95
+ # requirements-eval.txt is installed, and silence here is how a bump
96
+ # used to pass.
97
+ print(f"✗ eval SDK not importable: {error} (pip install -r requirements-eval.txt)")
98
+ return 1
99
+
100
+ received: list = []
101
+ server = _server(broken, received)
102
+ port = server.server_address[1]
103
+ threading.Thread(target=server.serve_forever, daemon=True).start()
104
+
105
+ # A proxy variable would send 127.0.0.1 somewhere else; a real key must
106
+ # never be read by a smoke that is supposed to cost nothing.
107
+ for key in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"):
108
+ os.environ.pop(key, None)
109
+ os.environ.update({
110
+ "ANTHROPIC_API_KEY": "sk-ant-smoke",
111
+ "DEEPSEEK_API_KEY": "sk-smoke",
112
+ "ANTHROPIC_BASE_URL": f"http://127.0.0.1:{port}",
113
+ "NO_PROXY": "127.0.0.1,localhost",
114
+ })
115
+ os.chdir(REPO)
116
+ sys.path.insert(0, str(REPO / "scripts"))
117
+ spec = importlib.util.spec_from_file_location("test_fidelity", REPO / "scripts" / "test-fidelity.py")
118
+ fidelity = importlib.util.module_from_spec(spec)
119
+ spec.loader.exec_module(fidelity)
120
+ fidelity.PROVIDERS["deepseek"]["base_url"] = f"http://127.0.0.1:{port}/v1"
121
+
122
+ print(f"eval SDK smoke — anthropic {anthropic.__version__}, openai {openai.__version__}"
123
+ f"{' (--break: server returns no answer text)' if broken else ''}")
124
+ failures = 0
125
+
126
+ def check(label: str, ok: bool) -> None:
127
+ nonlocal failures
128
+ print(f" {'✓' if ok else '✗'} {label}")
129
+ failures += 0 if ok else 1
130
+
131
+ for provider, extra in (("anthropic", []), ("deepseek", ["--model", "deepseek-v4-flash"])):
132
+ received.clear()
133
+ sys.argv = ["test-fidelity.py", "--master", "master-huineng", "--provider", provider,
134
+ "--max-tests", "1", "--concurrency", "1", "--json", *extra]
135
+ out = io.StringIO()
136
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()):
137
+ fidelity.main()
138
+ suite = json.loads(out.getvalue())[0]
139
+ result = suite["results"][0]
140
+ print(f"── {provider}: {result.get('status')}")
141
+
142
+ check("the request reached the local server, not the internet", bool(received))
143
+ if not received:
144
+ continue
145
+ path, body, headers = received[0]
146
+ check("the reply was graded, not recorded as api_error",
147
+ result.get("status") in ("PASS", "FAIL"))
148
+ check("the answer text survived SDK parsing intact", result.get("response") == ANSWER)
149
+ check("the citation audit ran on it",
150
+ (suite.get("audit") or {}).get("citations_checked", 0) >= 1)
151
+ check("model, max_tokens and messages were sent",
152
+ {"model", "max_tokens", "messages"} <= set(body))
153
+ if provider == "anthropic":
154
+ check(f"sent to /v1/messages (got {path})", path == "/v1/messages")
155
+ check("the persona prompt carries cache_control",
156
+ (body.get("system") or [{}])[0].get("cache_control") == {"type": "ephemeral"})
157
+ check("cache token usage was read back",
158
+ (suite.get("cache") or {}).get("created") == CACHE_CREATED)
159
+ check("the pinned SDK is the one that sent it",
160
+ anthropic.__version__ in headers.get("user-agent", ""))
161
+ else:
162
+ check(f"sent to /v1/chat/completions (got {path})", path == "/v1/chat/completions")
163
+ check("the key went in the Authorization header",
164
+ headers.get("authorization") == "Bearer sk-smoke")
165
+ check("the pinned SDK is the one that sent it",
166
+ openai.__version__ in headers.get("user-agent", ""))
167
+
168
+ server.shutdown()
169
+ print(f"{'✓ eval SDK path intact' if not failures else f'✗ {failures} check(s) failed'}")
170
+ return 1 if failures else 0
171
+
172
+
173
+ if __name__ == "__main__":
174
+ sys.exit(main(sys.argv[1:]))