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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/GEMINI.md +1 -1
- package/README.md +84 -336
- package/README_EN.md +108 -321
- package/bin/cli.mjs +249 -9
- package/gemini-extension.json +1 -1
- package/hooks/session-start +68 -74
- package/hooks/session_start.py +152 -0
- package/package.json +6 -2
- package/prebuilt/{compare → compare-masters}/SKILL.md +21 -2
- package/prebuilt/master-ajahn-chah/meta.json +6 -0
- package/prebuilt/master-ajahn-chah/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-atisha/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-curriculum/SKILL.md +1 -1
- package/prebuilt/master-curriculum/references/tiantai.md +1 -1
- package/prebuilt/master-debate/SKILL.md +15 -3
- package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-help/SKILL.md +94 -0
- package/prebuilt/master-help/tests/fidelity.jsonl +10 -0
- package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-kumarajiva/meta.json +14 -3
- package/prebuilt/master-kumarajiva/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-mahasi-sayadaw/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-milarepa/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-nagarjuna/meta.json +19 -4
- package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-ouyi/meta.json +5 -0
- package/prebuilt/master-ouyi/references/teaching.md +3 -3
- package/prebuilt/master-ouyi/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-tsongkhapa/meta.json +32 -5
- package/prebuilt/master-tsongkhapa/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-xuanzang/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-xuyun/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-zhiyi/meta.json +2 -2
- package/prebuilt/master-zhiyi/tests/fidelity.jsonl +2 -2
- package/references/teaching-modes.md +8 -1
- package/routing.json +209 -0
- package/scripts/check-audit-ignores.py +105 -0
- package/scripts/check-eval-sdk-surface.py +142 -0
- package/scripts/check-gate-liveness.py +421 -0
- package/scripts/reaudit-report.py +163 -0
- package/scripts/regrade-report.py +157 -0
- package/scripts/smoke-eval-sdk.py +174 -0
- package/scripts/test-fidelity.py +992 -89
- package/scripts/validate-citation-references.py +150 -0
- package/scripts/validate-citation-templates.py +176 -0
- package/scripts/validate-fidelity.py +6 -1
- package/scripts/validate-fixture-terms.py +127 -0
- package/scripts/validate-routing.py +254 -0
- package/scripts/validate.py +63 -36
- package/scripts/verify-adjudication.py +316 -0
- package/scripts/verify_citations.py +739 -39
- package/skill-catalog.json +83 -20
- package/tools/cross_reference.py +44 -10
- package/tools/fojin-known-absent.json +14 -0
- package/tools/fojin_bridge.py +138 -8
- package/tools/rag_query.py +45 -2
- package/tools/skill_writer.py +50 -7
- package/tools/verify_sources.py +240 -15
- package/hooks/tests/test_run_hook.sh +0 -114
- package/hooks/tests/test_run_hook_cmd.sh +0 -94
- package/hooks/tests/test_session_start.sh +0 -149
- package/scripts/tests/test_check_manifest_versions.py +0 -217
- package/scripts/tests/test_debate_protocol.py +0 -159
- package/scripts/tests/test_injection_hardening.py +0 -174
- package/scripts/tests/test_select_fidelity_smoke.py +0 -142
- package/scripts/tests/test_validate_citation_contract.py +0 -408
- package/scripts/tests/test_validate_cross_critique.py +0 -149
- package/scripts/tests/test_validate_curriculum_sources.py +0 -144
- package/scripts/tests/test_validate_fidelity.py +0 -59
- package/scripts/tests/test_validate_lore_triggers_content.py +0 -372
- package/scripts/tests/test_validate_persona_fidelity.py +0 -317
- package/scripts/tests/test_validate_promptfoo_configs.py +0 -386
- package/scripts/tests/test_validate_workflow.py +0 -265
- /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:]))
|