diff --git a/AGENTS.md b/AGENTS.md index 5f63564..939109c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,12 +46,12 @@ disinto/ ```bash # ShellCheck all scripts -shellcheck dev/dev-poll.sh dev/dev-agent.sh dev/phase-test.sh \ +shellcheck dev/dev-poll.sh dev/dev-agent.sh dev/phase-handler.sh dev/phase-test.sh \ review/review-poll.sh review/review-pr.sh \ gardener/gardener-poll.sh gardener/gardener-agent.sh \ supervisor/supervisor-poll.sh supervisor/update-prompt.sh \ - lib/env.sh lib/ci-debug.sh lib/ci-helpers.sh lib/load-project.sh \ - lib/parse-deps.sh lib/matrix_listener.sh lib/agent-session.sh + lib/env.sh lib/agent-session.sh lib/ci-debug.sh lib/ci-helpers.sh lib/load-project.sh \ + lib/parse-deps.sh lib/matrix_listener.sh # Run phase protocol test bash dev/phase-test.sh diff --git a/dev/dev-agent.sh b/dev/dev-agent.sh index 930b624..13ad612 100755 --- a/dev/dev-agent.sh +++ b/dev/dev-agent.sh @@ -23,6 +23,8 @@ set -euo pipefail # Load shared environment source "$(dirname "$0")/../lib/env.sh" source "$(dirname "$0")/../lib/agent-session.sh" +# shellcheck source=./phase-handler.sh +source "$(dirname "$0")/phase-handler.sh" # Auto-pull factory code to pick up merged fixes before any logic runs git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true @@ -53,48 +55,26 @@ THREAD_FILE="/tmp/dev-thread-${PROJECT_NAME}-${ISSUE}" # Timing export PHASE_POLL_INTERVAL=30 # seconds between phase checks (read by agent-session.sh) IDLE_TIMEOUT=7200 # 2h: kill session if phase stale this long +# shellcheck disable=SC2034 # used by phase-handler.sh CI_POLL_TIMEOUT=1800 # 30min max for CI to complete +# shellcheck disable=SC2034 # used by phase-handler.sh REVIEW_POLL_TIMEOUT=10800 # 3h max wait for review # Limits +# shellcheck disable=SC2034 # used by phase-handler.sh MAX_CI_FIXES=3 +# shellcheck disable=SC2034 # used by phase-handler.sh MAX_REVIEW_ROUNDS=5 -# Counters — global state across phase transitions +# Counters — global state shared with phase-handler.sh across phase transitions +# shellcheck disable=SC2034 CI_RETRY_COUNT=0 +# shellcheck disable=SC2034 CI_FIX_COUNT=0 +# shellcheck disable=SC2034 REVIEW_ROUND=0 PR_NUMBER="" -# --- Refusal comment helper (used in PHASE:failed handler) --- -post_refusal_comment() { - local emoji="$1" title="$2" body="$3" - local last_has_title - last_has_title=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/issues/${ISSUE}/comments?limit=5" | \ - jq -r --arg t "Dev-agent: ${title}" '[.[] | .body // ""] | any(contains($t)) | tostring') || true - if [ "$last_has_title" = "true" ]; then - log "skipping duplicate refusal comment: ${title}" - return 0 - fi - local comment - comment="${emoji} **Dev-agent: ${title}** - -${body} - ---- -*Automated assessment by dev-agent · $(date -u '+%Y-%m-%d %H:%M UTC')*" - printf '%s' "$comment" > "/tmp/refusal-comment.txt" - jq -Rs '{body: .}' < "/tmp/refusal-comment.txt" > "/tmp/refusal-comment.json" - curl -sf -o /dev/null -X POST \ - -H "Authorization: token ${CODEBERG_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/issues/${ISSUE}/comments" \ - --data-binary @"/tmp/refusal-comment.json" 2>/dev/null || \ - log "WARNING: failed to post refusal comment" - rm -f "/tmp/refusal-comment.txt" "/tmp/refusal-comment.json" -} - # --- Cleanup helpers --- cleanup_worktree() { cd "$REPO_ROOT" @@ -131,137 +111,6 @@ cleanup() { } trap cleanup EXIT -# ============================================================================= -# MERGE HELPER -# ============================================================================= -do_merge() { - local sha="$1" - local pr="${PR_NUMBER}" - - for _m in $(seq 1 20); do - local ci - ci=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/commits/${sha}/status" | jq -r '.state // "unknown"') - [ "$ci" = "success" ] && break - if [ "$ci" = "failure" ] || [ "$ci" = "error" ]; then - log "CI is red before merge attempt — aborting" - notify "PR #${pr} CI is failing; cannot merge." - return 1 - fi - sleep 30 - done - - # Pre-emptive rebase to avoid merge conflicts - local mergeable - mergeable=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/pulls/${pr}" | jq -r '.mergeable // true') - if [ "$mergeable" = "false" ]; then - log "PR #${pr} has merge conflicts — attempting rebase" - local work_dir="${WORKTREE:-$REPO_ROOT}" - if (cd "$work_dir" && git fetch origin "${PRIMARY_BRANCH}" && git rebase "origin/${PRIMARY_BRANCH}" 2>&1); then - log "rebase succeeded — force pushing" - (cd "$work_dir" && git push origin "${BRANCH}" --force-with-lease 2>&1) || true - sha=$(cd "$work_dir" && git rev-parse HEAD) - log "waiting for CI on rebased commit ${sha:0:7}" - local r_ci - for _r in $(seq 1 20); do - r_ci=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/commits/${sha}/status" | jq -r '.state // "unknown"') - [ "$r_ci" = "success" ] && break - if [ "$r_ci" = "failure" ] || [ "$r_ci" = "error" ]; then - log "CI failed after rebase" - notify "PR #${pr} CI failed after rebase. Needs manual fix." - return 1 - fi - sleep 30 - done - else - log "rebase failed — aborting and escalating" - (cd "$work_dir" && git rebase --abort 2>/dev/null) || true - notify "PR #${pr} has merge conflicts that need manual resolution." - return 1 - fi - fi - - local http_code - http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ - -H "Authorization: token ${CODEBERG_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/pulls/${pr}/merge" \ - -d '{"Do":"merge","delete_branch_after_merge":true}') - - if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then - log "PR #${pr} merged!" - curl -sf -X DELETE \ - -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/branches/${BRANCH}" >/dev/null 2>&1 || true - curl -sf -X PATCH \ - -H "Authorization: token ${CODEBERG_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/issues/${ISSUE}" \ - -d '{"state":"closed"}' >/dev/null 2>&1 || true - cleanup_labels - notify_ctx \ - "✅ PR #${pr} merged! Issue #${ISSUE} done." \ - "✅ PR #${pr} merged! Issue #${ISSUE} done." - kill_tmux_session - cleanup_worktree - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" - exit 0 - else - log "merge failed (HTTP ${http_code}) — attempting rebase and retry" - local work_dir="${WORKTREE:-$REPO_ROOT}" - if (cd "$work_dir" && git fetch origin "${PRIMARY_BRANCH}" && git rebase "origin/${PRIMARY_BRANCH}" 2>&1); then - log "rebase succeeded — force pushing" - (cd "$work_dir" && git push origin "${BRANCH}" --force-with-lease 2>&1) || true - sha=$(cd "$work_dir" && git rev-parse HEAD) - log "waiting for CI on rebased commit ${sha:0:7}" - local r2_ci - for _r2 in $(seq 1 20); do - r2_ci=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/commits/${sha}/status" | jq -r '.state // "unknown"') - [ "$r2_ci" = "success" ] && break - if [ "$r2_ci" = "failure" ] || [ "$r2_ci" = "error" ]; then - log "CI failed after merge-retry rebase" - notify "PR #${pr} CI failed after rebase. Needs manual fix." - return 1 - fi - sleep 30 - done - # Re-approve (force push dismisses stale approvals) - curl -sf -X POST \ - -H "Authorization: token ${REVIEW_BOT_TOKEN:-${CODEBERG_TOKEN}}" \ - -H "Content-Type: application/json" \ - "${API}/pulls/${pr}/reviews" \ - -d '{"event":"APPROVED","body":"Auto-approved after rebase."}' >/dev/null 2>&1 || true - # Retry merge - http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ - -H "Authorization: token ${CODEBERG_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/pulls/${pr}/merge" \ - -d '{"Do":"merge","delete_branch_after_merge":true}') - if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then - log "PR #${pr} merged after rebase!" - notify_ctx \ - "✅ PR #${pr} merged! Issue #${ISSUE} done." \ - "✅ PR #${pr} merged! Issue #${ISSUE} done." - curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true - cleanup_labels - kill_tmux_session - cleanup_worktree - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" - exit 0 - fi - else - (cd "$work_dir" && git rebase --abort 2>/dev/null) || true - fi - log "merge still failing after rebase (HTTP ${http_code})" - notify "PR #${pr} merge failed after rebase (HTTP ${http_code}). Needs human attention." - return 1 - fi -} # ============================================================================= # LOG ROTATION @@ -331,24 +180,8 @@ fi # ============================================================================= status "preflight check" -# Extract dependency references from issue body -# Only from ## Dependencies / ## Depends on / ## Blocked by sections -# and inline "depends on #NNN" / "blocked by #NNN" phrases. -# NEVER extract from ## Related or other sections. -DEP_NUMBERS="" - -# 1. Inline phrases anywhere in body (explicit dep language only) -INLINE_DEPS=$(echo "$ISSUE_BODY" | \ - grep -ioP '(?:depends on|blocked by)\s+#\K[0-9]+' | \ - sort -un || true) -[ -n "$INLINE_DEPS" ] && DEP_NUMBERS="$INLINE_DEPS" - -# 2. ## Dependencies / ## Depends on / ## Blocked by section (bullet items) -DEP_SECTION=$(echo "$ISSUE_BODY" | sed -n '/^##\?\s*\(Dependencies\|Depends on\|Blocked by\)/I,/^##/p' | sed '1d;$d') -if [ -n "$DEP_SECTION" ]; then - SECTION_DEPS=$(echo "$DEP_SECTION" | grep -oP '#\K[0-9]+' | sort -un || true) - DEP_NUMBERS=$(printf '%s\n%s' "$DEP_NUMBERS" "$SECTION_DEPS" | sort -un | grep -v '^$' || true) -fi +# Extract dependency references using shared parser +DEP_NUMBERS=$(echo "$ISSUE_BODY" | bash "${FACTORY_ROOT}/lib/parse-deps.sh") BLOCKED_BY=() if [ -n "$DEP_NUMBERS" ]; then @@ -815,526 +648,6 @@ if [ ! -f "${THREAD_FILE}" ] || [ -z "$(cat "$THREAD_FILE" 2>/dev/null)" ]; then fi notify "tmux session ${SESSION_NAME} started for issue #${ISSUE}: ${ISSUE_TITLE}" -# ============================================================================= -# PHASE MONITORING LOOP -# ============================================================================= - -# _on_phase_change — Phase dispatch callback for monitor_phase_loop -# Receives the current phase as $1. -# Returns 0 to continue the loop, 1 to break (terminal phase reached). -_on_phase_change() { - local phase="$1" - - # ── PHASE: awaiting_ci ────────────────────────────────────────────────────── - if [ "$phase" = "PHASE:awaiting_ci" ]; then - - # Create PR if not yet created - if [ -z "${PR_NUMBER:-}" ]; then - status "creating PR for issue #${ISSUE}" - IMPL_SUMMARY="" - if [ -f "$IMPL_SUMMARY_FILE" ]; then - # Don't treat refusal JSON as a PR summary - if ! jq -e '.status' < "$IMPL_SUMMARY_FILE" >/dev/null 2>&1; then - IMPL_SUMMARY=$(head -c 4000 "$IMPL_SUMMARY_FILE") - fi - fi - - printf 'Fixes #%s\n\n## Changes\n%s' "$ISSUE" "$IMPL_SUMMARY" > "/tmp/pr-body-${ISSUE}.txt" - jq -n \ - --arg title "fix: ${ISSUE_TITLE} (#${ISSUE})" \ - --rawfile body "/tmp/pr-body-${ISSUE}.txt" \ - --arg head "$BRANCH" \ - --arg base "${PRIMARY_BRANCH}" \ - '{title: $title, body: $body, head: $head, base: $base}' > "/tmp/pr-request-${ISSUE}.json" - - PR_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - -H "Authorization: token ${CODEBERG_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/pulls" \ - --data-binary @"/tmp/pr-request-${ISSUE}.json") - - PR_HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1) - PR_RESPONSE_BODY=$(echo "$PR_RESPONSE" | sed '$d') - rm -f "/tmp/pr-body-${ISSUE}.txt" "/tmp/pr-request-${ISSUE}.json" - - if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then - PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number') - log "created PR #${PR_NUMBER}" - PR_URL="${CODEBERG_WEB}/pulls/${PR_NUMBER}" - notify_ctx \ - "PR #${PR_NUMBER} created: ${ISSUE_TITLE}" \ - "PR #${PR_NUMBER} created: ${ISSUE_TITLE}" - elif [ "$PR_HTTP_CODE" = "409" ]; then - # PR already exists (race condition) — find it - FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/pulls?state=open&limit=20" | \ - jq -r --arg branch "$BRANCH" \ - '.[] | select(.head.ref == $branch) | .number' | head -1) || true - if [ -n "$FOUND_PR" ]; then - PR_NUMBER="$FOUND_PR" - log "PR already exists: #${PR_NUMBER}" - else - log "ERROR: PR creation got 409 but no existing PR found" - inject_into_session "ERROR: Could not create PR (HTTP 409, no existing PR found). Check the Codeberg API. Retry by writing PHASE:awaiting_ci again after verifying the branch was pushed." - return 0 - fi - else - log "ERROR: PR creation failed (HTTP ${PR_HTTP_CODE})" - notify "failed to create PR (HTTP ${PR_HTTP_CODE})" - inject_into_session "ERROR: Could not create PR (HTTP ${PR_HTTP_CODE}). Check branch was pushed: git push origin ${BRANCH}. Then write PHASE:awaiting_ci again." - return 0 - fi - fi - - # No CI configured? Treat as success immediately - if [ "${WOODPECKER_REPO_ID:-2}" = "0" ]; then - log "no CI configured — treating as passed" - inject_into_session "CI passed on PR #${PR_NUMBER} (no CI configured for this project). -Write PHASE:awaiting_review to the phase file, then stop and wait for review feedback." - return 0 - fi - - # Poll CI until done or timeout - status "waiting for CI on PR #${PR_NUMBER}" - CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || \ - curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/pulls/${PR_NUMBER}" | jq -r '.head.sha') - - CI_DONE=false - CI_STATE="unknown" - CI_POLL_ELAPSED=0 - while [ "$CI_POLL_ELAPSED" -lt "$CI_POLL_TIMEOUT" ]; do - sleep 30 - CI_POLL_ELAPSED=$(( CI_POLL_ELAPSED + 30 )) - - # Check session still alive during CI wait - if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then - log "session died during CI wait" - break - fi - - CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/commits/${CI_CURRENT_SHA}/status" | jq -r '.state // "unknown"') - if [ "$CI_STATE" = "success" ] || [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then - CI_DONE=true - [ "$CI_STATE" = "success" ] && CI_FIX_COUNT=0 - break - fi - done - - if ! $CI_DONE; then - log "TIMEOUT: CI didn't complete in ${CI_POLL_TIMEOUT}s" - notify "CI timeout on PR #${PR_NUMBER}" - inject_into_session "CI TIMEOUT: CI did not complete within 30 minutes for PR #${PR_NUMBER} (SHA: ${CI_CURRENT_SHA:0:7}). This may be an infrastructure issue. Write PHASE:needs_human if you cannot proceed." - return 0 - fi - - log "CI: ${CI_STATE}" - - if [ "$CI_STATE" = "success" ]; then - inject_into_session "CI passed on PR #${PR_NUMBER}. -Write PHASE:awaiting_review to the phase file, then stop and wait for review feedback: - echo \"PHASE:awaiting_review\" > \"${PHASE_FILE}\"" - else - # Fetch CI error details - PIPELINE_NUM=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/commits/${CI_CURRENT_SHA}/status" | \ - jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true) - - FAILED_STEP="" - FAILED_EXIT="" - IS_INFRA=false - if [ -n "$PIPELINE_NUM" ]; then - FAILED_INFO=$(curl -sf \ - -H "Authorization: Bearer ${WOODPECKER_TOKEN}" \ - "${WOODPECKER_SERVER}/api/repos/${WOODPECKER_REPO_ID}/pipelines/${PIPELINE_NUM}" | \ - jq -r '.workflows[]?.children[]? | select(.state=="failure") | "\(.name)|\(.exit_code)"' | head -1 || true) - FAILED_STEP=$(echo "$FAILED_INFO" | cut -d'|' -f1) - FAILED_EXIT=$(echo "$FAILED_INFO" | cut -d'|' -f2) - fi - - log "CI failed: step=${FAILED_STEP:-unknown} exit=${FAILED_EXIT:-?}" - - case "${FAILED_STEP}" in git*) IS_INFRA=true ;; esac - case "${FAILED_EXIT}" in 128|137) IS_INFRA=true ;; esac - - if [ "$IS_INFRA" = true ] && [ "${CI_RETRY_COUNT:-0}" -lt 1 ]; then - CI_RETRY_COUNT=$(( CI_RETRY_COUNT + 1 )) - log "infra failure — retrigger CI (retry ${CI_RETRY_COUNT})" - (cd "$WORKTREE" && git commit --allow-empty \ - -m "ci: retrigger after infra failure (#${ISSUE})" --no-verify 2>&1 | tail -1) - (cd "$WORKTREE" && git push origin "$BRANCH" --force 2>&1 | tail -3) - # Touch phase file so we recheck CI on the new SHA - # Do NOT update LAST_PHASE_MTIME here — let the main loop detect the fresh mtime - touch "$PHASE_FILE" - CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || true) - return 0 - fi - - CI_FIX_COUNT=$(( CI_FIX_COUNT + 1 )) - _ci_pipeline_url="${WOODPECKER_SERVER}/repos/${WOODPECKER_REPO_ID}/pipeline/${PIPELINE_NUM:-0}" - if [ "$CI_FIX_COUNT" -gt "$MAX_CI_FIXES" ]; then - log "CI failure not recoverable after ${CI_FIX_COUNT} fix attempts — escalating" - echo "{\"issue\":${ISSUE},\"pr\":${PR_NUMBER},\"reason\":\"ci_exhausted\",\"step\":\"${FAILED_STEP:-unknown}\",\"attempts\":${CI_FIX_COUNT},\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \ - >> "${FACTORY_ROOT}/supervisor/escalations-${PROJECT_NAME}.jsonl" - notify_ctx \ - "CI exhausted after ${CI_FIX_COUNT} attempts — escalated to supervisor" \ - "CI exhausted after ${CI_FIX_COUNT} attempts on PR #${PR_NUMBER} | Pipeline
Step: ${FAILED_STEP:-unknown} — escalated to supervisor" - printf 'PHASE:failed\nReason: ci_exhausted after %d attempts\n' "$CI_FIX_COUNT" > "$PHASE_FILE" - # Do NOT update LAST_PHASE_MTIME here — let the main loop detect PHASE:failed - return 0 - fi - - CI_ERROR_LOG="" - if [ -n "$PIPELINE_NUM" ]; then - CI_ERROR_LOG=$(bash "${FACTORY_ROOT}/lib/ci-debug.sh" failures "$PIPELINE_NUM" 2>/dev/null | tail -80 | head -c 8000 || echo "") - fi - - # Save CI result for crash recovery - printf 'CI failed (attempt %d/%d)\nStep: %s\nExit: %s\n\n%s' \ - "$CI_FIX_COUNT" "$MAX_CI_FIXES" "${FAILED_STEP:-unknown}" "${FAILED_EXIT:-?}" "$CI_ERROR_LOG" \ - > "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" 2>/dev/null || true - - # Notify Matrix with rich CI failure context - _ci_snippet=$(printf '%s' "${CI_ERROR_LOG:-}" | tail -5 | head -c 500 | sed 's/&/\&/g; s//\>/g') - notify_ctx \ - "CI failed on PR #${PR_NUMBER}: step=${FAILED_STEP:-unknown} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES})" \ - "CI failed on PR #${PR_NUMBER} | Pipeline #${PIPELINE_NUM:-?}
Step: ${FAILED_STEP:-unknown} (exit ${FAILED_EXIT:-?})
Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}
${_ci_snippet:-no logs}
" - - inject_into_session "CI failed on PR #${PR_NUMBER} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}). - -Failed step: ${FAILED_STEP:-unknown} (exit code ${FAILED_EXIT:-?}, pipeline #${PIPELINE_NUM:-?}) - -CI debug tool: - bash ${FACTORY_ROOT}/lib/ci-debug.sh failures ${PIPELINE_NUM:-0} - bash ${FACTORY_ROOT}/lib/ci-debug.sh logs ${PIPELINE_NUM:-0} - -Error snippet: -${CI_ERROR_LOG:-No logs available. Use ci-debug.sh to query the pipeline.} - -Instructions: -1. Run ci-debug.sh failures to get the full error output. -2. Read the failing test file(s) — understand what the tests EXPECT. -3. Fix the root cause — do NOT weaken tests. -4. Commit your fix and push: git push origin ${BRANCH} -5. Write: echo \"PHASE:awaiting_ci\" > \"${PHASE_FILE}\" -6. Stop and wait." - fi - - # ── PHASE: awaiting_review ────────────────────────────────────────────────── - elif [ "$phase" = "PHASE:awaiting_review" ]; then - status "waiting for review on PR #${PR_NUMBER:-?}" - CI_FIX_COUNT=0 # Reset CI fix budget for this review cycle - - if [ -z "${PR_NUMBER:-}" ]; then - log "WARNING: awaiting_review but PR_NUMBER unknown — searching for PR" - FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/pulls?state=open&limit=20" | \ - jq -r --arg branch "$BRANCH" \ - '.[] | select(.head.ref == $branch) | .number' | head -1) || true - if [ -n "$FOUND_PR" ]; then - PR_NUMBER="$FOUND_PR" - log "found PR #${PR_NUMBER}" - else - inject_into_session "ERROR: Cannot find open PR for branch ${BRANCH}. Did you push? Verify with git status and git push origin ${BRANCH}, then write PHASE:awaiting_ci." - return 0 - fi - fi - - REVIEW_POLL_ELAPSED=0 - REVIEW_FOUND=false - while [ "$REVIEW_POLL_ELAPSED" -lt "$REVIEW_POLL_TIMEOUT" ]; do - sleep 300 # 5 min between review checks - REVIEW_POLL_ELAPSED=$(( REVIEW_POLL_ELAPSED + 300 )) - - # Check session still alive - if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then - log "session died during review wait" - REVIEW_FOUND=false - break - fi - - # Check if phase was updated while we wait (e.g., Claude reacted to something) - NEW_MTIME=$(stat -c %Y "$PHASE_FILE" 2>/dev/null || echo 0) - if [ "$NEW_MTIME" -gt "$LAST_PHASE_MTIME" ]; then - log "phase file updated during review wait — re-entering main loop" - # Do NOT update LAST_PHASE_MTIME here — leave it stale so the outer - # loop detects the change on its next tick and dispatches the new phase. - REVIEW_FOUND=true # Prevent timeout injection - # Clean up review-poll sentinel if it exists (session already advanced) - rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}" - break - fi - - REVIEW_SHA=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ - "${API}/pulls/${PR_NUMBER}" | jq -r '.head.sha') || true - REVIEW_COMMENT=$(codeberg_api_all "/issues/${PR_NUMBER}/comments" | \ - jq -r --arg sha "$REVIEW_SHA" \ - '[.[] | select(.body | contains("