disinto/dev/dev-poll.sh
openhands 0f979fd6c9 fix: stuck PRs priority + STATE.md in first commit + 405 bug in dev-poll
1. PRIORITY 1.5 in dev-poll: scan ALL open PRs for REQUEST_CHANGES or CI
   failure before picking new backlog issues. Stuck PRs get fixed first
   to avoid complex rebases piling up.

2. STATE.md written in worktree before claude starts (included in first
   commit, not a separate push that dismisses stale approvals).

3. Removed HTTP 405 from merge success check in dev-poll.sh (was fixed
   in dev-agent.sh but not here — 2 occurrences).
2026-03-14 07:34:47 +00:00

383 lines
15 KiB
Bash
Executable file

#!/usr/bin/env bash
# dev-poll.sh — Pull-based factory: find the next ready issue and start dev-agent
#
# Pull system: issues labeled "backlog" are candidates. An issue is READY when
# ALL its dependency issues are closed AND their PRs are merged into master.
# No "todo" label needed — readiness is derived from reality.
#
# Priority:
# 1. Orphaned "in-progress" issues (agent died or PR needs attention)
# 2. Ready "backlog" issues (all deps merged)
#
# Usage: cron every 10min
set -euo pipefail
# Load shared environment
source "$(dirname "$0")/../lib/env.sh"
REPO="${CODEBERG_REPO}"
API="${CODEBERG_API}"
LOCKFILE="/tmp/dev-agent.lock"
LOGFILE="${FACTORY_ROOT}/dev/dev-agent.log"
PREFLIGHT_RESULT="/tmp/dev-agent-preflight.json"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
log() {
printf '[%s] poll: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE"
}
# --- Check if dev-agent already running ---
if [ -f "$LOCKFILE" ]; then
LOCK_PID=$(cat "$LOCKFILE" 2>/dev/null || echo "")
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
log "agent running (PID ${LOCK_PID})"
exit 0
fi
rm -f "$LOCKFILE"
fi
# --- Memory guard ---
AVAIL_MB=$(awk '/MemAvailable/{printf "%d", $2/1024}' /proc/meminfo)
if [ "$AVAIL_MB" -lt 2000 ]; then
log "SKIP: only ${AVAIL_MB}MB available (need 2000MB)"
exit 0
fi
# =============================================================================
# HELPER: check if a dependency issue is fully resolved (closed + PR merged)
# =============================================================================
dep_is_merged() {
local dep_num="$1"
# Check issue is closed
local dep_state
dep_state=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/issues/${dep_num}" | jq -r '.state // "open"')
if [ "$dep_state" != "closed" ]; then
return 1
fi
# Issue closed = dep satisfied. The factory only closes issues after
# merging, so closed state is trustworthy. No need to hunt for the
# specific PR — that was over-engineering that caused false negatives.
return 0
}
# =============================================================================
# HELPER: extract dependency numbers from issue body
# =============================================================================
get_deps() {
local issue_body="$1"
# Extract #NNN references from "Depends on" / "Blocked by" sections
# Capture the header line AND subsequent lines until next ## section
{
echo "$issue_body" | awk '
BEGIN { IGNORECASE=1 }
/^##? *(Depends on|Blocked by|Dependencies)/ { capture=1; next }
capture && /^##? / { capture=0 }
capture { print }
' | grep -oP '#\K[0-9]+' || true
# Also check inline deps on same line as keyword
echo "$issue_body" | grep -iE '(depends on|blocked by)' | grep -oP '#\K[0-9]+' || true
} | sort -un
}
# =============================================================================
# HELPER: check if issue is ready (all deps merged)
# =============================================================================
issue_is_ready() {
local issue_num="$1"
local issue_body="$2"
local deps
deps=$(get_deps "$issue_body")
if [ -z "$deps" ]; then
# No dependencies — always ready
return 0
fi
while IFS= read -r dep; do
[ -z "$dep" ] && continue
if ! dep_is_merged "$dep"; then
log " #${issue_num} blocked: dep #${dep} not merged"
return 1
fi
done <<< "$deps"
return 0
}
# =============================================================================
# PRIORITY 1: orphaned in-progress issues
# =============================================================================
log "checking for in-progress issues"
ORPHANS_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/issues?state=open&labels=in-progress&limit=10&type=issues")
ORPHAN_COUNT=$(echo "$ORPHANS_JSON" | jq 'length')
if [ "$ORPHAN_COUNT" -gt 0 ]; then
ISSUE_NUM=$(echo "$ORPHANS_JSON" | jq -r '.[0].number')
# Check if there's already an open PR for this issue
HAS_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls?state=open&limit=20" | \
jq -r --arg branch "fix/issue-${ISSUE_NUM}" \
'.[] | select(.head.ref == $branch) | .number' | head -1) || true
if [ -n "$HAS_PR" ]; then
PR_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls/${HAS_PR}" | jq -r '.head.sha') || true
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
# Check formal reviews
HAS_APPROVE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls/${HAS_PR}/reviews" | \
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
HAS_CHANGES=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls/${HAS_PR}/reviews" | \
jq -r '[.[] | select(.state == "REQUEST_CHANGES") | select(.stale == false)] | length') || true
if [ "$CI_STATE" = "success" ] && [ "${HAS_APPROVE:-0}" -gt 0 ]; then
log "PR #${HAS_PR} approved + CI green → merging"
MERGE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/pulls/${HAS_PR}/merge" \
-d '{"Do":"merge","delete_branch_after_merge":true}')
if [ "$MERGE_CODE" = "200" ] || [ "$MERGE_CODE" = "204" ] ; then
log "PR #${HAS_PR} merged! Closing #${ISSUE_NUM}"
curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
curl -sf -X DELETE -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/issues/${ISSUE_NUM}/labels/in-progress" >/dev/null 2>&1 || true
openclaw system event --text "✅ PR #${HAS_PR} merged! Issue #${ISSUE_NUM} done." --mode now 2>/dev/null || true
else
log "merge failed (HTTP ${MERGE_CODE})"
fi
exit 0
elif [ "$CI_STATE" = "success" ] && [ "${HAS_CHANGES:-0}" -gt 0 ]; then
log "issue #${ISSUE_NUM} PR #${HAS_PR} has REQUEST_CHANGES — spawning agent"
nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 &
log "started dev-agent PID $! for issue #${ISSUE_NUM} (review fix)"
exit 0
elif [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
log "issue #${ISSUE_NUM} PR #${HAS_PR} CI failed — spawning agent to fix"
nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 &
log "started dev-agent PID $! for issue #${ISSUE_NUM} (CI fix)"
exit 0
else
log "issue #${ISSUE_NUM} has open PR #${HAS_PR} (CI: ${CI_STATE}, waiting)"
fi
else
log "recovering orphaned issue #${ISSUE_NUM} (no PR found)"
nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 &
log "started dev-agent PID $! for issue #${ISSUE_NUM} (recovery)"
exit 0
fi
fi
# =============================================================================
# PRIORITY 1.5: any open PR with REQUEST_CHANGES or CI failure (stuck PRs)
# =============================================================================
log "checking for stuck PRs"
OPEN_PRS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls?state=open&limit=20")
for i in $(seq 0 $(($(echo "$OPEN_PRS" | jq 'length') - 1))); do
PR_NUM=$(echo "$OPEN_PRS" | jq -r ".[$i].number")
PR_BRANCH=$(echo "$OPEN_PRS" | jq -r ".[$i].head.ref")
PR_SHA=$(echo "$OPEN_PRS" | jq -r ".[$i].head.sha")
# Extract issue number from branch name (fix/issue-NNN)
STUCK_ISSUE=$(echo "$PR_BRANCH" | grep -oP '(?<=fix/issue-)\d+' || true)
[ -z "$STUCK_ISSUE" ] && continue
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
HAS_CHANGES=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls/${PR_NUM}/reviews" | \
jq -r '[.[] | select(.state == "REQUEST_CHANGES") | select(.stale == false)] | length') || true
HAS_APPROVE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls/${PR_NUM}/reviews" | \
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
# Try merge if approved + CI green
if [ "$CI_STATE" = "success" ] && [ "${HAS_APPROVE:-0}" -gt 0 ]; then
log "PR #${PR_NUM} (issue #${STUCK_ISSUE}) approved + CI green → merging"
MERGE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/pulls/${PR_NUM}/merge" \
-d '{"Do":"merge","delete_branch_after_merge":true}')
if [ "$MERGE_CODE" = "200" ] || [ "$MERGE_CODE" = "204" ]; then
log "PR #${PR_NUM} merged! Closing #${STUCK_ISSUE}"
curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${STUCK_ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
openclaw system event --text "✅ PR #${PR_NUM} merged! Issue #${STUCK_ISSUE} done." --mode now 2>/dev/null || true
fi
continue
fi
# Stuck: REQUEST_CHANGES or CI failure → spawn agent
if [ "$CI_STATE" = "success" ] && [ "${HAS_CHANGES:-0}" -gt 0 ]; then
log "PR #${PR_NUM} (issue #${STUCK_ISSUE}) has REQUEST_CHANGES — fixing first"
nohup "${SCRIPT_DIR}/dev-agent.sh" "$STUCK_ISSUE" >> "$LOGFILE" 2>&1 &
log "started dev-agent PID $! for stuck PR #${PR_NUM}"
exit 0
elif [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
log "PR #${PR_NUM} (issue #${STUCK_ISSUE}) CI failed — fixing first"
nohup "${SCRIPT_DIR}/dev-agent.sh" "$STUCK_ISSUE" >> "$LOGFILE" 2>&1 &
log "started dev-agent PID $! for stuck PR #${PR_NUM}"
exit 0
fi
done
# =============================================================================
# PRIORITY 2: find ready backlog issues (pull system)
# =============================================================================
log "scanning backlog for ready issues"
BACKLOG_JSON=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/issues?state=open&labels=backlog&limit=20&type=issues")
BACKLOG_COUNT=$(echo "$BACKLOG_JSON" | jq 'length')
if [ "$BACKLOG_COUNT" -eq 0 ]; then
log "no backlog issues"
exit 0
fi
log "found ${BACKLOG_COUNT} backlog issues"
# Check each for readiness
READY_ISSUE=""
for i in $(seq 0 $((BACKLOG_COUNT - 1))); do
ISSUE_NUM=$(echo "$BACKLOG_JSON" | jq -r ".[$i].number")
ISSUE_BODY=$(echo "$BACKLOG_JSON" | jq -r ".[$i].body // \"\"")
if ! issue_is_ready "$ISSUE_NUM" "$ISSUE_BODY"; then
continue
fi
# Check if there's already an open PR for this issue that needs attention
EXISTING_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls?state=open&limit=20" | \
jq -r --arg branch "fix/issue-${ISSUE_NUM}" --arg num "#${ISSUE_NUM}" \
'.[] | select((.head.ref == $branch) or (.title | contains($num))) | .number' | head -1) || true
if [ -n "$EXISTING_PR" ]; then
PR_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls/${EXISTING_PR}" | jq -r '.head.sha') || true
CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
HAS_APPROVE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls/${EXISTING_PR}/reviews" | \
jq -r '[.[] | select(.state == "APPROVED") | select(.stale == false)] | length') || true
HAS_CHANGES=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API}/pulls/${EXISTING_PR}/reviews" | \
jq -r '[.[] | select(.state == "REQUEST_CHANGES") | select(.stale == false)] | length') || true
if [ "$CI_STATE" = "success" ] && [ "${HAS_APPROVE:-0}" -gt 0 ]; then
log "#${ISSUE_NUM} PR #${EXISTING_PR} approved + CI green → merging"
MERGE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/pulls/${EXISTING_PR}/merge" \
-d '{"Do":"merge","delete_branch_after_merge":true}')
if [ "$MERGE_CODE" = "200" ] || [ "$MERGE_CODE" = "204" ] ; then
log "PR #${EXISTING_PR} merged! Closing #${ISSUE_NUM}"
curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
openclaw system event --text "✅ PR #${EXISTING_PR} merged! Issue #${ISSUE_NUM} done." --mode now 2>/dev/null || true
fi
continue
elif [ "${HAS_CHANGES:-0}" -gt 0 ]; then
log "#${ISSUE_NUM} PR #${EXISTING_PR} has REQUEST_CHANGES — picking up"
READY_ISSUE="$ISSUE_NUM"
break
elif [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
log "#${ISSUE_NUM} PR #${EXISTING_PR} CI failed — picking up"
READY_ISSUE="$ISSUE_NUM"
break
else
log "#${ISSUE_NUM} PR #${EXISTING_PR} exists (CI: ${CI_STATE}, waiting)"
continue
fi
fi
READY_ISSUE="$ISSUE_NUM"
log "#${ISSUE_NUM} is READY (all deps merged, no existing PR)"
break
done
if [ -z "$READY_ISSUE" ]; then
log "no ready issues (all blocked by unmerged deps)"
exit 0
fi
# =============================================================================
# LAUNCH: start dev-agent for the ready issue
# =============================================================================
log "launching dev-agent for #${READY_ISSUE}"
rm -f "$PREFLIGHT_RESULT"
nohup "${SCRIPT_DIR}/dev-agent.sh" "$READY_ISSUE" >> "$LOGFILE" 2>&1 &
AGENT_PID=$!
# Wait briefly for preflight (agent writes result before claiming)
for w in $(seq 1 30); do
if [ -f "$PREFLIGHT_RESULT" ]; then
break
fi
if ! kill -0 "$AGENT_PID" 2>/dev/null; then
break
fi
sleep 2
done
if [ -f "$PREFLIGHT_RESULT" ]; then
PREFLIGHT_STATUS=$(jq -r '.status // "unknown"' < "$PREFLIGHT_RESULT")
rm -f "$PREFLIGHT_RESULT"
case "$PREFLIGHT_STATUS" in
ready)
log "dev-agent running for #${READY_ISSUE}"
;;
unmet_dependency)
log "#${READY_ISSUE} has code-level dependency (preflight blocked)"
wait "$AGENT_PID" 2>/dev/null || true
;;
too_large)
REASON=$(jq -r '.reason // "unspecified"' < "$PREFLIGHT_RESULT" 2>/dev/null || echo "unspecified")
log "#${READY_ISSUE} too large: ${REASON}"
# Label as underspecified
curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${READY_ISSUE}/labels" \
-d '{"labels":["underspecified"]}' >/dev/null 2>&1 || true
;;
already_done)
log "#${READY_ISSUE} already done"
;;
*)
log "#${READY_ISSUE} unknown preflight: ${PREFLIGHT_STATUS}"
;;
esac
elif kill -0 "$AGENT_PID" 2>/dev/null; then
log "dev-agent running for #${READY_ISSUE} (passed preflight)"
else
log "dev-agent exited for #${READY_ISSUE} without preflight result"
fi