|
|
|
|
@@ -1,110 +1,345 @@
|
|
|
|
|
#!/bin/bash
|
|
|
|
|
# kimi-heartbeat.sh — Polls Gitea for assigned-kimi tickets, dispatches to OpenClaw
|
|
|
|
|
# Run as: bash ~/.timmy/uniwizard/kimi-heartbeat.sh
|
|
|
|
|
# Or as a cron: every 5m
|
|
|
|
|
# kimi-heartbeat.sh — Polls Gitea for assigned-kimi issues, dispatches to KimiClaw via OpenClaw
|
|
|
|
|
# Zero LLM cost for polling — only calls kimi/kimi-code for actual work.
|
|
|
|
|
#
|
|
|
|
|
# Run manually: bash ~/.timmy/uniwizard/kimi-heartbeat.sh
|
|
|
|
|
# Runs via launchd every 2 minutes: ai.timmy.kimi-heartbeat.plist
|
|
|
|
|
#
|
|
|
|
|
# Workflow for humans:
|
|
|
|
|
# 1. Create or open a Gitea issue in any tracked repo
|
|
|
|
|
# 2. Add the "assigned-kimi" label
|
|
|
|
|
# 3. This script picks it up, dispatches to KimiClaw, posts results back
|
|
|
|
|
# 4. Label transitions: assigned-kimi → kimi-in-progress → kimi-done
|
|
|
|
|
#
|
|
|
|
|
# PLANNING: If the issue body is >500 chars or contains "##" headers,
|
|
|
|
|
# KimiClaw first runs a 2-minute planning pass to decompose the task.
|
|
|
|
|
# If it needs subtasks, it creates child issues and labels them assigned-kimi
|
|
|
|
|
# for the next heartbeat cycle. This prevents 10-minute timeouts on complex work.
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
TOKEN=$(cat /Users/apayne/.timmy/kimi_gitea_token | tr -d '[:space:]')
|
|
|
|
|
BASE="http://100.126.61.75:3000/api/v1"
|
|
|
|
|
# --- Config ---
|
|
|
|
|
TOKEN=$(cat "$HOME/.timmy/kimi_gitea_token" | tr -d '[:space:]')
|
|
|
|
|
TIMMY_TOKEN=$(cat "$HOME/.config/gitea/timmy-token" | tr -d '[:space:]')
|
|
|
|
|
# Prefer Tailscale (private network) over public IP
|
|
|
|
|
if curl -sf --connect-timeout 2 "http://100.126.61.75:3000/api/v1/version" > /dev/null 2>&1; then
|
|
|
|
|
BASE="http://100.126.61.75:3000/api/v1"
|
|
|
|
|
else
|
|
|
|
|
BASE="http://143.198.27.163:3000/api/v1"
|
|
|
|
|
fi
|
|
|
|
|
LOG="/tmp/kimi-heartbeat.log"
|
|
|
|
|
LOCKFILE="/tmp/kimi-heartbeat.lock"
|
|
|
|
|
MAX_DISPATCH=10 # Increased max dispatch to 10
|
|
|
|
|
PLAN_TIMEOUT=120 # 2 minutes for planning pass
|
|
|
|
|
EXEC_TIMEOUT=480 # 8 minutes for execution pass
|
|
|
|
|
BODY_COMPLEXITY_THRESHOLD=500 # chars — above this triggers planning
|
|
|
|
|
STALE_PROGRESS_SECONDS=3600 # reclaim kimi-in-progress after 1 hour of silence
|
|
|
|
|
|
|
|
|
|
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
|
|
|
|
|
REPOS=(
|
|
|
|
|
"Timmy_Foundation/timmy-home"
|
|
|
|
|
"Timmy_Foundation/timmy-config"
|
|
|
|
|
"Timmy_Foundation/the-nexus"
|
|
|
|
|
"Timmy_Foundation/hermes-agent"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Find all issues labeled "assigned-kimi" across repos
|
|
|
|
|
REPOS=("Timmy_Foundation/timmy-home" "Timmy_Foundation/timmy-config" "Timmy_Foundation/the-nexus")
|
|
|
|
|
# --- Helpers ---
|
|
|
|
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
|
|
|
|
|
|
|
|
|
|
needs_pr_proof() {
|
|
|
|
|
local haystack="${1,,}"
|
|
|
|
|
[[ "$haystack" =~ implement|fix|refactor|feature|perf|performance|rebase|deploy|integration|module|script|pipeline|benchmark|cache|test|bug|build|port ]]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
has_pr_proof() {
|
|
|
|
|
local haystack="${1,,}"
|
|
|
|
|
[[ "$haystack" == *"proof:"* || "$haystack" == *"pr:"* || "$haystack" == *"/pulls/"* || "$haystack" == *"commit:"* ]]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
post_issue_comment_json() {
|
|
|
|
|
local repo="$1"
|
|
|
|
|
local issue_num="$2"
|
|
|
|
|
local token="$3"
|
|
|
|
|
local body="$4"
|
|
|
|
|
local payload
|
|
|
|
|
payload=$(python3 - "$body" <<'PY'
|
|
|
|
|
import json, sys
|
|
|
|
|
print(json.dumps({"body": sys.argv[1]}))
|
|
|
|
|
PY
|
|
|
|
|
)
|
|
|
|
|
curl -sf -X POST -H "Authorization: token $token" -H "Content-Type: application/json" \
|
|
|
|
|
-d "$payload" "$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Prevent overlapping runs
|
|
|
|
|
if [ -f "$LOCKFILE" ]; then
|
|
|
|
|
lock_age=$(( $(date +%s) - $(stat -f %m "$LOCKFILE" 2>/dev/null || echo 0) ))
|
|
|
|
|
if [ "$lock_age" -lt 600 ]; then
|
|
|
|
|
log "SKIP: previous run still active (lock age: ${lock_age}s)"
|
|
|
|
|
exit 0
|
|
|
|
|
else
|
|
|
|
|
log "WARN: stale lock (${lock_age}s), removing"
|
|
|
|
|
rm -f "$LOCKFILE"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
trap 'rm -f "$LOCKFILE"' EXIT
|
|
|
|
|
touch "$LOCKFILE"
|
|
|
|
|
|
|
|
|
|
dispatched=0
|
|
|
|
|
|
|
|
|
|
for repo in "${REPOS[@]}"; do
|
|
|
|
|
# Get issues with assigned-kimi label but NOT kimi-in-progress or kimi-done
|
|
|
|
|
issues=$(curl -s -H "Authorization: token $TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues?state=open&labels=assigned-kimi&limit=10" | \
|
|
|
|
|
python3 -c "
|
|
|
|
|
import json, sys
|
|
|
|
|
issues = json.load(sys.stdin)
|
|
|
|
|
for i in issues:
|
|
|
|
|
labels = [l['name'] for l in i.get('labels',[])]
|
|
|
|
|
# Skip if already in-progress or done
|
|
|
|
|
if 'kimi-in-progress' in labels or 'kimi-done' in labels:
|
|
|
|
|
# Fetch open issues with assigned-kimi label
|
|
|
|
|
response=$(curl -sf -H "Authorization: token $TIMMY_TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues?state=open&labels=assigned-kimi&limit=20" 2>/dev/null || echo "[]")
|
|
|
|
|
|
|
|
|
|
# Filter: skip done tasks, but reclaim stale kimi-in-progress work automatically
|
|
|
|
|
issues=$(echo "$response" | python3 -c "
|
|
|
|
|
import json, sys, datetime
|
|
|
|
|
STALE = int(${STALE_PROGRESS_SECONDS})
|
|
|
|
|
|
|
|
|
|
def parse_ts(value):
|
|
|
|
|
if not value:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
return datetime.datetime.fromisoformat(value.replace('Z', '+00:00'))
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
data = json.loads(sys.stdin.buffer.read())
|
|
|
|
|
except:
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
now = datetime.datetime.now(datetime.timezone.utc)
|
|
|
|
|
for i in data:
|
|
|
|
|
labels = [l['name'] for l in i.get('labels', [])]
|
|
|
|
|
if 'kimi-done' in labels:
|
|
|
|
|
continue
|
|
|
|
|
body = (i.get('body','') or '')[:500].replace('\n',' ')
|
|
|
|
|
print(f\"{i['number']}|{i['title']}|{body}\")
|
|
|
|
|
|
|
|
|
|
reclaim = False
|
|
|
|
|
updated_at = i.get('updated_at', '') or ''
|
|
|
|
|
if 'kimi-in-progress' in labels:
|
|
|
|
|
ts = parse_ts(updated_at)
|
|
|
|
|
age = (now - ts).total_seconds() if ts else (STALE + 1)
|
|
|
|
|
if age < STALE:
|
|
|
|
|
continue
|
|
|
|
|
reclaim = True
|
|
|
|
|
|
|
|
|
|
body = (i.get('body', '') or '')
|
|
|
|
|
body_len = len(body)
|
|
|
|
|
body_clean = body[:1500].replace('\n', ' ').replace('|', ' ')
|
|
|
|
|
title = i['title'].replace('|', ' ')
|
|
|
|
|
updated_clean = updated_at.replace('|', ' ')
|
|
|
|
|
reclaim_flag = 'reclaim' if reclaim else 'fresh'
|
|
|
|
|
print(f\"{i['number']}|{title}|{body_len}|{reclaim_flag}|{updated_clean}|{body_clean}\")
|
|
|
|
|
" 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
if [ -z "$issues" ]; then
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
[ -z "$issues" ] && continue
|
|
|
|
|
|
|
|
|
|
while IFS='|' read -r issue_num title body; do
|
|
|
|
|
while IFS='|' read -r issue_num title body_len reclaim_flag updated_at body; do
|
|
|
|
|
[ -z "$issue_num" ] && continue
|
|
|
|
|
log "DISPATCH: $repo #$issue_num — $title"
|
|
|
|
|
log "FOUND: $repo #$issue_num — $title (body: ${body_len} chars, mode: ${reclaim_flag}, updated: ${updated_at})"
|
|
|
|
|
|
|
|
|
|
# Add kimi-in-progress label
|
|
|
|
|
# First get the label ID
|
|
|
|
|
label_id=$(curl -s -H "Authorization: token $TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/labels" | \
|
|
|
|
|
python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-in-progress']" 2>/dev/null)
|
|
|
|
|
# --- Get label IDs for this repo ---
|
|
|
|
|
label_json=$(curl -sf -H "Authorization: token $TIMMY_TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/labels" 2>/dev/null || echo "[]")
|
|
|
|
|
|
|
|
|
|
if [ -n "$label_id" ]; then
|
|
|
|
|
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"labels\":[$label_id]}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1
|
|
|
|
|
progress_id=$(echo "$label_json" | python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-in-progress']" 2>/dev/null)
|
|
|
|
|
done_id=$(echo "$label_json" | python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-done']" 2>/dev/null)
|
|
|
|
|
kimi_id=$(echo "$label_json" | python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='assigned-kimi']" 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
if [ "$reclaim_flag" = "reclaim" ]; then
|
|
|
|
|
log "RECLAIM: $repo #$issue_num — stale kimi-in-progress since $updated_at"
|
|
|
|
|
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
|
|
|
|
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"body\":\"🟡 **KimiClaw reclaiming stale task.**\\nPrevious kimi-in-progress state exceeded ${STALE_PROGRESS_SECONDS}s without resolution.\\nLast update: $updated_at\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Post "picking up" comment
|
|
|
|
|
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"body\":\"🟠 **Kimi picking up this task** via OpenClaw heartbeat.\\nBackend: kimi/kimi-code\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1
|
|
|
|
|
# --- Add kimi-in-progress label ---
|
|
|
|
|
if [ -n "$progress_id" ]; then
|
|
|
|
|
curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"labels\":[$progress_id]}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1 || true
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Dispatch to OpenClaw
|
|
|
|
|
# Build a self-contained prompt from the issue
|
|
|
|
|
prompt="You are Timmy, working on $repo issue #$issue_num: $title
|
|
|
|
|
# --- Decide: plan first or execute directly ---
|
|
|
|
|
needs_planning=false
|
|
|
|
|
if [ "$body_len" -gt "$BODY_COMPLEXITY_THRESHOLD" ]; then
|
|
|
|
|
needs_planning=true
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ "$needs_planning" = true ]; then
|
|
|
|
|
# =============================================
|
|
|
|
|
# PHASE 1: PLANNING PASS (2 min timeout)
|
|
|
|
|
# =============================================
|
|
|
|
|
log "PLAN: $repo #$issue_num — complex task, running planning pass"
|
|
|
|
|
|
|
|
|
|
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"body\":\"🟠 **KimiClaw picking up this task** via heartbeat.\\nBackend: kimi/kimi-code (Moonshot AI)\\nMode: **Planning first** (task is complex)\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
|
|
|
|
|
|
|
|
|
plan_prompt="You are KimiClaw, a planning agent. You have 2 MINUTES.\n\nTASK: Analyze this Gitea issue and decide if you can complete it in under 8 minutes, or if it needs to be broken into subtasks.\n\nISSUE #$issue_num in $repo: $title\n\nBODY:\n$body\n\nRULES:\n- If you CAN complete this in one pass (research, write analysis, answer a question): respond with EXECUTE followed by a one-line plan.\n- If the task is TOO BIG (needs git operations, multiple repos, >2000 words of output, or multi-step implementation): respond with DECOMPOSE followed by a numbered list of 2-5 smaller subtasks. Each subtask must be completable in under 8 minutes by itself.\n- Each subtask line format: SUBTASK: <title> | <one-line description>\n- Be realistic about what fits in 8 minutes with no terminal access.\n- You CANNOT clone repos, run git, or execute code. You CAN research, analyze, write specs, review code via API, and produce documents.\n\nRespond with ONLY your decision. No preamble."
|
|
|
|
|
|
|
|
|
|
plan_result=$(openclaw agent --agent main --message "$plan_prompt" --timeout $PLAN_TIMEOUT --json 2>/dev/null || echo '{\"status\":\"error\"}')
|
|
|
|
|
plan_status=$(echo "$plan_result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status','error'))" 2>/dev/null || echo "error")
|
|
|
|
|
plan_text=$(echo "$plan_result" | python3 -c "\nimport json,sys\nd = json.load(sys.stdin)\npayloads = d.get('result',{}).get('payloads',[])\nprint(payloads[0]['text'] if payloads else '')\n" 2>/dev/null || echo "")
|
|
|
|
|
|
|
|
|
|
if echo "$plan_text" | grep -qi "^DECOMPOSE"; then
|
|
|
|
|
# --- Create subtask issues ---
|
|
|
|
|
log "DECOMPOSE: $repo #$issue_num — creating subtasks"
|
|
|
|
|
|
|
|
|
|
# Post the plan as a comment
|
|
|
|
|
escaped_plan=$(echo "$plan_text" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null)
|
|
|
|
|
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"body\":\"📝 **Planning complete — decomposing into subtasks:**\\n\\n$plan_text\"}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
|
|
|
|
|
|
|
|
|
# Extract SUBTASK lines and create child issues
|
|
|
|
|
echo "$plan_text" | grep -i "^SUBTASK:" | head -5 | while IFS='|' read -r sub_title sub_desc; do
|
|
|
|
|
sub_title=$(echo "$sub_title" | sed 's/^SUBTASK: *//')
|
|
|
|
|
sub_desc=$(echo "${sub_desc:-$sub_title}" | sed 's/^ *//')
|
|
|
|
|
|
|
|
|
|
if [ -n "$sub_title" ]; then
|
|
|
|
|
sub_body="## Parent Issue\\nChild of #$issue_num: $title\\n\\n## Task\\n$sub_desc\\n\\n## Constraints\\n- Must complete in under 8 minutes\\n- No git/terminal operations\\n- Post results as analysis/documentation\\n\\n## Assignee\\n@KimiClaw"
|
|
|
|
|
|
|
|
|
|
curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"title\":\"[SUB] $sub_title\",\"body\":\"$sub_body\"}" \
|
|
|
|
|
"$BASE/repos/$repo/issues" > /dev/null 2>&1
|
|
|
|
|
|
|
|
|
|
# Get the issue number of what we just created and label it
|
|
|
|
|
new_num=$(curl -sf -H "Authorization: token $TIMMY_TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues?state=open&limit=1&type=issues" | \
|
|
|
|
|
python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['number'] if d else '')" 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
if [ -n "$new_num" ] && [ -n "$kimi_id" ]; then
|
|
|
|
|
curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"labels\":[$kimi_id]}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$new_num/labels" > /dev/null 2>&1 || true
|
|
|
|
|
log "SUBTASK: $repo #$new_num — $sub_title"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# Mark parent as kimi-done (subtasks will be picked up next cycle)
|
|
|
|
|
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
|
|
|
|
[ -n "$done_id" ] && curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"labels\":[$done_id]}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1 || true
|
|
|
|
|
|
|
|
|
|
dispatched=$((dispatched + 1))
|
|
|
|
|
log "PLANNED: $repo #$issue_num — subtasks created, parent marked done"
|
|
|
|
|
|
|
|
|
|
else
|
|
|
|
|
# --- Plan says EXECUTE — proceed to execution ---
|
|
|
|
|
log "EXECUTE: $repo #$issue_num — planning pass says single-pass OK"
|
|
|
|
|
# Fall through to execution below
|
|
|
|
|
needs_planning=false
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ "$needs_planning" = false ]; then
|
|
|
|
|
# =============================================
|
|
|
|
|
# PHASE 2: EXECUTION PASS (8 min timeout)
|
|
|
|
|
# =============================================
|
|
|
|
|
|
|
|
|
|
# Post pickup comment if we didn't already (simple tasks skip planning)
|
|
|
|
|
if [ "$body_len" -le "$BODY_COMPLEXITY_THRESHOLD" ]; then
|
|
|
|
|
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"body\":\"🟠 **KimiClaw picking up this task** via heartbeat.\\nBackend: kimi/kimi-code (Moonshot AI)\\nMode: **Direct execution** (task fits in one pass)\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
log "DISPATCH: $repo #$issue_num to openclaw (timeout: ${EXEC_TIMEOUT}s)"
|
|
|
|
|
|
|
|
|
|
exec_prompt="You are KimiClaw, an AI agent powered by Kimi K2.5 (Moonshot AI).
|
|
|
|
|
You are working on Gitea issue #$issue_num in repo $repo.
|
|
|
|
|
You have 8 MINUTES maximum. Be concise and focused.
|
|
|
|
|
|
|
|
|
|
ISSUE TITLE: $title
|
|
|
|
|
|
|
|
|
|
ISSUE BODY:
|
|
|
|
|
$body
|
|
|
|
|
|
|
|
|
|
YOUR TASK:
|
|
|
|
|
1. Read the issue carefully
|
|
|
|
|
2. Do the work described — create files, write code, analyze, review as needed
|
|
|
|
|
3. Work in ~/.timmy/uniwizard/ for new files
|
|
|
|
|
4. When done, post a summary of what you did as a comment on the Gitea issue
|
|
|
|
|
Gitea API: $BASE, token in /Users/apayne/.config/gitea/token
|
|
|
|
|
Repo: $repo, Issue: $issue_num
|
|
|
|
|
5. Be thorough but practical. Ship working code."
|
|
|
|
|
1. Read the issue carefully and do the work described
|
|
|
|
|
2. Stay focused — deliver the core ask, skip nice-to-haves
|
|
|
|
|
3. Provide your COMPLETE results as your response (use markdown)
|
|
|
|
|
4. If you realize mid-task this will take longer than 8 minutes, STOP and summarize what you've done so far plus what remains"
|
|
|
|
|
|
|
|
|
|
# Fire via openclaw agent (async via background)
|
|
|
|
|
(
|
|
|
|
|
result=$(openclaw agent --agent main --message "$prompt" --json 2>/dev/null)
|
|
|
|
|
status=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status','error'))" 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
if [ "$status" = "ok" ]; then
|
|
|
|
|
log "COMPLETED: $repo #$issue_num"
|
|
|
|
|
# Swap kimi-in-progress for kimi-done
|
|
|
|
|
done_id=$(curl -s -H "Authorization: token $TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/labels" | \
|
|
|
|
|
python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-done']" 2>/dev/null)
|
|
|
|
|
progress_id=$(curl -s -H "Authorization: token $TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/labels" | \
|
|
|
|
|
python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-in-progress']" 2>/dev/null)
|
|
|
|
|
# --- Dispatch to OpenClaw (background) ---
|
|
|
|
|
(
|
|
|
|
|
result=$(openclaw agent --agent main --message "$exec_prompt" --timeout $EXEC_TIMEOUT --json 2>/dev/null || echo '{"status":"error"}')
|
|
|
|
|
status=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status','error'))" 2>/dev/null || echo "error")
|
|
|
|
|
|
|
|
|
|
[ -n "$progress_id" ] && curl -s -X DELETE -H "Authorization: token $TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1
|
|
|
|
|
[ -n "$done_id" ] && curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"labels\":[$done_id]}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1
|
|
|
|
|
else
|
|
|
|
|
log "FAILED: $repo #$issue_num — $status"
|
|
|
|
|
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"body\":\"🔴 **Kimi failed on this task.**\\nStatus: $status\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1
|
|
|
|
|
fi
|
|
|
|
|
) &
|
|
|
|
|
|
|
|
|
|
log "DISPATCHED: $repo #$issue_num (background PID $!)"
|
|
|
|
|
|
|
|
|
|
# Don't flood — wait 5s between dispatches
|
|
|
|
|
sleep 5
|
|
|
|
|
|
|
|
|
|
# Extract response text
|
|
|
|
|
response_text=$(echo "$result" | python3 -c "
|
|
|
|
|
import json,sys
|
|
|
|
|
d = json.load(sys.stdin)
|
|
|
|
|
payloads = d.get('result',{}).get('payloads',[])
|
|
|
|
|
print(payloads[0]['text'][:3000] if payloads else 'No response')
|
|
|
|
|
" 2>/dev/null || echo "No response")
|
|
|
|
|
|
|
|
|
|
if [ "$status" = "ok" ] && [ "$response_text" != "No response" ]; then
|
|
|
|
|
escaped=$(echo "$response_text" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read())[1:-1])" 2>/dev/null)
|
|
|
|
|
if needs_pr_proof "$title $body" && ! has_pr_proof "$response_text"; then
|
|
|
|
|
log "BLOCKED: $repo #$issue_num — response lacked PR/proof for code task"
|
|
|
|
|
post_issue_comment_json "$repo" "$issue_num" "$TOKEN" "🟡 **KimiClaw produced analysis only — no PR/proof detected.**
|
|
|
|
|
|
|
|
|
|
This issue looks like implementation work, so it is NOT being marked kimi-done.
|
|
|
|
|
Kimi response excerpt:
|
|
|
|
|
|
|
|
|
|
$escaped
|
|
|
|
|
|
|
|
|
|
Action: removing Kimi queue labels so a code-capable agent can pick it up."
|
|
|
|
|
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
|
|
|
|
[ -n "$kimi_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels/$kimi_id" > /dev/null 2>&1 || true
|
|
|
|
|
else
|
|
|
|
|
log "COMPLETED: $repo #$issue_num"
|
|
|
|
|
post_issue_comment_json "$repo" "$issue_num" "$TOKEN" "🟢 **KimiClaw result:**
|
|
|
|
|
|
|
|
|
|
$escaped"
|
|
|
|
|
|
|
|
|
|
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
|
|
|
|
[ -n "$done_id" ] && curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"labels\":[$done_id]}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1 || true
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
log "FAILED: $repo #$issue_num — status=$status"
|
|
|
|
|
|
|
|
|
|
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"body\":\"\ud83d\udd34 **KimiClaw failed/timed out.**\\nStatus: $status\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\\n\\nTask may be too complex for single-pass execution. Consider breaking into smaller subtasks.\"}" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
|
|
|
|
|
|
|
|
|
# Remove kimi-in-progress on failure
|
|
|
|
|
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
|
|
|
|
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
|
|
|
|
fi
|
|
|
|
|
) &
|
|
|
|
|
|
|
|
|
|
dispatched=$((dispatched + 1))
|
|
|
|
|
log "DISPATCHED: $repo #$issue_num (background PID $!)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Enforce dispatch cap
|
|
|
|
|
if [ "$dispatched" -ge "$MAX_DISPATCH" ]; then
|
|
|
|
|
log "CAPPED: reached $MAX_DISPATCH dispatches, remaining issues deferred to next heartbeat"
|
|
|
|
|
break 2 # Break out of both loops
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Stagger dispatches to avoid overwhelming kimi
|
|
|
|
|
sleep 3
|
|
|
|
|
|
|
|
|
|
done <<< "$issues"
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
log "Heartbeat complete. $(date)"
|
|
|
|
|
if [ "$dispatched" -eq 0 ]; then
|
|
|
|
|
log "Heartbeat: no pending tasks"
|
|
|
|
|
else
|
|
|
|
|
log "Heartbeat: dispatched $dispatched task(s)"
|
|
|
|
|
fi
|
|
|
|
|
|