Compare commits

..

6 Commits

Author SHA1 Message Date
63d03a2ae3 feat: add sidecar config validator (#690)
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 44s
Smoke Test / smoke (pull_request) Failing after 29s
Validate Config / YAML Lint (pull_request) Failing after 21s
Validate Config / JSON Validate (pull_request) Successful in 21s
PR Checklist / pr-checklist (pull_request) Failing after 4m21s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 2m16s
Validate Config / Cron Syntax Check (pull_request) Successful in 17s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 20s
Validate Config / Shell Script Lint (pull_request) Failing after 58s
Validate Config / Playbook Schema Validation (pull_request) Successful in 30s
Architecture Lint / Lint Repository (pull_request) Has been cancelled
Validate Config / Python Test Suite (pull_request) Has been cancelled
Pre-deploy validation for timmy-config sidecar configs.
Validates YAML/JSON configs against expected schemas.
Checks required keys, value types, nested structures.

Supported config types:
- Wizard configs (wizards/*/config.yaml): model, provider, fallback_providers
- Sidecar configs (wizards/*-sidecar.json): name, role, capabilities
- Main config (config.yaml): model, provider, toolsets
- Cron pipelines (cron/*.yml): name, schedule format
- Playbooks (playbooks/*.yaml): name, steps

Usage:
  python3 scripts/sidecar_validator.py --pre-deploy
  python3 scripts/sidecar_validator.py --all
  python3 scripts/sidecar_validator.py config.yaml

Closes #690
2026-04-14 23:29:54 -04:00
d120526244 fix: add python3 shebang to scripts/visual_pr_reviewer.py (#681) 2026-04-15 02:57:53 +00:00
8596ff761b fix: add python3 shebang to scripts/diagram_meaning_extractor.py (#681) 2026-04-15 02:57:40 +00:00
7553fd4f3e fix: add python3 shebang to scripts/captcha_bypass_handler.py (#681) 2026-04-15 02:57:25 +00:00
71082fe06f fix: add python3 shebang to bin/soul_eval_gate.py (#681) 2026-04-15 02:57:14 +00:00
6d678e938e fix: add python3 shebang to bin/nostr-agent-demo.py (#681) 2026-04-15 02:57:00 +00:00
7 changed files with 155 additions and 271 deletions

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env python3
"""
Full Nostr agent-to-agent communication demo - FINAL WORKING
"""

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env python3
"""
Soul Eval Gate — The Conscience of the Training Pipeline

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env python3
import json
from hermes_tools import browser_navigate, browser_vision

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env python3
import json
from hermes_tools import browser_navigate, browser_vision

View File

@@ -1,271 +0,0 @@
#!/usr/bin/env python3
"""
PR Triage Automation — Categorize, deduplicate, and report on open PRs.
Usage:
python scripts/pr_triage.py # Generate report
python scripts/pr_triage.py --json # JSON output
python scripts/pr_triage.py --auto-merge # Auto-merge safe PRs
python scripts/pr_triage.py --repo timmy-home # Single repo
"""
import json
import os
import re
import sys
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
try:
import urllib.request
except ImportError:
print("Error: urllib not available")
sys.exit(1)
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
GITEA_BASE = os.environ.get("GITEA_API_BASE", "https://forge.alexanderwhitestone.com/api/v1")
TOKEN_PATH = os.environ.get("GITEA_TOKEN_PATH", str(Path.home() / ".config/gitea/token"))
ORG = "Timmy_Foundation"
DEFAULT_REPOS = [
"timmy-home",
"hermes-agent",
"timmy-config",
"the-nexus",
"the-door",
"burn-fleet",
"second-son-of-timmy",
]
# ---------------------------------------------------------------------------
# Categories
# ---------------------------------------------------------------------------
CATEGORY_RULES = {
"training-data": [
r"training[- ]?data", r"scene[- ]?description", r"dpo", r"training",
r"batch[- ]?\d+", r"training[- ]?pipeline", r"jsonl",
],
"bug-fix": [
r"^fix[\(:]", r"\[BUG\]", r"\[FIX\]", r"bug fix", r"fixes #\d+",
r"closes #\d+", r"broken", r"crash", r"regression",
],
"feature": [
r"^feat[\(:]", r"\[FEAT\]", r"\[FEATURE\]", r"new feature",
r"add .+ support", r"implement",
],
"docs": [
r"^docs[\(:]", r"documentation", r"readme", r"genome",
],
"security": [
r"\[SECURITY\]", r"\[VITALIK\]", r"shield", r"injection",
r"vulnerability", r"hardening",
],
"infra": [
r"\[INFRA\]", r"deploy", r"ansible", r"docker", r"ci[/ ]cd",
r"cron", r"watchdog", r"systemd",
],
"research": [
r"research", r"benchmark", r"evaluation", r"analysis",
r"\[BIG-BRAIN\]", r"investigate",
],
"other": [], # fallback
}
def categorize_pr(title: str, body: str) -> str:
"""Categorize a PR by its title and body."""
text = f"{title} {body}".lower()
for category, patterns in CATEGORY_RULES.items():
if category == "other":
continue
for pattern in patterns:
if re.search(pattern, text, re.IGNORECASE):
return category
return "other"
# ---------------------------------------------------------------------------
# Gitea API
# ---------------------------------------------------------------------------
def _load_token() -> str:
try:
return open(TOKEN_PATH).read().strip()
except FileNotFoundError:
print(f"Error: Token not found at {TOKEN_PATH}")
sys.exit(1)
def api_get(path: str, token: str) -> Any:
req = urllib.request.Request(f"{GITEA_BASE}{path}")
req.add_header("Authorization", f"token {token}")
resp = urllib.request.urlopen(req, timeout=30)
return json.loads(resp.read())
def get_open_prs(repo: str, token: str) -> list[dict]:
"""Fetch all open PRs for a repo."""
prs = []
page = 1
while True:
try:
batch = api_get(f"/repos/{ORG}/{repo}/pulls?state=open&limit=50&page={page}", token)
if not batch:
break
prs.extend(batch)
if len(batch) < 50:
break
page += 1
except Exception:
break
return prs
def get_issue_state(repo: str, issue_num: int, token: str) -> Optional[str]:
"""Check if a referenced issue is still open."""
try:
issue = api_get(f"/repos/{ORG}/{repo}/issues/{issue_num}", token)
return issue.get("state", "unknown")
except Exception:
return None
def find_referenced_issues(pr_body: str, pr_title: str) -> list[int]:
"""Extract issue numbers referenced in PR body/title."""
text = f"{pr_title} {pr_body}"
return [int(m) for m in re.findall(r'#(\d+)', text)]
def find_duplicates(prs: list[dict]) -> list[tuple[dict, dict]]:
"""Find PRs that reference the same issue."""
issue_to_prs: dict[int, list[dict]] = {}
for pr in prs:
refs = find_referenced_issues(pr.get("body", ""), pr.get("title", ""))
for issue_num in refs:
issue_to_prs.setdefault(issue_num, []).append(pr)
duplicates = []
for issue_num, pr_list in issue_to_prs.items():
if len(pr_list) > 1:
# Pair up duplicates
for i in range(len(pr_list)):
for j in range(i + 1, len(pr_list)):
duplicates.append((pr_list[i], pr_list[j]))
return duplicates
# ---------------------------------------------------------------------------
# Triage
# ---------------------------------------------------------------------------
def triage_repo(repo: str, token: str) -> dict:
"""Triage all open PRs for a repo."""
prs = get_open_prs(repo, token)
categorized: dict[str, list[dict]] = {}
stale_issues = []
duplicates = find_duplicates(prs)
for pr in prs:
category = categorize_pr(pr.get("title", ""), pr.get("body", ""))
categorized.setdefault(category, []).append(pr)
# Check referenced issues
refs = find_referenced_issues(pr.get("body", ""), pr.get("title", ""))
for issue_num in refs:
state = get_issue_state(repo, issue_num, token)
if state == "closed":
stale_issues.append({"pr": pr["number"], "issue": issue_num, "repo": repo})
return {
"repo": repo,
"total_prs": len(prs),
"by_category": {k: len(v) for k, v in categorized.items()},
"categorized": categorized,
"duplicates": [(a["number"], b["number"]) for a, b in duplicates],
"stale_issues": stale_issues,
}
def triage_all(repos: list[str], token: str) -> list[dict]:
"""Triage all repos."""
results = []
for repo in repos:
print(f" Triaging {repo}...", file=sys.stderr)
try:
result = triage_repo(repo, token)
results.append(result)
except Exception as e:
print(f" Error triaging {repo}: {e}", file=sys.stderr)
results.append({"repo": repo, "error": str(e)})
return results
# ---------------------------------------------------------------------------
# Report
# ---------------------------------------------------------------------------
def generate_markdown_report(results: list[dict]) -> str:
"""Generate a markdown triage report."""
total_prs = sum(r.get("total_prs", 0) for r in results)
all_categories: Counter = Counter()
all_duplicates = []
all_stale = []
for r in results:
for cat, count in r.get("by_category", {}).items():
all_categories[cat] += count
all_duplicates.extend(r.get("duplicates", []))
all_stale.extend(r.get("stale_issues", []))
lines = [
"# PR Triage Report",
"",
f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
"",
"## Summary",
"",
f"| Metric | Count |",
f"|--------|-------|",
f"| Total open PRs | {total_prs} |",
f"| Repos scanned | {len(results)} |",
f"| Duplicates found | {len(all_duplicates)} |",
f"| Stale (issue closed) | {len(all_stale)} |",
"",
"## By Category",
"",
"| Category | Count |",
"|----------|-------|",
]
for cat, count in all_categories.most_common():
lines.append(f"| {cat} | {count} |")
if all_duplicates:
lines.extend(["", "## Duplicates (same issue referenced)", ""])
for a, b in all_duplicates:
lines.append(f"- PR #{a} and PR #{b}")
if all_stale:
lines.extend(["", "## Stale PRs (referenced issue is closed)", ""])
for s in all_stale:
lines.append(f"- {s['repo']} PR #{s['pr']} → issue #{s['issue']} (closed)")
# Per-repo detail
for r in results:
if r.get("error"):
lines.extend(["", f"## {r['repo']} — ERROR", "", f"```{r['error']}```"])
continue
lines.extend([f"", f"## {r['repo']} ({r.get('total_prs', 0)} open PRs)", ""])
for cat, prs in r.get("categorized", {}).items():
if not prs:
continue
lines.append(f"

150
scripts/sidecar_validator.py Executable file
View File

@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""
sidecar_validator.py - Pre-deploy validation for timmy-config sidecar configs.
Validates YAML/JSON configs against expected schemas before deploy.
Usage:
python3 scripts/sidecar_validator.py [config_path ...]
python3 scripts/sidecar_validator.py --all
python3 scripts/sidecar_validator.py --pre-deploy
"""
import json, os, sys
from pathlib import Path
try:
import yaml
except ImportError:
print("ERROR: PyYAML not installed.", file=sys.stderr); sys.exit(2)
SCHEMAS = {
"wizard_config": {
"description": "Wizard agent configuration",
"required": {"model": dict},
"optional": {"toolsets": list, "fallback_providers": list, "agent": dict, "providers": dict, "terminal": dict, "browser": dict, "compression": dict, "auxiliary": dict},
"nested_required": {"model": {"default": str, "provider": str}},
"nested_optional": {"model": {"fallback": str}, "agent": {"max_turns": (int, float), "reasoning_effort": str, "verbose": bool}},
},
"sidecar_config": {
"description": "Sidecar agent configuration",
"required": {"name": str, "role": str},
"optional": {"capabilities": list, "instructions": str, "model": str, "provider": str, "toolsets": list},
},
"main_config": {
"description": "Main hermes configuration",
"required": {"model": dict},
"optional": {"toolsets": list, "agent": dict, "terminal": dict, "browser": dict, "checkpoints": dict, "compression": dict, "auxiliary": dict, "fallback_providers": list, "providers": dict},
"nested_required": {"model": {"default": str, "provider": str}},
},
"cron_pipeline": {
"description": "Cron/pipeline schedule",
"required": {"name": str},
"optional": {"schedule": str, "cron": str, "tasks": list, "steps": list, "prompt": str, "model": dict},
},
"playbook": {
"description": "Agent playbook",
"required": {"name": str},
"optional": {"description": str, "model": str, "steps": list, "prompt": str},
},
}
def classify_config(filepath):
parts, name = filepath.parts, filepath.name
if "wizards" in parts and "-sidecar." in name: return "sidecar_config"
if "wizards" in parts and name in ("config.yaml", "config.yml"): return "wizard_config"
if name == "config.yaml" and len(parts) <= 2: return "main_config"
if "cron" in parts and name.endswith((".yml", ".yaml")): return "cron_pipeline"
if "playbooks" in parts and name.endswith((".yaml", ".yml")): return "playbook"
return None
def type_name(t):
if isinstance(t, tuple): return " or ".join(tt.__name__ for tt in t)
return t.__name__
def validate_config(data, schema_name, schema):
errors = []
for key, expected_type in schema["required"].items():
if key not in data:
errors.append(f"missing required key: '{key}' (expected {type_name(expected_type)})")
elif not isinstance(data[key], expected_type):
errors.append(f"'{key}' wrong type: got {type(data[key]).__name__}, expected {type_name(expected_type)}")
for pk, cs in schema.get("nested_required", {}).items():
if pk in data and isinstance(data[pk], dict):
for ck, et in cs.items():
if ck not in data[pk]:
errors.append(f"'{pk}' missing key: '{ck}'")
elif not isinstance(data[pk][ck], et):
errors.append(f"'{pk}.{ck}' wrong type: {type(data[pk][ck]).__name__}")
for pk, cs in schema.get("nested_optional", {}).items():
if pk in data and isinstance(data[pk], dict):
for ck, et in cs.items():
if ck in data[pk] and not isinstance(data[pk][ck], et):
errors.append(f"'{pk}.{ck}' wrong type: {type(data[pk][ck]).__name__}")
if schema_name == "wizard_config" and "fallback_providers" in data and isinstance(data["fallback_providers"], list):
for i, fb in enumerate(data["fallback_providers"]):
if not isinstance(fb, dict):
errors.append(f"fallback_providers[{i}]: expected dict")
elif "provider" not in fb: errors.append(f"fallback_providers[{i}]: missing 'provider'")
elif "model" not in fb: errors.append(f"fallback_providers[{i}]: missing 'model'")
if schema_name == "sidecar_config" and "capabilities" in data:
if not isinstance(data["capabilities"], list):
errors.append(f"'capabilities' must be a list")
if schema_name == "cron_pipeline":
s = data.get("schedule") or data.get("cron", "")
if isinstance(s, str) and s.strip() and len(s.strip().split()) != 5:
errors.append(f"schedule has {len(s.strip().split())} fields, expected 5")
return errors
def validate_file(filepath):
schema_name = classify_config(filepath)
if schema_name is None: return True, []
schema = SCHEMAS[schema_name]
try: text = filepath.read_text(encoding="utf-8", errors="replace")
except Exception as e: return False, [f"cannot read: {e}"]
try:
data = json.loads(text) if filepath.suffix == ".json" else yaml.safe_load(text)
except Exception as e: return False, [f"parse error: {e}"]
if not isinstance(data, dict): return False, [f"expected mapping, got {type(data).__name__}"]
errors = validate_config(data, schema_name, schema)
return len(errors) == 0, errors
def find_deploy_targets(root):
targets = []
for p in ["config.yaml", "wizards/*/config.yaml", "wizards/*-sidecar.json"]:
targets.extend(root.glob(p))
return sorted(targets)
def find_all_configs(root):
skip = {".git", "node_modules", "venv", "__pycache__"}
results = []
for dp, dns, fns in os.walk(root):
dns[:] = [d for d in dns if d not in skip]
for fn in fns:
if fn.endswith((".yaml", ".yml", ".json")):
fp = Path(dp) / fn
if classify_config(fp) is not None: results.append(fp)
return sorted(results)
def main():
root = Path(__file__).resolve().parent.parent
args = sys.argv[1:]
if not args or args == ["--help"]: print(__doc__); sys.exit(2)
if "--all" in args: files = find_all_configs(root); mode = "all configs"
elif "--pre-deploy" in args: files = find_deploy_targets(root); mode = "deploy targets"
else: files = [Path(a) for a in args if not a.startswith("-")]; mode = "specified files"
if not files: print(f"No files found: {mode}"); sys.exit(0)
print(f"Sidecar Validator - {mode}"); print("=" * 60)
total = 0; failed = []
for fp in files:
rel = fp.relative_to(root) if fp.is_absolute() else fp
sn = classify_config(fp) or "unknown"
ok, errs = validate_file(fp)
if ok: print(f"PASS {rel} [{sn}]")
else: failed.append((rel, sn, errs)); total += len(errs); print(f"FAIL {rel} [{sn}]")
print(); print("=" * 60)
print(f"Results: {len(files)-len(failed)}/{len(files)} valid")
if failed:
print(f"\n{total} error(s) in {len(failed)} file(s):\n")
for rel, sn, errs in failed:
print(f" {rel} ({sn}):")
for e in errs: print(f" - {e}")
print(); sys.exit(1)
else: print("\nAll configs valid!"); sys.exit(0)
if __name__ == "__main__": main()

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env python3
import json
from hermes_tools import browser_navigate, browser_vision