Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
A comprehensive collection of Agent Skills for context engineering, multi-agent architectures, and production agent systems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
researcher/scripts/validate_repo.py
1#!/usr/bin/env python32"""Validate the context-engineering skill corpus and researcher harness.34This script is intentionally deterministic. It checks structure, manifests, and5machine-readable artifacts without calling an LLM.6"""78from __future__ import annotations910import argparse11import json12import re13import sys14from dataclasses import dataclass, asdict15from pathlib import Path16from typing import Any171819REQUIRED_RESEARCHER_FILES = [20"README.md",21"source-registry.md",22"mechanisms/README.md",23"mechanisms/registry.jsonl",24"mechanisms/ledgers/accepted.jsonl",25"mechanisms/ledgers/rejected.jsonl",26"claims/README.md",27"claims/index.jsonl",28"corpus/index.json",29"benchmarks/README.md",30"benchmarks/scenarios/adversarial.jsonl",31"benchmarks/goldens/adversarial-goldens.json",32"rubrics/content-curation.md",33"rubrics/skill-change.md",34"rubrics/harness-change.md",35"rubrics/pairwise-skill-revision.md",36"templates/source-evaluation.json",37"templates/skill-proposal.md",38"templates/mechanism-proposal.jsonl",39"templates/research-thread.md",40"runbooks/autonomous-research-loop.md",41"runbooks/pr-readiness.md",42"scripts/validate_repo.py",43"scripts/validate_run.py",44"scripts/research_loop.py",45"scripts/novelty_check.py",46"scripts/compare_skill_revisions.py",47"scripts/check_activation_cases.py",48"scripts/run_benchmarks.py",49"fixtures/activation-cases.jsonl",50"reports/benchmark-history.jsonl",51]525354REQUIRED_SOURCE_EVAL_KEYS = {55"evaluation_id",56"timestamp",57"source",58"gatekeeper",59"scoring",60"decision",61"extraction",62}636465@dataclass66class Finding:67severity: str68path: str69message: str707172class Validator:73def __init__(self, root: Path) -> None:74self.root = root.resolve()75self.findings: list[Finding] = []7677def error(self, path: Path | str, message: str) -> None:78self.findings.append(Finding("error", self.rel(path), message))7980def warn(self, path: Path | str, message: str) -> None:81self.findings.append(Finding("warning", self.rel(path), message))8283def rel(self, path: Path | str) -> str:84p = Path(path)85if not p.is_absolute():86return str(p)87try:88return str(p.relative_to(self.root))89except ValueError:90return str(p)9192def run(self) -> dict[str, Any]:93skill_names = self.validate_skills()94self.validate_manifests(skill_names)95self.validate_docs(skill_names)96self.validate_researcher()97self.validate_rubrics()98self.validate_mechanisms(skill_names)99self.validate_claims(skill_names)100self.validate_corpus_index(skill_names)101self.validate_activation_cases(skill_names)102self.validate_benchmark_scenarios()103self.validate_runs()104self.validate_root_provenance()105self.validate_source_evaluations()106errors = sum(1 for f in self.findings if f.severity == "error")107warnings = sum(1 for f in self.findings if f.severity == "warning")108return {109"ok": errors == 0,110"summary": {111"errors": errors,112"warnings": warnings,113"skill_count": len(skill_names),114},115"findings": [asdict(f) for f in self.findings],116}117118def validate_skills(self) -> list[str]:119skills_dir = self.root / "skills"120if not skills_dir.exists():121self.error(skills_dir, "skills directory missing")122return []123124skill_names: list[str] = []125for skill_dir in sorted(p for p in skills_dir.iterdir() if p.is_dir()):126skill_file = skill_dir / "SKILL.md"127if not skill_file.exists():128self.error(skill_file, "missing SKILL.md")129continue130131text = skill_file.read_text(encoding="utf-8")132lines = text.splitlines()133frontmatter = self.parse_frontmatter(text, skill_file)134name = str(frontmatter.get("name", "")).strip()135description = str(frontmatter.get("description", "")).strip()136skill_names.append(skill_dir.name)137138if not name:139self.error(skill_file, "frontmatter missing name")140elif name != skill_dir.name:141self.error(skill_file, f"name '{name}' does not match directory '{skill_dir.name}'")142if name and not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name):143self.error(skill_file, "name must be lowercase kebab-case without repeated hyphens")144if not description:145self.error(skill_file, "frontmatter missing description")146elif len(description) > 1024:147self.error(skill_file, "description exceeds 1024 characters")148if re.search(r"\b(I can|Use me|You can use this)\b", description):149self.warn(skill_file, "description may not be third person")150if len(lines) > 500:151self.error(skill_file, f"SKILL.md exceeds 500 lines ({len(lines)})")152for section in [153"## When to Activate",154"## Core Concepts",155"## Practical Guidance",156"## Examples",157"## Guidelines",158"## Gotchas",159"## Integration",160"## References",161]:162if section not in text:163self.error(skill_file, f"missing required section: {section}")164if "Do not activate" not in text:165self.warn(skill_file, "missing explicit non-activation boundary")166167return sorted(skill_names)168169def parse_frontmatter(self, text: str, path: Path) -> dict[str, str]:170if not text.startswith("---\n"):171self.error(path, "missing opening frontmatter delimiter")172return {}173end = text.find("\n---", 4)174if end == -1:175self.error(path, "missing closing frontmatter delimiter")176return {}177data: dict[str, str] = {}178for line in text[4:end].splitlines():179if not line.strip() or line.startswith(" "):180continue181if ":" not in line:182continue183key, value = line.split(":", 1)184data[key.strip()] = value.strip().strip('"')185return data186187def validate_manifests(self, skill_names: list[str]) -> None:188marketplace_path = self.root / ".claude-plugin" / "marketplace.json"189plugin_path = self.root / ".plugin" / "plugin.json"190marketplace = self.load_json(marketplace_path)191plugin = self.load_json(plugin_path)192if not marketplace or not plugin:193return194195try:196plugins = marketplace["plugins"]197if len(plugins) != 1:198self.error(marketplace_path, "marketplace must contain exactly one bundled plugin")199return200plugin_entry = plugins[0]201if plugin_entry.get("source") != "./":202self.error(marketplace_path, "plugin source must be './'")203manifest_paths = plugin_entry["skills"]204except (KeyError, IndexError, TypeError):205self.error(marketplace_path, "plugins[0].skills missing or invalid")206return207208manifest_names = sorted(Path(p).name for p in manifest_paths)209if len(manifest_paths) != len(set(manifest_paths)):210self.error(marketplace_path, "duplicate skill paths in marketplace manifest")211if manifest_names != sorted(skill_names):212self.error(213marketplace_path,214f"manifest skills differ from skills directory: manifest={manifest_names} skills={skill_names}",215)216for raw_path in manifest_paths:217if Path(raw_path).is_absolute() or ".." in Path(raw_path).parts:218self.error(marketplace_path, f"skill path escapes repo: {raw_path}")219continue220path = self.root / raw_path221if not path.exists():222self.error(marketplace_path, f"skill path does not exist: {raw_path}")223224marketplace_version = str(marketplace.get("metadata", {}).get("version", ""))225plugin_version = str(plugin.get("version", ""))226if marketplace_version != plugin_version:227self.warn(228plugin_path,229f"plugin version {plugin_version} differs from marketplace metadata {marketplace_version}",230)231232def validate_docs(self, skill_names: list[str]) -> None:233readme = self.root / "README.md"234if not readme.exists():235self.error(readme, "required doc missing")236else:237text = readme.read_text(encoding="utf-8")238for skill in skill_names:239if f"skills/{skill}/" not in text and f"`{skill}`" not in text:240self.error(readme, f"missing exact published skill mention: {skill}")241242root_skill = self.root / "SKILL.md"243if not root_skill.exists():244self.error(root_skill, "required doc missing")245else:246text = root_skill.read_text(encoding="utf-8")247for skill in skill_names:248if f"skills/{skill}/SKILL.md" not in text:249self.error(root_skill, f"missing exact internal skill path: {skill}")250251claude = self.root / "CLAUDE.md"252if claude.exists():253text = claude.read_text(encoding="utf-8")254expected = f"{len(skill_names)} skill"255if expected not in text:256self.warn(claude, f"does not mention current skill count phrase '{expected}'")257258def validate_researcher(self) -> None:259researcher = self.root / "researcher"260for relative in REQUIRED_RESEARCHER_FILES:261path = researcher / relative262if not path.exists():263self.error(path, "required researcher OS file missing")264for relative in ["templates/source-evaluation.json"]:265self.load_json(researcher / relative)266267content_rubric = researcher / "rubrics" / "content-curation.md"268if content_rubric.exists():269text = content_rubric.read_text(encoding="utf-8")270if "any failed gate rejects" not in text.lower() and "any gate fails" not in text.lower():271self.warn(content_rubric, "gate failure semantics are not explicit")272273def validate_rubrics(self) -> None:274rubric_requirements = {275"content-curation.md": ["G1", "G2", "G3", "G4", "O1", "O2", "O3", "O4", "1.4", "0.9"],276"skill-change.md": ["S1", "S2", "S3", "S4", "S5", "1.4"],277"harness-change.md": ["H1", "H2", "H3", "H4", "H5", "1.5"],278"pairwise-skill-revision.md": ["Behavioral Improvement", "Evidence Fidelity", "Activation Clarity", "Corpus Fit", "Simplicity"],279}280rubric_dir = self.root / "researcher" / "rubrics"281for filename, required_terms in rubric_requirements.items():282path = rubric_dir / filename283if not path.exists():284continue285text = path.read_text(encoding="utf-8")286for term in required_terms:287if term not in text:288self.error(path, f"missing rubric term or threshold: {term}")289290def validate_mechanisms(self, skill_names: list[str]) -> None:291path = self.root / "researcher" / "mechanisms" / "registry.jsonl"292if not path.exists():293return294claim_ids = {295entry.get("claim_id")296for _, entry in self.load_jsonl(self.root / "researcher" / "claims" / "index.jsonl")297if entry.get("claim_id")298}299seen: set[str] = set()300required = {"mechanism_id", "owning_skill", "status", "activation_scenario", "behavior_change", "evidence", "failure_modes"}301valid_status = {"accepted", "candidate", "deprecated", "rejected"}302for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):303if not line.strip():304continue305try:306entry = json.loads(line)307except json.JSONDecodeError as exc:308self.error(path, f"line {line_number} invalid JSON: {exc}")309continue310missing = required - set(entry)311if missing:312self.error(path, f"line {line_number} missing fields: {sorted(missing)}")313mechanism_id = entry.get("mechanism_id")314if mechanism_id in seen:315self.error(path, f"duplicate mechanism_id: {mechanism_id}")316if isinstance(mechanism_id, str):317seen.add(mechanism_id)318if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", mechanism_id):319self.error(path, f"invalid mechanism_id: {mechanism_id}")320if entry.get("status") not in valid_status:321self.error(path, f"line {line_number} invalid status: {entry.get('status')}")322if entry.get("owning_skill") not in skill_names:323self.error(path, f"line {line_number} owning_skill is not published: {entry.get('owning_skill')}")324for key in ["activation_scenario", "behavior_change"]:325if not isinstance(entry.get(key), str) or not entry.get(key, "").strip():326self.error(path, f"line {line_number} {key} must be a non-empty string")327if not isinstance(entry.get("evidence"), list) or not entry.get("evidence"):328self.error(path, f"line {line_number} evidence must be a non-empty list")329else:330for evidence in entry["evidence"]:331if not isinstance(evidence, str):332self.error(path, f"line {line_number} evidence entries must be strings")333continue334if re.match(r"https?://", evidence):335continue336if evidence in claim_ids:337continue338if not (self.root / evidence).exists():339self.error(path, f"line {line_number} evidence path does not exist: {evidence}")340if not isinstance(entry.get("failure_modes"), list) or not entry.get("failure_modes"):341self.error(path, f"line {line_number} failure_modes must be a non-empty list")342343for ledger_name in ["accepted.jsonl", "rejected.jsonl"]:344ledger_path = self.root / "researcher" / "mechanisms" / "ledgers" / ledger_name345for line_number, entry in self.load_jsonl(ledger_path):346if not entry.get("mechanism_id"):347self.error(ledger_path, f"line {line_number} missing mechanism_id")348if not entry.get("rationale"):349self.error(ledger_path, f"line {line_number} missing rationale")350351def validate_claims(self, skill_names: list[str]) -> None:352path = self.root / "researcher" / "claims" / "index.jsonl"353seen: set[str] = set()354valid_strength = {"primary", "secondary", "anecdotal", "derived"}355valid_volatility = {"low", "medium", "high"}356for line_number, entry in self.load_jsonl(path):357claim_id = entry.get("claim_id")358if not isinstance(claim_id, str) or not claim_id:359self.error(path, f"line {line_number} missing claim_id")360elif claim_id in seen:361self.error(path, f"duplicate claim_id: {claim_id}")362else:363seen.add(claim_id)364if entry.get("owning_skill") not in skill_names:365self.error(path, f"line {line_number} owning_skill is not published: {entry.get('owning_skill')}")366for key in ["claim_text", "section", "source_url", "retrieved_at", "last_reviewed"]:367if not isinstance(entry.get(key), str) or not entry.get(key, "").strip():368self.error(path, f"line {line_number} {key} must be a non-empty string")369source_url = entry.get("source_url")370if isinstance(source_url, str) and not re.match(r"https?://", source_url):371if not (self.root / source_url).exists():372self.error(path, f"line {line_number} source path does not exist: {source_url}")373if entry.get("evidence_strength") not in valid_strength:374self.error(path, f"line {line_number} invalid evidence_strength")375if entry.get("volatility") not in valid_volatility:376self.error(path, f"line {line_number} invalid volatility")377378def validate_corpus_index(self, skill_names: list[str]) -> None:379path = self.root / "researcher" / "corpus" / "index.json"380data = self.load_json(path)381if not isinstance(data, dict):382return383skills = data.get("skills")384if not isinstance(skills, list):385self.error(path, "skills must be a list")386return387indexed_names = sorted(item.get("name") for item in skills if isinstance(item, dict))388if indexed_names != sorted(skill_names):389self.error(path, f"corpus skill index differs from skills directory: {indexed_names} != {skill_names}")390claim_ids = {entry.get("claim_id") for _, entry in self.load_jsonl(self.root / "researcher" / "claims" / "index.jsonl")}391mechanism_ids = {392entry.get("mechanism_id")393for _, entry in self.load_jsonl(self.root / "researcher" / "mechanisms" / "registry.jsonl")394}395for item in skills:396if not isinstance(item, dict):397self.error(path, "skill index entries must be objects")398continue399skill_path = item.get("path")400if not isinstance(skill_path, str) or not (self.root / skill_path).exists():401self.error(path, f"skill path missing: {skill_path}")402for claim_id in item.get("claim_ids", []):403if claim_id not in claim_ids:404self.error(path, f"unknown claim_id in corpus index: {claim_id}")405for mechanism_id in item.get("mechanism_ids", []):406if mechanism_id not in mechanism_ids:407self.error(path, f"unknown mechanism_id in corpus index: {mechanism_id}")408409def validate_activation_cases(self, skill_names: list[str]) -> None:410path = self.root / "researcher" / "fixtures" / "activation-cases.jsonl"411for line_number, entry in self.load_jsonl(path):412expected = entry.get("expected_primary_skill")413if expected not in skill_names:414self.error(path, f"line {line_number} expected_primary_skill is not published: {expected}")415for key in ["case_id", "prompt", "reason"]:416if not isinstance(entry.get(key), str) or not entry.get(key, "").strip():417self.error(path, f"line {line_number} {key} must be a non-empty string")418for key in ["acceptable_secondary_skills", "rejected_skills"]:419values = entry.get(key)420if not isinstance(values, list):421self.error(path, f"line {line_number} {key} must be a list")422continue423for value in values:424if value not in skill_names:425self.error(path, f"line {line_number} unknown skill in {key}: {value}")426427def validate_benchmark_scenarios(self) -> None:428scenario_dir = self.root / "researcher" / "benchmarks" / "scenarios"429for path in sorted(scenario_dir.glob("*.jsonl")):430for line_number, entry in self.load_jsonl(path):431for key in ["scenario_id", "class", "description", "expected_gate", "deterministic_signal"]:432if not isinstance(entry.get(key), str) or not entry.get(key, "").strip():433self.error(path, f"line {line_number} {key} must be a non-empty string")434435def validate_runs(self) -> None:436runs_dir = self.root / "researcher" / "runs"437if not runs_dir.exists():438return439for run_dir in sorted(p for p in runs_dir.iterdir() if p.is_dir()):440required_paths = [441run_dir / "THREAD.md",442run_dir / "run-state.json",443run_dir / "sources" / "queue.jsonl",444run_dir / "sources" / "evaluations",445run_dir / "sources" / "evidence" / "raw",446run_dir / "proposals",447run_dir / "proposals" / "mechanism-proposal.jsonl",448]449for path in required_paths:450if not path.exists():451self.error(path, "run artifact missing")452queue = run_dir / "sources" / "queue.jsonl"453if queue.exists():454for line_number, line in enumerate(queue.read_text(encoding="utf-8").splitlines(), start=1):455if not line.strip():456continue457try:458json.loads(line)459except json.JSONDecodeError as exc:460self.error(queue, f"line {line_number} invalid JSONL: {exc}")461report = run_dir / "reports" / "validation-report.json"462if report.exists():463data = self.load_json(report)464if isinstance(data, dict) and data.get("ok") is not True:465self.error(report, "run validation report is not passing")466state = run_dir / "run-state.json"467if state.exists():468data = self.load_json(state)469if isinstance(data, dict):470if data.get("current_state") not in {471"initialized",472"retrieved",473"evaluated",474"proposed",475"novelty_checked",476"validated",477"pr_ready",478"closed",479}:480self.error(state, "invalid current_state")481if not isinstance(data.get("state_history"), list) or not data["state_history"]:482self.error(state, "state_history must be a non-empty list")483484def validate_root_provenance(self) -> None:485for path in self.root.glob("autonomous-research-*.json"):486self.error(487path,488"raw research artifact must live under researcher/runs/<run>/sources/evidence/raw or be excluded",489)490491def validate_source_evaluations(self) -> None:492candidates: list[Path] = []493for base in [self.root / "researcher" / "runs", self.root / "researcher" / "fixtures"]:494if base.exists():495candidates.extend(base.rglob("*.json"))496497for path in candidates:498data = self.load_json(path)499if not isinstance(data, dict):500continue501if REQUIRED_SOURCE_EVAL_KEYS.issubset(data.keys()):502if "draft" in path.stem:503continue504self.validate_source_eval_shape(path, data)505506def validate_source_eval_shape(self, path: Path, data: dict[str, Any]) -> None:507source = data.get("source", {})508if source.get("retrieval_status") == "failed":509decision = data.get("decision", {}).get("verdict")510if decision != "REJECT":511self.error(path, "failed retrieval must reject or reroute before evaluation")512if source.get("retrieval_status") != "retrieved" and data.get("decision", {}).get("verdict") == "APPROVE":513self.error(path, "only retrieved sources may receive APPROVE")514515gatekeeper = data.get("gatekeeper", {})516gate_values = [517gatekeeper.get("G1_mechanism_specificity", {}).get("pass"),518gatekeeper.get("G2_implementable_artifacts", {}).get("pass"),519gatekeeper.get("G3_beyond_basics", {}).get("pass"),520gatekeeper.get("G4_source_verifiability", {}).get("pass"),521]522if not all(isinstance(value, bool) for value in gate_values):523self.error(path, "all gate pass values must be booleans")524return525expected_gatekeeper = "PASS" if all(gate_values) else "REJECT"526if gatekeeper.get("verdict") != expected_gatekeeper:527self.error(path, f"gatekeeper verdict must be {expected_gatekeeper}")528529scoring = data.get("scoring", {})530score_keys = [531"D1_technical_depth_actionability",532"D2_repo_relevance",533"D3_evidence_rigor",534"D4_novelty_insight",535]536scores: dict[str, float] = {}537for key in score_keys:538score = scoring.get(key, {}).get("score")539if not isinstance(score, (int, float)) or score < 0 or score > 2:540self.error(path, f"{key}.score must be a number from 0 to 2")541return542scores[key] = float(score)543544recomputed_total = (545scores["D1_technical_depth_actionability"] * 0.35546+ scores["D2_repo_relevance"] * 0.30547+ scores["D3_evidence_rigor"] * 0.20548+ scores["D4_novelty_insight"] * 0.15549)550recorded_total = scoring.get("weighted_total")551if not isinstance(recorded_total, (int, float)):552self.error(path, "scoring.weighted_total must be numeric")553return554if abs(float(recorded_total) - recomputed_total) > 0.011:555self.error(556path,557f"weighted_total {recorded_total} does not match recomputed {recomputed_total:.3f}",558)559560expected_decision = self.expected_content_decision(all(gate_values), scores, recomputed_total)561decision = data.get("decision", {})562if decision.get("verdict") != expected_decision["verdict"]:563self.error(564path,565f"decision verdict must be {expected_decision['verdict']} under content-curation rubric",566)567expected_override = expected_decision["override_triggered"]568actual_override = decision.get("override_triggered")569if actual_override == "null":570actual_override = None571if actual_override != expected_override:572self.error(path, f"override_triggered must be {expected_override or 'null'}")573574def expected_content_decision(575self,576gates_pass: bool,577scores: dict[str, float],578total: float,579) -> dict[str, str | None]:580if not gates_pass:581return {"verdict": "REJECT", "override_triggered": None}582if scores["D1_technical_depth_actionability"] == 0:583return {"verdict": "REJECT", "override_triggered": "O1"}584if scores["D2_repo_relevance"] == 0:585return {"verdict": "REJECT", "override_triggered": "O2"}586if scores["D3_evidence_rigor"] == 1 and total >= 1.4:587return {"verdict": "HUMAN_REVIEW", "override_triggered": "O3"}588if scores["D4_novelty_insight"] == 2 and total < 1.4:589return {"verdict": "HUMAN_REVIEW", "override_triggered": "O4"}590if total >= 1.4:591return {"verdict": "APPROVE", "override_triggered": None}592if total >= 0.9:593return {"verdict": "HUMAN_REVIEW", "override_triggered": None}594return {"verdict": "REJECT", "override_triggered": None}595596def load_json(self, path: Path) -> Any:597if not path.exists():598self.error(path, "JSON file missing")599return None600try:601return json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=self.reject_duplicate_keys(path))602except json.JSONDecodeError as exc:603self.error(path, f"invalid JSON: {exc}")604return None605606def load_jsonl(self, path: Path) -> list[tuple[int, dict[str, Any]]]:607if not path.exists():608self.error(path, "JSONL file missing")609return []610entries: list[tuple[int, dict[str, Any]]] = []611for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):612if not line.strip():613continue614try:615data = json.loads(line, object_pairs_hook=self.reject_duplicate_keys(path))616except json.JSONDecodeError as exc:617self.error(path, f"line {line_number} invalid JSONL: {exc}")618continue619if not isinstance(data, dict):620self.error(path, f"line {line_number} JSONL entry must be an object")621continue622entries.append((line_number, data))623return entries624625def reject_duplicate_keys(self, path: Path):626def hook(pairs: list[tuple[str, Any]]) -> dict[str, Any]:627result: dict[str, Any] = {}628for key, value in pairs:629if key in result:630self.error(path, f"duplicate JSON key: {key}")631result[key] = value632return result633634return hook635636637def main() -> int:638parser = argparse.ArgumentParser(description="Validate skill corpus and researcher harness")639parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2])640parser.add_argument("--json", action="store_true", help="print machine-readable JSON")641parser.add_argument("--strict", action="store_true", help="treat warnings as failures")642args = parser.parse_args()643644result = Validator(args.root).run()645if args.json:646print(json.dumps(result, indent=2))647else:648summary = result["summary"]649print(650f"Validation {'passed' if result['ok'] else 'failed'}: "651f"{summary['errors']} errors, {summary['warnings']} warnings, "652f"{summary['skill_count']} skills"653)654for finding in result["findings"]:655print(f"[{finding['severity']}] {finding['path']}: {finding['message']}")656657if result["summary"]["errors"] > 0:658return 1659if args.strict and result["summary"]["warnings"] > 0:660return 1661return 0662663664if __name__ == "__main__":665sys.exit(main())666