claude-skills/scripts/smoke_scripts.py
Claude c84e322a99
chore(registry,ci): derive counters, register 11 plugins, add CI guards
- marketplace.json: 11 shipped-but-unregistered plugins added (compliance-os,
  snowflake-development, behuman, claude-coach, grill-with-docs,
  llm-cost-optimizer, prompt-governance, business-investment-advisor,
  video-content-strategist, compliance-team-eu-ai-act, compliance-team-iso42001)
  -> 77/77 registered; self-contradicting counts fixed
- counters reconciled to derived ground truth across README.md, CLAUDE.md,
  agents/CLAUDE.md, marketplace.json (346 skills / 17 domains / 584 tools /
  701 references / 93 agents / 99 commands / 77 plugins)
- new guards: scripts/derive_counters.py (--check, gate G3),
  scripts/check_dual_publish.py (11 pairs, gate G4),
  scripts/smoke_scripts.py + smoke_exceptions.txt (gate G8)
- meta-tooling fixes (gate G9): generate-docs.py --help no longer rewrites
  docs/ as a side effect; audit_skills.py --help instant; markdown-html added
  to hermes/vibe/gemini sync scripts and generate-docs domain maps
- ci-quality-gate.yml: compileall completed 9 -> 17 domains; 4 new advisory
  gate steps (continue-on-error pending burn-in)

Verification (all green): check_plugin_json --all 77 OK; check_paths --all 0
findings; check_dual_publish 0 drifted; smoke 586 pass / 0 fail / 11 by-design
exceptions; derive_counters --check pass; compileall 17 domains rc=0.

https://claude.ai/code/session_019AJddAL1NADWMXsy1qNPQF
2026-06-10 14:33:19 +00:00

157 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Repo-wide `--help` smoke gate for Python tools (audit gate G8).
Contract: every `.py` file in the canonical tree must exit 0 on
`python3 <file> --help` within 15 seconds. Hook-style scripts that read
stdin and fixed-contract evaluators are listed (with a reason) in
scripts/smoke_exceptions.txt and skipped.
Excluded directories: derived sync copies (.codex/.gemini/.hermes/.vibe),
docs/, audit/, node_modules/, .git/, generated integrations/, gitignored
maintainer-local folders, and __pycache__.
Exit codes:
0 every non-excepted script passed
1 one or more non-excepted scripts failed (listed on stdout)
2 harness error (e.g. exceptions file unreadable)
Usage:
python3 scripts/smoke_scripts.py # human-readable report
python3 scripts/smoke_scripts.py --json # machine-readable report
python3 scripts/smoke_scripts.py --jobs 4 # control parallelism
"""
from __future__ import annotations
import argparse
import concurrent.futures
import json
import os
import subprocess
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
EXCEPTIONS_FILE = os.path.join(REPO_ROOT, "scripts", "smoke_exceptions.txt")
TIMEOUT_SECONDS = 15
# Directory names pruned anywhere in the walk.
EXCLUDE_DIRS = {
".git", ".codex", ".gemini", ".hermes", ".vibe", "docs", "audit",
"node_modules", "integrations", "eval-workspace", "site",
"__pycache__", ".venv", "venv",
}
def load_exceptions(path):
"""Parse the by-design exceptions file: `<relpath> # reason` per line."""
exceptions = {}
if not os.path.isfile(path):
return exceptions
with open(path, "r", encoding="utf-8") as f:
for raw in f:
line = raw.strip()
if not line or line.startswith("#"):
continue
if "#" in line:
rel, reason = line.split("#", 1)
exceptions[rel.strip()] = reason.strip()
else:
exceptions[line] = "(no reason given)"
return exceptions
def find_python_files(root):
files = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = sorted(
d for d in dirnames if d not in EXCLUDE_DIRS)
for name in sorted(filenames):
if name.endswith(".py"):
files.append(os.path.relpath(
os.path.join(dirpath, name), root).replace(os.sep, "/"))
return files
def smoke_one(rel_path):
"""Run `python3 <file> --help`; return (rel_path, ok, detail)."""
abs_path = os.path.join(REPO_ROOT, rel_path)
try:
proc = subprocess.run(
[sys.executable, abs_path, "--help"],
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
timeout=TIMEOUT_SECONDS,
cwd=os.path.dirname(abs_path),
)
except subprocess.TimeoutExpired:
return rel_path, False, f"timeout after {TIMEOUT_SECONDS}s"
except OSError as exc:
return rel_path, False, f"could not execute: {exc}"
if proc.returncode != 0:
tail = (proc.stderr or proc.stdout or "").strip().splitlines()
detail = tail[-1] if tail else "(no output)"
return rel_path, False, f"exit {proc.returncode}: {detail[:200]}"
return rel_path, True, ""
def main(argv=None):
parser = argparse.ArgumentParser(
description="Run `python3 <file> --help` on every .py in the "
"canonical tree and assert exit 0 (gate G8).")
parser.add_argument("--json", action="store_true",
help="emit a JSON report instead of human-readable output")
parser.add_argument("--jobs", type=int, default=os.cpu_count() or 4,
help="parallel workers (default: CPU count)")
parser.add_argument("--root", default=REPO_ROOT,
help="repo root (default: parent of this script)")
args = parser.parse_args(argv)
try:
exceptions = load_exceptions(EXCEPTIONS_FILE)
except OSError as exc:
print(f"ERROR: cannot read exceptions file: {exc}", file=sys.stderr)
return 2
all_files = find_python_files(args.root)
to_check = [f for f in all_files if f not in exceptions]
skipped = sorted(set(all_files) & set(exceptions))
stale_exceptions = sorted(set(exceptions) - set(all_files))
failures = []
with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as pool:
for rel_path, ok, detail in pool.map(smoke_one, to_check):
if not ok:
failures.append({"file": rel_path, "detail": detail})
failures.sort(key=lambda f: f["file"])
if args.json:
print(json.dumps({
"total": len(all_files),
"checked": len(to_check),
"passed": len(to_check) - len(failures),
"failed": failures,
"skipped_by_exception": [
{"file": f, "reason": exceptions[f]} for f in skipped],
"stale_exceptions": stale_exceptions,
}, indent=2))
else:
print(f"Scripts found: {len(all_files)}")
print(f"Checked: {len(to_check)}")
print(f"Passed: {len(to_check) - len(failures)}")
print(f"Failed: {len(failures)}")
print(f"Skipped (by-design exceptions): {len(skipped)}")
if failures:
print("\nFAILURES:")
for f in failures:
print(f" {f['file']}")
print(f" {f['detail']}")
if stale_exceptions:
print("\nWARNING: exceptions listing files that no longer exist:")
for f in stale_exceptions:
print(f" {f}")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())