Agent Skills Pipeline v1.0
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)
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
"""Stage 8: Publisher — Create branch, commit, open PR on Gitea."""
|
||||
import json
|
||||
import subprocess
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import datetime
|
||||
|
||||
def publish_skill(review_result, config):
|
||||
"""
|
||||
Publish approved skill to Gitea repo via git branch + PR.
|
||||
Nothing is merged automatically — human approves.
|
||||
"""
|
||||
if review_result.get("status") != "APPROVED":
|
||||
return {
|
||||
"status": "BLOCKED",
|
||||
"reason": f"Review result: {review_result.get('status', 'unknown')} — {review_result.get('reason', '')}",
|
||||
}
|
||||
|
||||
gitea_config = config.get("gitea", {})
|
||||
token = gitea_config.get("token", "")
|
||||
base_url = gitea_config.get("base_url", "http://localhost:3000")
|
||||
owner = gitea_config.get("owner", "tonyjbala")
|
||||
repo_name = gitea_config.get("repo", "agent-skills")
|
||||
clone_url = gitea_config.get("clone_url", f"{base_url}/{owner}/{repo_name}.git")
|
||||
|
||||
gen = review_result.get("generator_output", {})
|
||||
skill_name = gen.get("skill_name", "unknown")
|
||||
files = gen.get("files", {})
|
||||
|
||||
# Create branch name
|
||||
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
branch_name = f"skill/{skill_name}-{ts}"
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Clone repo
|
||||
repo_dir = os.path.join(tmpdir, "agent-skills")
|
||||
result = subprocess.run(
|
||||
["git", "clone", "--branch", "main", "--single-branch", clone_url, repo_dir],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode != 0:
|
||||
# Try without --branch (might not exist yet)
|
||||
result = subprocess.run(
|
||||
["git", "clone", clone_url, repo_dir],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return {
|
||||
"status": "CLONE_ERROR",
|
||||
"error": result.stderr[:500],
|
||||
}
|
||||
|
||||
# Configure git
|
||||
subprocess.run(["git", "config", "user.email", "hermes@agent.local"], cwd=repo_dir)
|
||||
subprocess.run(["git", "config", "user.name", "Hermes Pipeline"], cwd=repo_dir)
|
||||
|
||||
# Create skill directory
|
||||
skill_dir = os.path.join(repo_dir, "skills", skill_name)
|
||||
os.makedirs(skill_dir, exist_ok=True)
|
||||
|
||||
# Write files
|
||||
for filename, content in files.items():
|
||||
filepath = os.path.join(skill_dir, filename)
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# Add and commit
|
||||
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", f"Add Skill: {skill_name}\n\nExtracted from: {gen.get('metadata', {}).get('source_repo', 'unknown')}\nScore: {gen.get('metadata', {}).get('score', 0)}"],
|
||||
cwd=repo_dir, capture_output=True
|
||||
)
|
||||
|
||||
# Push branch
|
||||
auth_url = clone_url.replace("http://", f"http://tonyjbala:{token}@")
|
||||
push_result = subprocess.run(
|
||||
["git", "push", "-u", auth_url, f"main:{branch_name}"],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
if push_result.returncode != 0:
|
||||
# Try creating from current branch
|
||||
subprocess.run(["git", "checkout", "-b", branch_name], cwd=repo_dir, capture_output=True)
|
||||
push_result = subprocess.run(
|
||||
["git", "push", "-u", auth_url, branch_name],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
if push_result.returncode != 0:
|
||||
return {
|
||||
"status": "PUSH_ERROR",
|
||||
"error": push_result.stderr[:500],
|
||||
}
|
||||
|
||||
# Create PR via API
|
||||
pr_url = f"{base_url}/api/v1/repos/{owner}/{repo_name}/pulls"
|
||||
pr_payload = {
|
||||
"title": f"Add Skill: {skill_name}",
|
||||
"body": f"## Skill: {skill_name}\n\n"
|
||||
f"**Goal:** {gen.get('metadata', {}).get('goal', '')}\n"
|
||||
f"**Source:** {gen.get('metadata', {}).get('source_repo', '')}\n"
|
||||
f"**Score:** {gen.get('metadata', {}).get('score', 0)}\n"
|
||||
f"**Confidence:** {gen.get('metadata', {}).get('confidence', 0)}\n"
|
||||
f"**Review:** {review_result.get('reason', '')}\n\n"
|
||||
f"### Files\n"
|
||||
+ "".join(f"- `{f}`\n" for f in files.keys()),
|
||||
"head": branch_name,
|
||||
"base": "main",
|
||||
}
|
||||
|
||||
import requests
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
resp = requests.post(pr_url, json=pr_payload, headers=headers, timeout=15)
|
||||
|
||||
if resp.status_code == 200:
|
||||
pr_data = resp.json()
|
||||
return {
|
||||
"status": "PUBLISHED",
|
||||
"skill_name": skill_name,
|
||||
"branch": branch_name,
|
||||
"pr_url": pr_data.get("html_url", ""),
|
||||
"pr_number": pr_data.get("index", ""),
|
||||
"message": f"PR opened: {pr_data.get('html_url', '')}",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "PR_ERROR",
|
||||
"http_code": resp.status_code,
|
||||
"error": resp.text[:500],
|
||||
"branch": branch_name,
|
||||
"message": f"Branch pushed but PR creation failed. Pushed branch: {branch_name}",
|
||||
}
|
||||
Reference in New Issue
Block a user