8da8d703da
8-stage pipeline: Scout → Filter → Reader → Extractor → Score → Generator → Reviewer → Publisher - Scout: GitHub search with token auth + rate limit retry - Filter: Deterministic rules (language, stars, age, keywords) - Reader: Incremental context loading (README → docs → examples → code) - Extractor: LLM workflow extraction with JSON retry - Score: Rule-based evaluation (no LLM) - Generator: Standardized Hermes Skill format - Reviewer: Independent LLM review (separate from generator) - Publisher: Branch + PR to Gitea First run: 5 repos discovered, 0 extracted (correct — all frameworks, no workflows)
165 lines
6.4 KiB
Python
165 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Agent Skills Pipeline Runner — full 8-stage pipeline."""
|
|
import yaml
|
|
import json
|
|
import sys
|
|
import os
|
|
import datetime
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from pipeline.scout import scout
|
|
from pipeline.filter import filter_repos
|
|
from pipeline.reader import read_repo
|
|
from pipeline.extractor import extract_workflow
|
|
from pipeline.scorer import score_workflow
|
|
from pipeline.generator import generate_skill
|
|
from pipeline.reviewer import review_skill
|
|
from pipeline.publisher import publish_skill
|
|
|
|
def load_config():
|
|
config_path = os.path.join(os.path.dirname(__file__), "config", "settings.yaml")
|
|
with open(config_path) as f:
|
|
return yaml.safe_load(f)
|
|
|
|
def main():
|
|
config = load_config()
|
|
runs_dir = os.path.join(os.path.dirname(__file__), "runs")
|
|
os.makedirs(runs_dir, exist_ok=True)
|
|
|
|
run_id = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
run_log = {"run_id": run_id, "started_at": datetime.datetime.now().isoformat(), "stages": {}}
|
|
|
|
print("=" * 60)
|
|
print("Agent Skills Pipeline — Run", run_id)
|
|
print("=" * 60)
|
|
|
|
# --- Stage 1: Scout ---
|
|
print("\n[1/8] Scout — Discovering repos from GitHub...")
|
|
state = {}
|
|
scout_result = scout(config, state)
|
|
|
|
if scout_result.get("status") in ("COOLDOWN", "RATE_LIMITED"):
|
|
print(f" ⏸ {scout_result.get('message', scout_result.get('status'))}")
|
|
return
|
|
|
|
repos = scout_result.get("repos", [])
|
|
print(f" Found {scout_result.get('count', 0)} repos")
|
|
run_log["stages"]["scout"] = {"count": scout_result.get("count", 0)}
|
|
|
|
if not repos:
|
|
print(" No repos found. Exiting.")
|
|
return
|
|
|
|
# --- Stage 2: Filter ---
|
|
print("\n[2/8] Filter — Applying deterministic rules...")
|
|
filter_result = filter_repos(repos, config)
|
|
kept = filter_result.get("kept", [])
|
|
print(f" Kept: {filter_result.get('kept_count', 0)} | Rejected: {filter_result.get('rejected_count', 0)}")
|
|
run_log["stages"]["filter"] = {"kept": filter_result.get("kept_count", 0), "rejected": filter_result.get("rejected_count", 0)}
|
|
|
|
for rej in filter_result.get("rejected", [])[:3]:
|
|
print(f" ✗ {rej['repo'].get('name', '?')}: {', '.join(rej['reasons'])}")
|
|
|
|
if not kept:
|
|
print(" All repos filtered out. Exiting.")
|
|
return
|
|
|
|
# --- Stages 3-8: Process each kept repo ---
|
|
print(f"\n[3-8/8] Processing {len(kept)} repos through pipeline...")
|
|
results = {"extracted": 0, "scored": 0, "generated": 0, "reviewed": 0, "published": 0}
|
|
|
|
for i, repo in enumerate(kept[:5]): # Cap at 5 per run
|
|
repo_name = repo.get("full_name", repo.get("name", "?"))
|
|
repo_url = repo.get("url", "")
|
|
print(f"\n ── Repo {i+1}/{min(len(kept), 5)}: {repo_name} ──")
|
|
|
|
# Stage 3: Reader
|
|
print(f" [3/8] Reader — Loading context...")
|
|
reader_output = read_repo(repo.get("clone_url", repo_url), config)
|
|
|
|
if reader_output.get("status") == "INSUFFICIENT":
|
|
print(f" ⏸ Insufficient context: {reader_output.get('decision_reason', '')}")
|
|
continue
|
|
|
|
print(f" Loaded {len(reader_output.get('context_loaded', []))} files")
|
|
|
|
# Stage 4: Extractor
|
|
print(f" [4/8] Extractor — Looking for reusable workflow...")
|
|
extract_output = extract_workflow(reader_output, config)
|
|
|
|
if extract_output.get("status") != "EXTRACTED":
|
|
print(f" ✗ No workflow: {extract_output.get('reason', extract_output.get('status', ''))}")
|
|
continue
|
|
|
|
workflow = extract_output.get("workflow", {})
|
|
print(f" ✓ Extracted: {workflow.get('skill_name', '?')} (confidence: {workflow.get('confidence', 0)})")
|
|
results["extracted"] += 1
|
|
|
|
# Stage 5: Scorer
|
|
print(f" [5/8] Scorer — Evaluating...")
|
|
extract_output["reader_output"] = reader_output
|
|
score_output = score_workflow(extract_output, config)
|
|
|
|
if score_output.get("decision") != "PASS":
|
|
print(f" ✗ Score {score_output.get('score', 0)} < {score_output.get('min_score', 0.85)}")
|
|
continue
|
|
|
|
print(f" ✓ Score: {score_output.get('score', 0)} (passed)")
|
|
results["scored"] += 1
|
|
|
|
# Stage 6: Generator
|
|
print(f" [6/8] Generator — Building Skill package...")
|
|
gen_output = generate_skill(score_output, config)
|
|
|
|
if gen_output.get("status") != "GENERATED":
|
|
print(f" ✗ Generation blocked: {gen_output.get('reason', '')}")
|
|
continue
|
|
|
|
print(f" ✓ Generated: {gen_output.get('skill_name', '?')} ({len(gen_output.get('files', {}))} files)")
|
|
results["generated"] += 1
|
|
|
|
# Stage 7: Reviewer
|
|
print(f" [7/8] Reviewer — LLM review...")
|
|
review_output = review_skill(gen_output, config)
|
|
|
|
if review_output.get("status") != "APPROVED":
|
|
print(f" ✗ Review: {review_output.get('status', '?')} — {review_output.get('reason', '')[:100]}")
|
|
continue
|
|
|
|
print(f" ✓ Approved (confidence: {review_output.get('confidence', 0)})")
|
|
results["reviewed"] += 1
|
|
|
|
# Stage 8: Publisher
|
|
print(f" [8/8] Publisher — Creating PR...")
|
|
publish_output = publish_skill(review_output, config)
|
|
|
|
if publish_output.get("status") == "PUBLISHED":
|
|
print(f" ✓ Published! PR: {publish_output.get('pr_url', '')}")
|
|
results["published"] += 1
|
|
else:
|
|
print(f" ! {publish_output.get('status', '?')}: {publish_output.get('message', publish_output.get('error', ''))[:100]}")
|
|
|
|
# --- Summary ---
|
|
print("\n" + "=" * 60)
|
|
print("PIPELINE COMPLETE")
|
|
print("=" * 60)
|
|
print(f" Scout: {scout_result.get('count', 0)} discovered")
|
|
print(f" Filter: {filter_result.get('kept_count', 0)} kept / {filter_result.get('rejected_count', 0)} rejected")
|
|
print(f" Extracted: {results['extracted']}")
|
|
print(f" Scored: {results['scored']}")
|
|
print(f" Generated: {results['generated']}")
|
|
print(f" Reviewed: {results['reviewed']}")
|
|
print(f" Published: {results['published']}")
|
|
|
|
# Save run log
|
|
run_log["results"] = results
|
|
run_log["ended_at"] = datetime.datetime.now().isoformat()
|
|
log_path = os.path.join(runs_dir, f"{run_id}.json")
|
|
with open(log_path, 'w') as f:
|
|
json.dump(run_log, f, indent=2)
|
|
print(f"\n Run log: {log_path}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|