From 4feb1fba976fc614e9efa9385f7eeaf2417b31e5 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 20 Mar 2026 10:55:34 +0000 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20dev-poll=20spawns=20duplicate=20agen?= =?UTF-8?q?ts=20=E2=80=94=20no=20tmux=20session=20guard=20(#371)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tmux has-session check before spawning dev-agent.sh at all four spawn points (orphan REQUEST_CHANGES, orphan CI fix, stuck-PR REQUEST_CHANGES, stuck-PR CI fix). If a tmux session already exists for the issue, log and skip instead of spawning a duplicate agent. Co-Authored-By: Claude Opus 4.6 (1M context) --- dev/dev-poll.sh | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/dev/dev-poll.sh b/dev/dev-poll.sh index bb44ee3..840eed2 100755 --- a/dev/dev-poll.sh +++ b/dev/dev-poll.sh @@ -347,9 +347,14 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then # Do NOT gate REQUEST_CHANGES on ci_passed: act immediately even if CI is # pending/unknown. Definitive CI failure is handled by the elif below. elif [ "${HAS_CHANGES:-0}" -gt 0 ] && { ci_passed "$CI_STATE" || [ "$CI_STATE" = "pending" ] || [ "$CI_STATE" = "unknown" ] || [ -z "$CI_STATE" ]; }; 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)" + SESSION_NAME="dev-${PROJECT_NAME}-${ISSUE_NUM}" + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + log "issue #${ISSUE_NUM} already has active session ${SESSION_NAME} — skipping" + else + 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)" + fi exit 0 elif ! ci_passed "$CI_STATE" && [ "$CI_STATE" != "" ] && [ "$CI_STATE" != "pending" ] && [ "$CI_STATE" != "unknown" ]; then @@ -357,9 +362,14 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then # Fall through to backlog scan instead of exit : else - log "issue #${ISSUE_NUM} PR #${HAS_PR} CI failed — spawning agent to fix (attempt ${CI_FIX_ATTEMPTS}/3)" - nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 & - log "started dev-agent PID $! for issue #${ISSUE_NUM} (CI fix)" + SESSION_NAME="dev-${PROJECT_NAME}-${ISSUE_NUM}" + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + log "issue #${ISSUE_NUM} already has active session ${SESSION_NAME} — skipping" + else + log "issue #${ISSUE_NUM} PR #${HAS_PR} CI failed — spawning agent to fix (attempt ${CI_FIX_ATTEMPTS}/3)" + nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 & + log "started dev-agent PID $! for issue #${ISSUE_NUM} (CI fix)" + fi exit 0 fi @@ -436,17 +446,27 @@ for i in $(seq 0 $(($(echo "$OPEN_PRS" | jq 'length') - 1))); do # CI to settle. Definitive CI failure (non-pending, non-unknown) is handled by # the elif below, so we only spawn here when CI has not definitively failed. if [ "${HAS_CHANGES:-0}" -gt 0 ] && { ci_passed "$CI_STATE" || [ "$CI_STATE" = "pending" ] || [ "$CI_STATE" = "unknown" ] || [ -z "$CI_STATE" ]; }; 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}" + SESSION_NAME="dev-${PROJECT_NAME}-${STUCK_ISSUE}" + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + log "issue #${STUCK_ISSUE} already has active session ${SESSION_NAME} — skipping" + else + 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}" + fi exit 0 elif ! ci_passed "$CI_STATE" && [ "$CI_STATE" != "" ] && [ "$CI_STATE" != "pending" ] && [ "$CI_STATE" != "unknown" ]; then if handle_ci_exhaustion "$PR_NUM" "$STUCK_ISSUE"; then continue # skip this PR, check next stuck PR or fall through to backlog else - log "PR #${PR_NUM} (issue #${STUCK_ISSUE}) CI failed — fixing (attempt ${CI_FIX_ATTEMPTS}/3)" - nohup "${SCRIPT_DIR}/dev-agent.sh" "$STUCK_ISSUE" >> "$LOGFILE" 2>&1 & - log "started dev-agent PID $! for stuck PR #${PR_NUM}" + SESSION_NAME="dev-${PROJECT_NAME}-${STUCK_ISSUE}" + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + log "issue #${STUCK_ISSUE} already has active session ${SESSION_NAME} — skipping" + else + log "PR #${PR_NUM} (issue #${STUCK_ISSUE}) CI failed — fixing (attempt ${CI_FIX_ATTEMPTS}/3)" + nohup "${SCRIPT_DIR}/dev-agent.sh" "$STUCK_ISSUE" >> "$LOGFILE" 2>&1 & + log "started dev-agent PID $! for stuck PR #${PR_NUM}" + fi exit 0 fi fi From 1d797c030309a2e3791440459b1feab61982c899 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 20 Mar 2026 11:13:21 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20guard?= =?UTF-8?q?=20before=20CI=20counter,=20cover=20all=20spawn=20points?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move tmux session guard BEFORE handle_ci_exhaustion in both CI-fix paths so poll cycles with an active session don't waste fix attempts - Add tmux guards to recovery spawn (orphan, no PR) and both agent-merge fallback paths (orphan + stuck-PR) - Use continue instead of exit 0 when guard fires in stuck-PR loop so remaining PRs are still checked Co-Authored-By: Claude Opus 4.6 (1M context) --- dev/dev-poll.sh | 76 +++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/dev/dev-poll.sh b/dev/dev-poll.sh index 840eed2..f20ab0d 100755 --- a/dev/dev-poll.sh +++ b/dev/dev-poll.sh @@ -339,9 +339,14 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then exit 0 fi # Direct merge failed (conflicts?) — fall back to dev-agent - log "falling back to dev-agent for PR #${HAS_PR} merge" - nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 & - log "started dev-agent PID $! for issue #${ISSUE_NUM} (agent-merge)" + SESSION_NAME="dev-${PROJECT_NAME}-${ISSUE_NUM}" + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + log "issue #${ISSUE_NUM} already has active session ${SESSION_NAME} — skipping" + else + log "falling back to dev-agent for PR #${HAS_PR} merge" + nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 & + log "started dev-agent PID $! for issue #${ISSUE_NUM} (agent-merge)" + fi exit 0 # Do NOT gate REQUEST_CHANGES on ci_passed: act immediately even if CI is @@ -358,18 +363,18 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then exit 0 elif ! ci_passed "$CI_STATE" && [ "$CI_STATE" != "" ] && [ "$CI_STATE" != "pending" ] && [ "$CI_STATE" != "unknown" ]; then + SESSION_NAME="dev-${PROJECT_NAME}-${ISSUE_NUM}" + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + log "issue #${ISSUE_NUM} already has active session ${SESSION_NAME} — skipping" + exit 0 + fi if handle_ci_exhaustion "$HAS_PR" "$ISSUE_NUM"; then # Fall through to backlog scan instead of exit : else - SESSION_NAME="dev-${PROJECT_NAME}-${ISSUE_NUM}" - if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then - log "issue #${ISSUE_NUM} already has active session ${SESSION_NAME} — skipping" - else - log "issue #${ISSUE_NUM} PR #${HAS_PR} CI failed — spawning agent to fix (attempt ${CI_FIX_ATTEMPTS}/3)" - nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 & - log "started dev-agent PID $! for issue #${ISSUE_NUM} (CI fix)" - fi + log "issue #${ISSUE_NUM} PR #${HAS_PR} CI failed — spawning agent to fix (attempt ${CI_FIX_ATTEMPTS}/3)" + nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 & + log "started dev-agent PID $! for issue #${ISSUE_NUM} (CI fix)" exit 0 fi @@ -377,9 +382,14 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then 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)" + SESSION_NAME="dev-${PROJECT_NAME}-${ISSUE_NUM}" + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + log "issue #${ISSUE_NUM} already has active session ${SESSION_NAME} — skipping" + 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)" + fi exit 0 fi fi @@ -434,9 +444,14 @@ for i in $(seq 0 $(($(echo "$OPEN_PRS" | jq 'length') - 1))); do exit 0 fi # Direct merge failed (conflicts?) — fall back to dev-agent - log "falling back to dev-agent for PR #${PR_NUM} merge" - nohup "${SCRIPT_DIR}/dev-agent.sh" "$STUCK_ISSUE" >> "$LOGFILE" 2>&1 & - log "started dev-agent PID $! for stuck PR #${PR_NUM} (agent-merge)" + SESSION_NAME="dev-${PROJECT_NAME}-${STUCK_ISSUE}" + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + log "issue #${STUCK_ISSUE} already has active session ${SESSION_NAME} — skipping" + else + log "falling back to dev-agent for PR #${PR_NUM} merge" + nohup "${SCRIPT_DIR}/dev-agent.sh" "$STUCK_ISSUE" >> "$LOGFILE" 2>&1 & + log "started dev-agent PID $! for stuck PR #${PR_NUM} (agent-merge)" + fi exit 0 fi @@ -449,26 +464,25 @@ for i in $(seq 0 $(($(echo "$OPEN_PRS" | jq 'length') - 1))); do SESSION_NAME="dev-${PROJECT_NAME}-${STUCK_ISSUE}" if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then log "issue #${STUCK_ISSUE} already has active session ${SESSION_NAME} — skipping" - else - 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}" + continue fi + 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_passed "$CI_STATE" && [ "$CI_STATE" != "" ] && [ "$CI_STATE" != "pending" ] && [ "$CI_STATE" != "unknown" ]; then + SESSION_NAME="dev-${PROJECT_NAME}-${STUCK_ISSUE}" + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + log "issue #${STUCK_ISSUE} already has active session ${SESSION_NAME} — skipping" + continue + fi if handle_ci_exhaustion "$PR_NUM" "$STUCK_ISSUE"; then continue # skip this PR, check next stuck PR or fall through to backlog - else - SESSION_NAME="dev-${PROJECT_NAME}-${STUCK_ISSUE}" - if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then - log "issue #${STUCK_ISSUE} already has active session ${SESSION_NAME} — skipping" - else - log "PR #${PR_NUM} (issue #${STUCK_ISSUE}) CI failed — fixing (attempt ${CI_FIX_ATTEMPTS}/3)" - nohup "${SCRIPT_DIR}/dev-agent.sh" "$STUCK_ISSUE" >> "$LOGFILE" 2>&1 & - log "started dev-agent PID $! for stuck PR #${PR_NUM}" - fi - exit 0 fi + log "PR #${PR_NUM} (issue #${STUCK_ISSUE}) CI failed — fixing (attempt ${CI_FIX_ATTEMPTS}/3)" + 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 From c3714380d7f077c71ac7f9102a7dd760b2ed4b17 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 20 Mar 2026 11:46:40 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20agent-smoke=20=E2=80=94=20explicit?= =?UTF-8?q?=20source=20for=20gardener=20monitor=5Fphase=5Floop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add lib/agent-session.sh as explicit extra definition source for the gardener-agent.sh check in agent-smoke.sh. Alpine busybox awk in CI intermittently fails to resolve monitor_phase_loop from LIB_FUNS; listing it as a direct cross-source makes the check robust. Co-Authored-By: Claude Opus 4.6 (1M context) --- .woodpecker/agent-smoke.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/agent-smoke.sh b/.woodpecker/agent-smoke.sh index cbb50e7..04bc050 100644 --- a/.woodpecker/agent-smoke.sh +++ b/.woodpecker/agent-smoke.sh @@ -157,7 +157,7 @@ check_script dev/dev-agent.sh dev/phase-handler.sh check_script dev/phase-handler.sh dev/dev-agent.sh check_script dev/dev-poll.sh check_script dev/phase-test.sh -check_script gardener/gardener-agent.sh +check_script gardener/gardener-agent.sh lib/agent-session.sh check_script gardener/gardener-poll.sh check_script review/review-pr.sh check_script review/review-poll.sh From 9c51f4dbe2232ddb2bee73cf9fd80c4667284d15 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 20 Mar 2026 12:02:40 +0000 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20agent-smoke=20get=5Ffns=20=E2=80=94?= =?UTF-8?q?=20use=20literal=20[=20\t]=20for=20busybox=20awk=20compat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Busybox awk in Alpine CI has incomplete POSIX character class support; [[:space:]] intermittently fails to match, causing function definitions to be missed from LIB_FUNS. Replace with literal [ \t] patterns. Co-Authored-By: Claude Opus 4.6 (1M context) --- .woodpecker/agent-smoke.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.woodpecker/agent-smoke.sh b/.woodpecker/agent-smoke.sh index 04bc050..591653e 100644 --- a/.woodpecker/agent-smoke.sh +++ b/.woodpecker/agent-smoke.sh @@ -21,9 +21,9 @@ FAILED=0 # Uses awk instead of grep -Eo for busybox/Alpine compatibility (#296). get_fns() { local f="$1" - awk '/^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]+[[:space:]]*\(\)/ { - sub(/^[[:space:]]+/, "") - sub(/[[:space:]]*\(\).*/, "") + awk '/^[ \t]*[a-zA-Z_][a-zA-Z0-9_]+[ \t]*\(\)/ { + sub(/^[ \t]+/, "") + sub(/[ \t]*\(\).*/, "") print }' "$f" 2>/dev/null | sort -u || true }