Compare commits
1 Commits
main
...
burn/602-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
034141d6c9 |
131
scripts/sprint/sprint-launcher.sh
Executable file
131
scripts/sprint/sprint-launcher.sh
Executable file
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
# ══════════════════════════════════════════════
|
||||
# Timmy-Sprint Launcher — Autonomous Backlog Burner
|
||||
# Launched by system crontab every 10 minutes.
|
||||
# Falls back to direct API if gateway is up,
|
||||
# or spawns hermes chat if not.
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Args: repo to target (default: timmy-home)
|
||||
TARGET_REPO="${1:-Timmy_Foundation/timmy-home}"
|
||||
|
||||
# Unique workspace per run
|
||||
WORKSPACE="/tmp/sprint-$(date +%s)-$$"
|
||||
mkdir -p "$WORKSPACE"
|
||||
|
||||
# Log file
|
||||
LOG_DIR="$HOME/.hermes/logs/sprint"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG="$LOG_DIR/$(date +%Y%m%d-%H%M%S)-$(echo "$TARGET_REPO" | tr '/' '-').log"
|
||||
|
||||
# Load env vars
|
||||
export GITEA_TOKEN="${GITEA_TOKEN:-$(cat "$HOME/.hermes/gitea_token_vps" 2>/dev/null)}"
|
||||
export GITEA_URL="https://forge.alexanderwhitestone.com/api/v1"
|
||||
GITEA="https://forge.alexanderwhitestone.com"
|
||||
|
||||
echo "[SPRINT] $(date) — Starting sprint for $TARGET_REPO in $WORKSPACE" | tee "$LOG"
|
||||
|
||||
# Preflight: fetch open issues and log what we find
|
||||
ISSUES=$(curl -sf -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA/api/v1/repos/$TARGET_REPO/issues?state=open&limit=15&sort=oldest" 2>/dev/null || echo "[]")
|
||||
ISSUE_COUNT=$(echo "$ISSUES" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
|
||||
echo "[SPRINT] Found $ISSUE_COUNT open issues on $TARGET_REPO" | tee -a "$LOG"
|
||||
|
||||
if [ "$ISSUE_COUNT" = "0" ]; then
|
||||
echo "[SPRINT] No issues found or API error, aborting" | tee -a "$LOG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Pick the first non-epic issue
|
||||
TARGET_ISSUE=$(echo "$ISSUES" | python3 -c "
|
||||
import sys, json
|
||||
issues = json.load(sys.stdin)
|
||||
for i in issues:
|
||||
labels = [l['name'].lower() for l in i.get('labels', [])]
|
||||
if 'epic' not in labels and 'study' not in labels:
|
||||
print(f\"#{i['number']}|{i['title']}\")
|
||||
break
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$TARGET_ISSUE" ]; then
|
||||
echo "[SPRINT] All issues are epics/studies, aborting" | tee -a "$LOG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ISSUE_NUM=$(echo "$TARGET_ISSUE" | cut -d'|' -f1 | tr -d '#')
|
||||
ISSUE_TITLE=$(echo "$TARGET_ISSUE" | cut -d'|' -f2)
|
||||
echo "[SPRINT] Targeting: #$ISSUE_NUM — $ISSUE_TITLE" | tee -a "$LOG"
|
||||
|
||||
# Write the prompt to a file
|
||||
PROMPT_FILE="$WORKSPACE/prompt.md"
|
||||
cat > "$PROMPT_FILE" <<PROMPT
|
||||
You are Timmy-Sprint. Your ONLY job: implement Gitea issue $TARGET_REPO#$ISSUE_NUM.
|
||||
|
||||
ISSUE: #$ISSUE_NUM — $ISSUE_TITLE
|
||||
|
||||
STEPS:
|
||||
1. Read the issue: curl -s -H "Authorization: token \$GITEA_TOKEN" "$GITEA/api/v1/repos/$TARGET_REPO/issues/$ISSUE_NUM"
|
||||
2. Read the issue body fully. Understand what's needed.
|
||||
3. cd $WORKSPACE
|
||||
4. Clone: git clone https://timmy:\$GITEA_TOKEN@forge.alexanderwhitestone.com/$TARGET_REPO.git
|
||||
5. cd into the repo
|
||||
6. Branch: git checkout -b feat/issue-$ISSUE_NUM
|
||||
7. Implement the fix/feature. Real code, real files.
|
||||
8. Verify: run tests, lint, build if available. Check files exist and are correct.
|
||||
9. Commit: git add -A && git commit -m "fix: $ISSUE_TITLE (closes #$ISSUE_NUM)"
|
||||
10. Push: git push origin feat/issue-$ISSUE_NUM
|
||||
11. Create PR: curl -s -X POST -H "Authorization: token \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"title":"fix: $ISSUE_TITLE","body":"Closes #$ISSUE_NUM\n\nAutomated sprint implementation.","base":"main","head":"feat/issue-$ISSUE_NUM"}' "$GITEA/api/v1/repos/$TARGET_REPO/pulls"
|
||||
12. Comment on issue: curl -s -X POST -H "Authorization: token \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"body":"PR submitted via automated sprint session."}' "$GITEA/api/v1/repos/$TARGET_REPO/issues/$ISSUE_NUM/comments"
|
||||
|
||||
RULES: Terse. Verify before done. One issue only. Commit early.
|
||||
PROMPT
|
||||
|
||||
echo "[SPRINT] Prompt written to $PROMPT_FILE" | tee -a "$LOG"
|
||||
|
||||
# Try gateway API first (fastest path)
|
||||
if curl -sf http://localhost:8642/health > /dev/null 2>&1; then
|
||||
echo "[SPRINT] Gateway up, using API" | tee -a "$LOG"
|
||||
|
||||
PROMPT_ESCAPED=$(python3 -c "import json; print(json.dumps(open('$PROMPT_FILE').read()))")
|
||||
|
||||
RESPONSE=$(curl -sf -X POST http://localhost:8642/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"model\":\"hermes-agent\",\"messages\":[{\"role\":\"user\",\"content\":$PROMPT_ESCAPED}],\"max_tokens\":8000}" \
|
||||
--max-time 600 2>&1) || true
|
||||
|
||||
if [ -n "$RESPONSE" ]; then
|
||||
echo "$RESPONSE" >> "$LOG"
|
||||
CONTENT=$(echo "$RESPONSE" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('choices',[{}])[0].get('message',{}).get('content','NO CONTENT')[:2000])
|
||||
except: print('PARSE ERROR')
|
||||
" 2>&1)
|
||||
echo "[SPRINT] Response: $CONTENT" | tee -a "$LOG"
|
||||
else
|
||||
echo "[SPRINT] Gateway returned empty, falling back to CLI" | tee -a "$LOG"
|
||||
cd "$WORKSPACE"
|
||||
hermes chat --yolo --quiet -q "$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG"
|
||||
fi
|
||||
else
|
||||
echo "[SPRINT] Gateway down, using CLI" | tee -a "$LOG"
|
||||
cd "$WORKSPACE"
|
||||
hermes chat --yolo --quiet -q "$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG"
|
||||
fi
|
||||
|
||||
EXIT_CODE=${PIPESTATUS[0]:-$?}
|
||||
echo "[SPRINT] $(date) — Exit code: $EXIT_CODE" | tee -a "$LOG"
|
||||
|
||||
# Record result to a summary file
|
||||
echo "$(date +%s)|$TARGET_REPO|#$ISSUE_NUM|$EXIT_CODE" >> "$LOG_DIR/results.csv"
|
||||
|
||||
# Cleanup old workspaces (keep last 24)
|
||||
ls -dt /tmp/sprint-* 2>/dev/null | tail -n +25 | xargs rm -rf 2>/dev/null || true
|
||||
|
||||
# Cleanup old logs (keep last 100)
|
||||
ls -t "$LOG_DIR"/*.log 2>/dev/null | tail -n +101 | xargs rm -f 2>/dev/null || true
|
||||
|
||||
exit $EXIT_CODE
|
||||
85
scripts/sprint/sprint-monitor.sh
Executable file
85
scripts/sprint/sprint-monitor.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# ══════════════════════════════════════════════
|
||||
# Sprint Monitor — Watch all sprint runners
|
||||
# Checks logs, active workspaces, and results.
|
||||
# Run every 30 min via crontab or manually.
|
||||
# ══════════════════════════════════════════════
|
||||
|
||||
LOG_DIR="$HOME/.hermes/logs/sprint"
|
||||
GITEA="https://forge.alexanderwhitestone.com"
|
||||
GITEA_TOKEN="${GITEA_TOKEN:-$(cat "$HOME/.hermes/gitea_token_vps" 2>/dev/null)}"
|
||||
|
||||
echo "========================================"
|
||||
echo " TIMMY SPRINT MONITOR"
|
||||
echo " $(date)"
|
||||
echo "========================================"
|
||||
|
||||
# Active workspaces
|
||||
ACTIVE=$(ls -d /tmp/sprint-* 2>/dev/null | wc -l | tr -d ' ')
|
||||
echo ""
|
||||
echo "ACTIVE WORKSPACES: $ACTIVE"
|
||||
if [ "$ACTIVE" -gt 8 ]; then
|
||||
echo " WARNING: $ACTIVE workspaces (possible stuck sessions)"
|
||||
ls -dt /tmp/sprint-* 2>/dev/null | head -5
|
||||
elif [ "$ACTIVE" -gt 0 ]; then
|
||||
ls -dt /tmp/sprint-* 2>/dev/null | head -3
|
||||
fi
|
||||
|
||||
# Check each target repo
|
||||
for REPO in "timmy-home" "the-beacon" "timmy-config"; do
|
||||
echo ""
|
||||
echo "--- $REPO ---"
|
||||
|
||||
# Count recent sprint logs for this repo
|
||||
LOG_PATTERN="$LOG_DIR/*${REPO}*.log"
|
||||
RECENT=$(ls -t $LOG_PATTERN 2>/dev/null | head -6)
|
||||
PASS=0
|
||||
FAIL=0
|
||||
TOTAL=0
|
||||
|
||||
for log in $RECENT; do
|
||||
TOTAL=$((TOTAL + 1))
|
||||
if grep -qi "exit code: [^0]" "$log" 2>/dev/null; then
|
||||
FAIL=$((FAIL + 1))
|
||||
elif grep -q "PR submitted\|pulls\|git push" "$log" 2>/dev/null; then
|
||||
PASS=$((PASS + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo " Last $TOTAL runs: $PASS work submitted, $FAIL failed"
|
||||
|
||||
# Show latest activity
|
||||
LATEST=$(ls -t $LOG_PATTERN 2>/dev/null | head -1)
|
||||
if [ -n "$LATEST" ]; then
|
||||
LAST_TIME=$(stat -f "%Sm" -t "%H:%M" "$LATEST" 2>/dev/null || echo "unknown")
|
||||
LAST_TARGET=$(grep "Targeting:" "$LATEST" 2>/dev/null | tail -1)
|
||||
echo " Latest: $LAST_TIME — ${LAST_TARGET:-no target selected}"
|
||||
else
|
||||
echo " No runs yet"
|
||||
fi
|
||||
|
||||
# Count open issues on the repo
|
||||
OPEN=$(curl -sf -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA/api/v1/repos/Timmy_Foundation/$REPO" 2>/dev/null | \
|
||||
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('open_issues_count','?'))" 2>/dev/null || echo "?")
|
||||
echo " Open issues on Gitea: $OPEN"
|
||||
done
|
||||
|
||||
# Check results CSV
|
||||
if [ -f "$LOG_DIR/results.csv" ]; then
|
||||
TOTAL_RUNS=$(wc -l < "$LOG_DIR/results.csv" | tr -d ' ')
|
||||
OK_RUNS=$(grep '|0$' "$LOG_DIR/results.csv" 2>/dev/null | wc -l | tr -d ' ')
|
||||
echo ""
|
||||
echo "ALL-TIME: $TOTAL_RUNS total runs, $OK_RUNS completed OK"
|
||||
fi
|
||||
|
||||
# Check gateway
|
||||
echo ""
|
||||
if curl -sf http://localhost:8642/health > /dev/null 2>&1; then
|
||||
echo "GATEWAY: UP (port 8642)"
|
||||
else
|
||||
echo "GATEWAY: DOWN (port 8642) — sprints use CLI fallback"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
301
scripts/sprint/sprint-runner.py
Executable file
301
scripts/sprint/sprint-runner.py
Executable file
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Timmy-Sprint Runner — Standalone Backlog Burner
|
||||
Calls Nous API directly via OpenAI SDK. No gateway needed.
|
||||
Each run: picks one Gitea issue, implements it, commits, pushes, PRs.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# ── Config ──────────────────────────────────────────────
|
||||
GITEA = "https://forge.alexanderwhitestone.com"
|
||||
GITEA_TOKEN = open(os.path.expanduser("~/.hermes/gitea_token_vps")).read().strip()
|
||||
|
||||
# Read model config from hermes config
|
||||
import yaml
|
||||
HERMES_CONFIG = os.path.expanduser("~/.hermes/config.yaml")
|
||||
with open(HERMES_CONFIG) as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
|
||||
MODEL = cfg.get("model", {}).get("default", "gpt-5.4")
|
||||
PROVIDER = cfg.get("model", {}).get("provider", "openai-codex")
|
||||
BASE_URL = cfg.get("model", {}).get("base_url", "https://chatgpt.com/backend-api/codex")
|
||||
|
||||
# Load auth for the active provider
|
||||
AUTH_FILE = os.path.expanduser("~/.hermes/auth.json")
|
||||
auth = json.load(open(AUTH_FILE))
|
||||
provider_auth = auth.get("providers", {}).get(PROVIDER, {})
|
||||
|
||||
# Extract access token based on provider type
|
||||
if PROVIDER == "openai-codex":
|
||||
tokens = provider_auth.get("tokens", {})
|
||||
API_KEY = tokens.get("access_token", "")
|
||||
# openai-codex goes through Cloudflare — not usable standalone
|
||||
# Fall back to local Ollama
|
||||
print(f"[WARN] openai-codex provider is Cloudflare-protected. Falling back to local Ollama.")
|
||||
PROVIDER = "ollama"
|
||||
BASE_URL = "http://localhost:11434/v1"
|
||||
MODEL = "gemma4:latest"
|
||||
API_KEY = "ollama"
|
||||
elif PROVIDER == "nous":
|
||||
API_KEY = provider_auth.get("agent_key", "")
|
||||
BASE_URL = "https://inference-api.nousresearch.com/v1"
|
||||
else:
|
||||
API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
||||
|
||||
print(f"[CONFIG] Model: {MODEL}, Provider: {PROVIDER}, URL: {BASE_URL}")
|
||||
|
||||
# ── Tools (local implementations) ──────────────────────
|
||||
def run_command(cmd, cwd=None, timeout=120):
|
||||
"""Run a shell command and return output."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, shell=True, cwd=cwd, capture_output=True,
|
||||
text=True, timeout=timeout
|
||||
)
|
||||
return {"stdout": result.stdout[-3000:], "stderr": result.stderr[-1000:], "exit_code": result.returncode}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"stdout": "", "stderr": "Command timed out", "exit_code": -1}
|
||||
except Exception as e:
|
||||
return {"stdout": "", "stderr": str(e), "exit_code": -1}
|
||||
|
||||
def read_file(path):
|
||||
"""Read a file."""
|
||||
try:
|
||||
content = Path(path).read_text()
|
||||
return content[:5000]
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def write_file(path, content):
|
||||
"""Write a file."""
|
||||
try:
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(path).write_text(content)
|
||||
return f"Written {len(content)} bytes to {path}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def gitea_api(method, endpoint, data=None):
|
||||
"""Call Gitea API."""
|
||||
url = f"{GITEA}/api/v1/{endpoint}"
|
||||
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||
|
||||
if data:
|
||||
body = json.dumps(data).encode()
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
else:
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=15)
|
||||
return json.loads(resp.read()) if resp.status != 204 else {"status": "ok"}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# ── Tool definitions for the LLM ───────────────────────
|
||||
TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_command",
|
||||
"description": "Run a shell command in the workspace. Use for git, curl, ls, tests, etc.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {"type": "string", "description": "Shell command to run"}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read a file's contents",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "File path to read"}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file (creates dirs)",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "File path"},
|
||||
"content": {"type": "string", "description": "File content"}
|
||||
},
|
||||
"required": ["path", "content"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "gitea_api",
|
||||
"description": "Call the Gitea API (GET/POST/PATCH). Endpoint is relative to /api/v1/",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {"type": "string", "enum": ["GET", "POST", "PATCH", "DELETE"]},
|
||||
"endpoint": {"type": "string", "description": "API endpoint, e.g. repos/Owner/repo/issues"},
|
||||
"data": {"type": "object", "description": "JSON body for POST/PATCH"}
|
||||
},
|
||||
"required": ["method", "endpoint"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
DISPATCH = {
|
||||
"run_command": lambda args: run_command(args["command"]),
|
||||
"read_file": lambda args: read_file(args["path"]),
|
||||
"write_file": lambda args: write_file(args["path"], args["content"]),
|
||||
"gitea_api": lambda args: gitea_api(args["method"], args["endpoint"], args.get("data")),
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────
|
||||
def main():
|
||||
repo = sys.argv[1] if len(sys.argv) > 1 else "Timmy_Foundation/timmy-home"
|
||||
workspace = tempfile.mkdtemp(prefix=f"sprint-{int(time.time())}-")
|
||||
log_dir = os.path.expanduser("~/.hermes/logs/sprint")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = os.path.join(log_dir, f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-{repo.replace('/','-')}.log")
|
||||
|
||||
def log(msg):
|
||||
line = f"[{datetime.now().strftime('%H:%M:%S')}] {msg}"
|
||||
print(line)
|
||||
with open(log_file, "a") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
log(f"Sprint starting for {repo} in {workspace}")
|
||||
|
||||
# Fetch issues
|
||||
issues = gitea_api("GET", f"repos/{repo}/issues?state=open&limit=15&sort=oldest")
|
||||
if not isinstance(issues, list) or not issues:
|
||||
log(f"No issues found: {issues}")
|
||||
return 0
|
||||
|
||||
# Pick first non-epic
|
||||
target = None
|
||||
for issue in issues:
|
||||
labels = [l["name"].lower() for l in issue.get("labels", [])]
|
||||
if "epic" not in labels and "study" not in labels:
|
||||
target = issue
|
||||
break
|
||||
|
||||
if not target:
|
||||
log("All issues are epics/studies")
|
||||
return 0
|
||||
|
||||
issue_num = target["number"]
|
||||
issue_title = target["title"]
|
||||
log(f"Targeting: #{issue_num} — {issue_title}")
|
||||
|
||||
# Fetch full issue body
|
||||
issue_detail = gitea_api("GET", f"repos/{repo}/issues/{issue_num}")
|
||||
issue_body = issue_detail.get("body", "(no description)")[:2000]
|
||||
|
||||
# Build prompt
|
||||
system_prompt = f"""You are Timmy-Sprint. Implement ONE Gitea issue. Terse. Verify before done.
|
||||
|
||||
Your workspace: {workspace}
|
||||
Target: {repo} #{issue_num}
|
||||
Title: {issue_title}
|
||||
|
||||
Issue body:
|
||||
{issue_body}
|
||||
|
||||
Steps:
|
||||
1. Read the issue body above carefully
|
||||
2. cd {workspace}
|
||||
3. Clone the repo: git clone https://timmy:{GITEA_TOKEN}@forge.alexanderwhitestone.com/{repo}.git
|
||||
4. cd into repo, branch: git checkout -b feat/issue-{issue_num}
|
||||
5. Make the changes (use run_command, read_file, write_file)
|
||||
6. Verify (tests/lint/build)
|
||||
7. git add -A && git commit -m "fix: {issue_title} (closes #{issue_num})"
|
||||
8. git push origin feat/issue-{issue_num}
|
||||
9. Create PR via gitea_api (POST repos/{repo}/pulls)
|
||||
10. Comment on issue via gitea_api (POST repos/{repo}/issues/{issue_num}/comments)
|
||||
|
||||
Work fast. One issue. Commit early."""
|
||||
|
||||
# Call LLM API (auto-detect provider)
|
||||
try:
|
||||
from openai import OpenAI
|
||||
client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
|
||||
|
||||
messages = [{"role": "user", "content": f"Implement issue #{issue_num}: {issue_title}\n\n{issue_body}"}]
|
||||
|
||||
for turn in range(20): # Max 20 tool-calling turns
|
||||
log(f"Turn {turn+1}...")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=MODEL,
|
||||
messages=[{"role": "system", "content": system_prompt}] + messages,
|
||||
tools=TOOLS,
|
||||
max_tokens=2000,
|
||||
timeout=180, # 3min per turn for slow local models
|
||||
)
|
||||
|
||||
msg = response.choices[0].message
|
||||
messages.append(msg.model_dump())
|
||||
|
||||
# Check if done (no tool calls)
|
||||
if not msg.tool_calls:
|
||||
log(f"Agent finished: {msg.content[:200] if msg.content else '(no content)'}")
|
||||
break
|
||||
|
||||
# Execute tool calls
|
||||
for tc in msg.tool_calls:
|
||||
func_name = tc.function.name
|
||||
func_args = json.loads(tc.function.arguments)
|
||||
log(f" Tool: {func_name}({json.dumps(func_args)[:100]})")
|
||||
|
||||
if func_name in DISPATCH:
|
||||
result = DISPATCH[func_name](func_args)
|
||||
else:
|
||||
result = {"error": f"Unknown tool: {func_name}"}
|
||||
|
||||
result_str = json.dumps(result) if isinstance(result, dict) else str(result)
|
||||
log(f" Result: {result_str[:150]}")
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tc.id,
|
||||
"content": result_str[:3000]
|
||||
})
|
||||
|
||||
# Record result
|
||||
with open(os.path.join(log_dir, "results.csv"), "a") as f:
|
||||
f.write(f"{int(time.time())}|{repo}|#{issue_num}|0\n")
|
||||
|
||||
log(f"Sprint complete for #{issue_num}")
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
log(f"Error: {e}")
|
||||
with open(os.path.join(log_dir, "results.csv"), "a") as f:
|
||||
f.write(f"{int(time.time())}|{repo}|#{issue_num}|1\n")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
151
skills/autonomous-ai-agents/sprint-backlog-burner/SKILL.md
Normal file
151
skills/autonomous-ai-agents/sprint-backlog-burner/SKILL.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: sprint-backlog-burner
|
||||
description: "Autonomous sprint system for burning Gitea backlog. Picks issues, implements, commits, pushes, PRs. High-frequency, isolated workspaces. Dual-path: system crontab + Hermes cron."
|
||||
version: 1.1.0
|
||||
author: Timmy Time
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [sprint, gitea, backlog, autonomous, burn, crontab, ollama]
|
||||
related_skills: [gitea-workflow-automation, test-driven-development, systematic-debugging]
|
||||
---
|
||||
|
||||
# Sprint Backlog Burner
|
||||
|
||||
## When To Use
|
||||
|
||||
- User wants autonomous issue implementation against Gitea repos
|
||||
- User wants to burn through a backlog with high-frequency parallel workers
|
||||
- User wants a system that survives gateway outages (system crontab fallback)
|
||||
|
||||
## Architecture — Dual Path
|
||||
|
||||
```
|
||||
PATH 1: System Crontab (ALWAYS works, no gateway dependency)
|
||||
└─→ scripts/sprint/sprint-runner.py (direct OpenAI SDK → LLM API)
|
||||
└─→ picks issue → clones → branches → implements → PR
|
||||
|
||||
PATH 2: Hermes Cron (full agent loop with tools, when gateway healthy)
|
||||
└─→ cron job with sprint prompt
|
||||
└─→ full tool access, session memory, skill loading
|
||||
|
||||
PATH 1 is the safety net. PATH 2 is the preferred path when available.
|
||||
Both can run simultaneously — overlapping workspaces prevent conflicts.
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
scripts/sprint/sprint-runner.py # Standalone Python runner (direct LLM API)
|
||||
scripts/sprint/sprint-launcher.sh # Shell launcher (gateway API path)
|
||||
scripts/sprint/sprint-monitor.sh # Health monitor with Gitea issue counts
|
||||
```
|
||||
|
||||
**Runtime paths** (created on first run):
|
||||
```
|
||||
~/.hermes/logs/sprint/ # Logs + results.csv
|
||||
~/.hermes/logs/sprint/results.csv # Track record: timestamp|repo|#issue|exit_code
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# 1. Ensure prerequisites
|
||||
pip install openai pyyaml
|
||||
|
||||
# 2. Ensure Gitea token exists
|
||||
echo "YOUR_TOKEN" > ~/.hermes/gitea_token_vps
|
||||
|
||||
# 3. Test manually
|
||||
python3 scripts/sprint/sprint-runner.py Timmy_Foundation/timmy-home
|
||||
|
||||
# 4. Set up crontab
|
||||
# Sprint: timmy-home — every 10 min, 10min timeout
|
||||
*/10 * * * * timeout 600 python3 /path/to/scripts/sprint/sprint-runner.py Timmy_Foundation/timmy-home >> ~/.hermes/logs/sprint/cron.log 2>&1
|
||||
|
||||
# Sprint: the-beacon — every 15 min
|
||||
*/15 * * * * timeout 600 python3 /path/to/scripts/sprint/sprint-runner.py Timmy_Foundation/the-beacon >> ~/.hermes/logs/sprint/cron-beacon.log 2>&1
|
||||
|
||||
# Sprint: timmy-config — every 20 min
|
||||
*/20 * * * * timeout 600 python3 /path/to/scripts/sprint/sprint-runner.py Timmy_Foundation/timmy-config >> ~/.hermes/logs/sprint/cron-config.log 2>&1
|
||||
|
||||
# Monitor — every 30 min
|
||||
*/30 * * * * /path/to/scripts/sprint/sprint-monitor.sh >> ~/.hermes/logs/sprint/monitor.log 2>&1
|
||||
```
|
||||
|
||||
## Provider Auto-Detection
|
||||
|
||||
sprint-runner.py reads `~/.hermes/config.yaml` and `~/.hermes/auth.json` to auto-detect the active provider.
|
||||
|
||||
```python
|
||||
# Reads from config.yaml
|
||||
MODEL = cfg["model"]["default"] # e.g. "gpt-5.4"
|
||||
PROVIDER = cfg["model"]["provider"] # e.g. "openai-codex"
|
||||
BASE_URL = cfg["model"]["base_url"]
|
||||
|
||||
# Reads auth from auth.json by provider name
|
||||
auth = json.load(open("~/.hermes/auth.json"))
|
||||
provider_auth = auth["providers"][PROVIDER]
|
||||
```
|
||||
|
||||
### Provider Fallback Chain
|
||||
|
||||
| Provider | Status | Notes |
|
||||
|----------|--------|-------|
|
||||
| `openai-codex` | Cloudflare-blocked | Cannot call directly via SDK. Gateway handles auth/challenges. |
|
||||
| `nous` | Works via SDK | Uses `agent_key` from auth.json. 24hr expiry. |
|
||||
| `ollama` | Works via SDK | Use `api_key="ollama"`. gemma4:latest is slow (~10s/turn). |
|
||||
| `openrouter` | Needs API key | `OPENROUTER_API_KEY` env var must be set. |
|
||||
|
||||
**Rule:** If `openai-codex` is the active provider, sprint-runner.py falls back to Ollama.
|
||||
|
||||
## Monitor
|
||||
|
||||
```bash
|
||||
bash scripts/sprint/sprint-monitor.sh
|
||||
```
|
||||
|
||||
Shows: active workspace count, per-repo pass/fail rates, open issue counts, gateway status, all-time results.
|
||||
|
||||
## Sprint Flow
|
||||
|
||||
1. **FETCH**: `GET /api/v1/repos/{repo}/issues?state=open&limit=15&sort=oldest`
|
||||
2. **PICK**: First non-epic, non-study issue
|
||||
3. **SCOPE**: Verify the bug isn't already fixed (git blame, check branches)
|
||||
4. **CLONE**: `git clone https://timmy:$TOKEN@forge.alexanderwhitestone.com/{repo}.git`
|
||||
5. **BRANCH**: `git checkout -b feat/issue-{N}`
|
||||
6. **IMPLEMENT**: Real code changes via tool calls
|
||||
7. **VERIFY**: Tests/lint/build if they exist
|
||||
8. **COMMIT+PUSH**: `git commit -m "fix: {title} (closes #{N})"`
|
||||
9. **PR**: `POST /api/v1/repos/{repo}/pulls`
|
||||
10. **COMMENT**: `POST /api/v1/repos/{repo}/issues/{N}/comments`
|
||||
|
||||
## Pre-Implementation Scoping
|
||||
|
||||
Before writing ANY code, verify the bug isn't already fixed:
|
||||
|
||||
```bash
|
||||
# Check git blame for affected lines
|
||||
git blame -L <line>,<line> <file>
|
||||
|
||||
# Check all branches for prior fix attempts
|
||||
git log --all --oneline --grep="issue-101"
|
||||
|
||||
# Compare issue claims against actual code
|
||||
```
|
||||
|
||||
If the bug is already fixed on main, skip it and document which commit fixed it.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **openai-codex is Cloudflare-blocked** — cannot call `chatgpt.com/backend-api/codex` directly. Use gateway or fall back to Ollama/nous.
|
||||
|
||||
2. **Overlapping workspaces** — unique `/tmp/sprint-{timestamp}-{pid}` per run prevents conflicts. Auto-cleanup keeps last 24.
|
||||
|
||||
3. **Ollama 64K context minimum** — gateway rejects gemma4:latest (8K context). Standalone runner doesn't enforce this.
|
||||
|
||||
4. **Always use `timeout 600`** — local models need ~3min for 20 tool-calling turns. Without timeout, stuck processes pile up.
|
||||
|
||||
5. **Stale branches lie** — a branch named `sprint/issue-101` doesn't mean the fix is correct. Always `git diff main..origin/branchname` before trusting it.
|
||||
|
||||
6. **Use `feat/` prefix** for PR branches, not `burn/`.
|
||||
Reference in New Issue
Block a user