feat: add governance and resource boundary quality gates
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ALLOWED_STATUS = {"experimental", "active", "deprecated"}
|
||||
ALLOWED_MATURITY = {"scaffold", "production", "library", "governed"}
|
||||
ALLOWED_REVIEW_CADENCE = {"monthly", "quarterly", "semiannual", "annual", "per-release"}
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def read_frontmatter(skill_md: Path) -> dict:
|
||||
if not skill_md.exists():
|
||||
return {}
|
||||
text = skill_md.read_text(encoding="utf-8")
|
||||
if not text.startswith("---"):
|
||||
return {}
|
||||
parts = text.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
return {}
|
||||
payload = {}
|
||||
for line in parts[1].splitlines():
|
||||
if ":" not in line:
|
||||
continue
|
||||
key, value = line.split(":", 1)
|
||||
payload[key.strip()] = value.strip().strip("'\"")
|
||||
return payload
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Check skill governance metadata and lifecycle readiness.")
|
||||
parser.add_argument("skill_dir")
|
||||
parser.add_argument("--require-manifest", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.skill_dir).resolve()
|
||||
manifest_path = root / "manifest.json"
|
||||
skill_md = root / "SKILL.md"
|
||||
failures = []
|
||||
warnings = []
|
||||
details = {"skill_dir": str(root), "manifest_present": manifest_path.exists()}
|
||||
|
||||
frontmatter = read_frontmatter(skill_md)
|
||||
manifest = {}
|
||||
if manifest_path.exists():
|
||||
try:
|
||||
manifest = load_json(manifest_path)
|
||||
except json.JSONDecodeError as exc:
|
||||
failures.append(f"Invalid manifest.json: {exc}")
|
||||
elif args.require_manifest:
|
||||
failures.append("Missing manifest.json")
|
||||
else:
|
||||
warnings.append("No manifest.json; governance metadata is unavailable.")
|
||||
|
||||
if manifest:
|
||||
required = ["name", "version", "owner", "updated_at", "review_cadence", "status", "maturity_tier", "lifecycle_stage"]
|
||||
missing = [field for field in required if not manifest.get(field)]
|
||||
if missing:
|
||||
failures.append(f"Missing manifest fields: {', '.join(missing)}")
|
||||
|
||||
if manifest.get("status") and manifest["status"] not in ALLOWED_STATUS:
|
||||
failures.append(f"Invalid status: {manifest['status']}")
|
||||
if manifest.get("maturity_tier") and manifest["maturity_tier"] not in ALLOWED_MATURITY:
|
||||
failures.append(f"Invalid maturity_tier: {manifest['maturity_tier']}")
|
||||
if manifest.get("lifecycle_stage") and manifest["lifecycle_stage"] not in ALLOWED_MATURITY:
|
||||
failures.append(f"Invalid lifecycle_stage: {manifest['lifecycle_stage']}")
|
||||
if manifest.get("review_cadence") and manifest["review_cadence"] not in ALLOWED_REVIEW_CADENCE:
|
||||
failures.append(f"Invalid review_cadence: {manifest['review_cadence']}")
|
||||
if manifest.get("updated_at"):
|
||||
try:
|
||||
datetime.strptime(manifest["updated_at"], "%Y-%m-%d")
|
||||
except ValueError:
|
||||
failures.append("updated_at must use YYYY-MM-DD")
|
||||
|
||||
if frontmatter.get("name") and manifest.get("name") and frontmatter["name"] != manifest["name"]:
|
||||
failures.append("manifest name does not match SKILL.md frontmatter name")
|
||||
|
||||
if manifest.get("status") == "deprecated" and not manifest.get("deprecation_note"):
|
||||
warnings.append("Deprecated skill should include deprecation_note in manifest.json.")
|
||||
|
||||
report = {
|
||||
"ok": not failures,
|
||||
"failures": failures,
|
||||
"warnings": warnings,
|
||||
"details": {
|
||||
**details,
|
||||
"frontmatter_name": frontmatter.get("name"),
|
||||
"manifest_name": manifest.get("name"),
|
||||
"status": manifest.get("status"),
|
||||
"maturity_tier": manifest.get("maturity_tier"),
|
||||
"review_cadence": manifest.get("review_cadence"),
|
||||
},
|
||||
}
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
if failures:
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -20,9 +20,16 @@ def main() -> None:
|
||||
|
||||
skill_md = root / "SKILL.md"
|
||||
if skill_md.exists():
|
||||
lines = skill_md.read_text(encoding="utf-8").splitlines()
|
||||
text = skill_md.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
if len(lines) > 300:
|
||||
warnings.append("SKILL.md is getting long; consider moving detail into references/.")
|
||||
for dirname in ("references", "scripts", "evals", "assets"):
|
||||
path = root / dirname
|
||||
if path.exists() and not any(child.is_file() for child in path.rglob("*")):
|
||||
warnings.append(f"{dirname}/ exists but is empty.")
|
||||
if path.exists() and any(child.is_file() for child in path.rglob("*")) and dirname not in text and dirname.capitalize() not in text:
|
||||
warnings.append(f"{dirname}/ contains files but is not referenced in SKILL.md.")
|
||||
|
||||
print(json.dumps({"ok": not failures, "failures": failures, "warnings": warnings}, ensure_ascii=False, indent=2))
|
||||
if failures:
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from context_sizer import estimate_tokens, read_text, TEXT_EXTS
|
||||
|
||||
|
||||
OPTIONAL_DIRS = ("references", "scripts", "assets", "evals", "templates")
|
||||
CANONICAL_PATHS = ("SKILL.md", "manifest.json", "agents", "references", "scripts", "assets", "evals", "templates")
|
||||
|
||||
|
||||
def has_files(path: Path) -> bool:
|
||||
return path.exists() and any(child.is_file() for child in path.rglob("*"))
|
||||
|
||||
|
||||
def iter_relevant_files(root: Path) -> list[Path]:
|
||||
files = []
|
||||
for entry in CANONICAL_PATHS:
|
||||
path = root / entry
|
||||
if path.is_file():
|
||||
files.append(path)
|
||||
elif path.is_dir():
|
||||
files.extend(sorted(file for file in path.rglob("*") if file.is_file()))
|
||||
return files
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Check whether a skill package keeps resource boundaries under control.")
|
||||
parser.add_argument("skill_dir")
|
||||
parser.add_argument("--max-initial-tokens", type=int, default=1800)
|
||||
parser.add_argument("--warn-skill-body-tokens", type=int, default=1400)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.skill_dir).resolve()
|
||||
skill_md = root / "SKILL.md"
|
||||
failures = []
|
||||
warnings = []
|
||||
|
||||
if not skill_md.exists():
|
||||
failures.append("Missing SKILL.md")
|
||||
report = {"ok": False, "failures": failures, "warnings": warnings}
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
raise SystemExit(2)
|
||||
|
||||
files = iter_relevant_files(root)
|
||||
skill_body_tokens = 0
|
||||
other_tokens = 0
|
||||
initial_load_tokens = 0
|
||||
total_text_tokens = 0
|
||||
for path in files:
|
||||
if path.suffix and path.suffix not in TEXT_EXTS and path.name != "SKILL.md":
|
||||
continue
|
||||
text = read_text(path)
|
||||
tokens = estimate_tokens(text)
|
||||
total_text_tokens += tokens
|
||||
rel = path.relative_to(root)
|
||||
if rel.name == "SKILL.md":
|
||||
skill_body_tokens += tokens
|
||||
initial_load_tokens += tokens
|
||||
else:
|
||||
other_tokens += tokens
|
||||
if rel.parts[0] in {"agents"}:
|
||||
initial_load_tokens += tokens
|
||||
|
||||
if initial_load_tokens > args.max_initial_tokens:
|
||||
failures.append(
|
||||
f"Estimated initial-load tokens exceed budget: {initial_load_tokens} > {args.max_initial_tokens}"
|
||||
)
|
||||
if skill_body_tokens > args.warn_skill_body_tokens:
|
||||
warnings.append(f"SKILL.md is getting heavy: {skill_body_tokens} estimated tokens.")
|
||||
|
||||
skill_text = skill_md.read_text(encoding="utf-8")
|
||||
for dirname in OPTIONAL_DIRS:
|
||||
path = root / dirname
|
||||
if path.exists() and not has_files(path):
|
||||
warnings.append(f"{dirname}/ exists but is empty.")
|
||||
continue
|
||||
if has_files(path) and dirname not in skill_text and dirname.capitalize() not in skill_text:
|
||||
warnings.append(f"{dirname}/ contains files but is not referenced explicitly in SKILL.md.")
|
||||
|
||||
if other_tokens and skill_body_tokens / (skill_body_tokens + other_tokens) > 0.75:
|
||||
warnings.append("Most text still lives in SKILL.md; consider moving detail into references/ or scripts/.")
|
||||
|
||||
report = {
|
||||
"ok": not failures,
|
||||
"failures": failures,
|
||||
"warnings": warnings,
|
||||
"stats": {
|
||||
"skill_body_tokens": skill_body_tokens,
|
||||
"other_text_tokens": other_tokens,
|
||||
"estimated_initial_load_tokens": initial_load_tokens,
|
||||
"estimated_total_text_tokens": total_text_tokens,
|
||||
"relevant_file_count": len(files),
|
||||
},
|
||||
}
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
if failures:
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,8 +12,10 @@ def main() -> None:
|
||||
|
||||
root = Path(args.skill_dir).resolve()
|
||||
failures = []
|
||||
warnings = []
|
||||
skill_md = root / "SKILL.md"
|
||||
interface = root / "agents" / "interface.yaml"
|
||||
manifest = root / "manifest.json"
|
||||
|
||||
if not skill_md.exists():
|
||||
failures.append("Missing SKILL.md")
|
||||
@@ -38,7 +40,19 @@ def main() -> None:
|
||||
if not meta.get(field):
|
||||
failures.append(f"Missing interface field: {field}")
|
||||
|
||||
print(json.dumps({"ok": not failures, "failures": failures}, ensure_ascii=False, indent=2))
|
||||
if manifest.exists():
|
||||
data = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
for field in ("name", "version", "owner", "updated_at"):
|
||||
if not data.get(field):
|
||||
failures.append(f"Missing manifest field: {field}")
|
||||
if not data.get("review_cadence"):
|
||||
warnings.append("Manifest exists without review_cadence.")
|
||||
if not data.get("status"):
|
||||
warnings.append("Manifest exists without status.")
|
||||
if not data.get("maturity_tier"):
|
||||
warnings.append("Manifest exists without maturity_tier.")
|
||||
|
||||
print(json.dumps({"ok": not failures, "failures": failures, "warnings": warnings}, ensure_ascii=False, indent=2))
|
||||
if failures:
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user