#!/usr/bin/env bash # dev-agent.sh — Synchronous developer agent for a single issue # # Usage: ./dev-agent.sh # # Architecture: # Synchronous bash loop using claude -p (one-shot invocations). # Session continuity via --resume and .sid file. # CI/review loop delegated to pr_walk_to_merge(). # # Flow: # 1. Preflight: issue_check_deps, issue_claim, memory guard, lock # 2. Worktree: worktree_recover or worktree_create # 3. Prompt: build context (issue body, open issues, push instructions) # 4. Implement: agent_run → Claude implements + pushes → save session_id # 5. Create PR: pr_create or pr_find_by_branch # 6. Walk to merge: pr_walk_to_merge (CI fix, review feedback loops) # 7. Cleanup: worktree_cleanup, issue_close, label cleanup # # Session file: /tmp/dev-session-{project}-{issue}.sid # Log: tail -f dev-agent.log set -euo pipefail # Load shared environment and libraries source "$(dirname "$0")/../lib/env.sh" source "$(dirname "$0")/../lib/ci-helpers.sh" source "$(dirname "$0")/../lib/issue-lifecycle.sh" source "$(dirname "$0")/../lib/worktree.sh" source "$(dirname "$0")/../lib/pr-lifecycle.sh" source "$(dirname "$0")/../lib/mirrors.sh" source "$(dirname "$0")/../lib/agent-sdk.sh" source "$(dirname "$0")/../lib/formula-session.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 # --- Config --- ISSUE="${1:?Usage: dev-agent.sh }" REPO_ROOT="${PROJECT_REPO_ROOT}" LOCKFILE="/tmp/dev-agent-${PROJECT_NAME:-default}.lock" STATUSFILE="/tmp/dev-agent-status-${PROJECT_NAME:-default}" BRANCH="fix/issue-${ISSUE}" WORKTREE="/tmp/${PROJECT_NAME}-worktree-${ISSUE}" SID_FILE="/tmp/dev-session-${PROJECT_NAME}-${ISSUE}.sid" PREFLIGHT_RESULT="/tmp/dev-agent-preflight.json" IMPL_SUMMARY_FILE="/tmp/dev-impl-summary-${PROJECT_NAME}-${ISSUE}.txt" LOGFILE="${DISINTO_LOG_DIR}/dev/dev-agent.log" log() { printf '[%s] #%s %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$ISSUE" "$*" >> "$LOGFILE" } status() { printf '[%s] dev-agent #%s: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$ISSUE" "$*" > "$STATUSFILE" log "$*" } # ============================================================================= # CLEANUP # ============================================================================= CLAIMED=false PR_NUMBER="" cleanup() { rm -f "$LOCKFILE" "$STATUSFILE" # If we claimed the issue but never created a PR, release it if [ "$CLAIMED" = true ] && [ -z "$PR_NUMBER" ]; then log "cleanup: releasing issue (no PR created)" issue_release "$ISSUE" fi } trap cleanup EXIT # ============================================================================= # LOG ROTATION # ============================================================================= if [ -f "$LOGFILE" ] && [ "$(stat -c%s "$LOGFILE" 2>/dev/null || echo 0)" -gt 102400 ]; then mv "$LOGFILE" "$LOGFILE.old" log "Log rotated" fi # ============================================================================= # MEMORY GUARD # ============================================================================= memory_guard 2000 # ============================================================================= # CONCURRENCY LOCK # ============================================================================= 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 "SKIP: another dev-agent running (PID ${LOCK_PID})" exit 0 fi log "Removing stale lock (PID ${LOCK_PID:-?})" rm -f "$LOCKFILE" fi echo $$ > "$LOCKFILE" # ============================================================================= # FETCH ISSUE # ============================================================================= status "fetching issue" ISSUE_JSON=$(forge_api GET "/issues/${ISSUE}") || true if [ -z "$ISSUE_JSON" ] || ! printf '%s' "$ISSUE_JSON" | jq -e '.id' >/dev/null 2>&1; then log "ERROR: failed to fetch issue #${ISSUE} (API down or invalid response)"; exit 1 fi ISSUE_TITLE=$(printf '%s' "$ISSUE_JSON" | jq -r '.title') ISSUE_BODY=$(printf '%s' "$ISSUE_JSON" | jq -r '.body // ""') ISSUE_BODY_ORIGINAL="$ISSUE_BODY" ISSUE_STATE=$(printf '%s' "$ISSUE_JSON" | jq -r '.state') if [ "$ISSUE_STATE" != "open" ]; then log "SKIP: issue #${ISSUE} is ${ISSUE_STATE}" echo '{"status":"already_done","reason":"issue is closed"}' > "$PREFLIGHT_RESULT" exit 0 fi log "Issue: ${ISSUE_TITLE}" # ============================================================================= # GUARD: Reject formula-labeled issues # ============================================================================= ISSUE_LABELS=$(printf '%s' "$ISSUE_JSON" | jq -r '[.labels[].name] | join(",")') || true if printf '%s' "$ISSUE_LABELS" | grep -qw 'formula'; then log "SKIP: issue #${ISSUE} has 'formula' label" echo '{"status":"unmet_dependency","blocked_by":"formula dispatch not implemented","suggestion":null}' > "$PREFLIGHT_RESULT" exit 0 fi # --- Append human comments to issue body --- _bot_login=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ "${FORGE_API%%/repos*}/user" | jq -r '.login // empty' 2>/dev/null || true) _bot_logins="${_bot_login}" [ -n "${FORGE_BOT_USERNAMES:-}" ] && \ _bot_logins="${_bot_logins:+${_bot_logins},}${FORGE_BOT_USERNAMES}" ISSUE_COMMENTS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ "${FORGE_API}/issues/${ISSUE}/comments" | \ jq -r --arg bots "$_bot_logins" \ '($bots | split(",") | map(select(. != ""))) as $bl | .[] | select(.user.login as $u | $bl | index($u) | not) | "### @\(.user.login) (\(.created_at[:10])):\n\(.body)\n"' 2>/dev/null || true) if [ -n "$ISSUE_COMMENTS" ]; then ISSUE_BODY="${ISSUE_BODY} ## Issue comments ${ISSUE_COMMENTS}" fi # ============================================================================= # PREFLIGHT: Check dependencies # ============================================================================= status "preflight check" if ! issue_check_deps "$ISSUE"; then BLOCKED_LIST=$(printf '#%s, ' "${_ISSUE_BLOCKED_BY[@]}" | sed 's/, $//') COMMENT_BODY="### Blocked by open issues This issue depends on ${BLOCKED_LIST}, which $([ "${#_ISSUE_BLOCKED_BY[@]}" -eq 1 ] && echo "is" || echo "are") not yet closed." [ -n "$_ISSUE_SUGGESTION" ] && COMMENT_BODY="${COMMENT_BODY} **Suggestion:** Work on #${_ISSUE_SUGGESTION} first." issue_post_refusal "$ISSUE" "🚧" "Unmet dependency" "$COMMENT_BODY" # Write preflight result BLOCKED_JSON=$(printf '%s\n' "${_ISSUE_BLOCKED_BY[@]}" | jq -R 'tonumber' | jq -sc '.') if [ -n "$_ISSUE_SUGGESTION" ]; then jq -n --argjson blocked "$BLOCKED_JSON" --argjson suggestion "$_ISSUE_SUGGESTION" \ '{"status":"unmet_dependency","blocked_by":$blocked,"suggestion":$suggestion}' > "$PREFLIGHT_RESULT" else jq -n --argjson blocked "$BLOCKED_JSON" \ '{"status":"unmet_dependency","blocked_by":$blocked,"suggestion":null}' > "$PREFLIGHT_RESULT" fi log "BLOCKED: unmet dependencies: ${_ISSUE_BLOCKED_BY[*]}" exit 0 fi log "preflight passed" # ============================================================================= # CLAIM ISSUE # ============================================================================= if ! issue_claim "$ISSUE"; then log "SKIP: failed to claim issue #${ISSUE} (already assigned to another agent)" echo '{"status":"already_done","reason":"issue was claimed by another agent"}' > "$PREFLIGHT_RESULT" exit 0 fi CLAIMED=true # ============================================================================= # CHECK FOR EXISTING PR (recovery mode) # ============================================================================= RECOVERY_MODE=false # Check issue body for explicit PR reference BODY_PR=$(printf '%s' "$ISSUE_BODY_ORIGINAL" | grep -oP 'Existing PR:\s*#\K[0-9]+' | head -1) || true if [ -n "$BODY_PR" ]; then PR_CHECK=$(forge_api GET "/pulls/${BODY_PR}") || true PR_CHECK_STATE=$(printf '%s' "$PR_CHECK" | jq -r '.state') if [ "$PR_CHECK_STATE" = "open" ]; then PR_NUMBER="$BODY_PR" BRANCH=$(printf '%s' "$PR_CHECK" | jq -r '.head.ref') log "found existing PR #${PR_NUMBER} on branch ${BRANCH} (from issue body)" fi fi # Priority 1: match by branch name if [ -z "$PR_NUMBER" ]; then PR_NUMBER=$(pr_find_by_branch "$BRANCH") || true [ -n "$PR_NUMBER" ] && log "found existing PR #${PR_NUMBER} (from branch match)" fi # Priority 2: match "Fixes #NNN" in PR body if [ -z "$PR_NUMBER" ]; then FOUND_PR=$(forge_api GET "/pulls?state=open&limit=20" | \ jq -r --arg issue "ixes #${ISSUE}\\b" \ '.[] | select(.body | test($issue; "i")) | "\(.number) \(.head.ref)"' | head -1) || true if [ -n "$FOUND_PR" ]; then PR_NUMBER=$(printf '%s' "$FOUND_PR" | awk '{print $1}') BRANCH=$(printf '%s' "$FOUND_PR" | awk '{print $2}') log "found existing PR #${PR_NUMBER} on branch ${BRANCH} (from body match)" fi fi # Priority 3: check closed PRs for prior art PRIOR_ART_DIFF="" if [ -z "$PR_NUMBER" ]; then CLOSED_PR=$(forge_api GET "/pulls?state=closed&limit=30" | \ jq -r --arg issue "#${ISSUE}" \ '.[] | select(.merged != true) | select((.title | contains($issue)) or (.body // "" | test("ixes " + $issue + "\\b"; "i"))) | "\(.number) \(.head.ref)"' | head -1) || true if [ -n "$CLOSED_PR" ]; then CLOSED_PR_NUM=$(printf '%s' "$CLOSED_PR" | awk '{print $1}') log "found closed (unmerged) PR #${CLOSED_PR_NUM} as prior art" PRIOR_ART_DIFF=$(forge_api GET "/pulls/${CLOSED_PR_NUM}.diff" 2>/dev/null \ | head -500) || true fi fi if [ -n "$PR_NUMBER" ]; then RECOVERY_MODE=true log "RECOVERY MODE: adopting PR #${PR_NUMBER} on branch ${BRANCH}" fi # Recover session_id from .sid file (crash recovery) agent_recover_session # ============================================================================= # WORKTREE SETUP # ============================================================================= status "setting up worktree" cd "$REPO_ROOT" # Determine forge remote by matching FORGE_URL host against git remotes _forge_host=$(printf '%s' "$FORGE_URL" | sed 's|https\?://||; s|/.*||') FORGE_REMOTE=$(git remote -v | awk -v host="$_forge_host" '$2 ~ host && /\(push\)/ {print $1; exit}') FORGE_REMOTE="${FORGE_REMOTE:-origin}" export FORGE_REMOTE log "forge remote: ${FORGE_REMOTE}" if [ "$RECOVERY_MODE" = true ]; then if ! worktree_recover "$WORKTREE" "$BRANCH" "$FORGE_REMOTE"; then log "ERROR: worktree recovery failed" issue_release "$ISSUE" CLAIMED=false exit 1 fi else # Ensure repo is in clean state if [ -d "$REPO_ROOT/.git/rebase-merge" ] || [ -d "$REPO_ROOT/.git/rebase-apply" ]; then log "WARNING: stale rebase detected — aborting" git rebase --abort 2>/dev/null || true fi CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") if [ "$CURRENT_BRANCH" != "${PRIMARY_BRANCH}" ]; then git checkout "${PRIMARY_BRANCH}" 2>/dev/null || true fi git fetch "${FORGE_REMOTE}" "${PRIMARY_BRANCH}" 2>/dev/null git pull --ff-only "${FORGE_REMOTE}" "${PRIMARY_BRANCH}" 2>/dev/null || true if ! worktree_create "$WORKTREE" "$BRANCH" "${FORGE_REMOTE}/${PRIMARY_BRANCH}"; then log "ERROR: worktree creation failed" issue_release "$ISSUE" CLAIMED=false exit 1 fi # Symlink shared node_modules from main repo for lib_dir in "$REPO_ROOT"/onchain/lib/*/; do lib_name=$(basename "$lib_dir") if [ -d "$lib_dir/node_modules" ] && [ ! -d "$WORKTREE/onchain/lib/$lib_name/node_modules" ]; then ln -s "$lib_dir/node_modules" "$WORKTREE/onchain/lib/$lib_name/node_modules" 2>/dev/null || true fi done fi # Track files changed for journal entry (will be populated after agent work) FILES_CHANGED="" if [ -n "$REMOTE_SHA" ]; then FILES_CHANGED=$(git -C "$WORKTREE" diff "${FORGE_REMOTE}/${PRIMARY_BRANCH}..HEAD" --name-only 2>/dev/null | tr '\n' ',' | sed 's/,$//') || true fi # ============================================================================= # BUILD PROMPT # ============================================================================= OPEN_ISSUES_SUMMARY=$(forge_api GET "/issues?state=open&labels=backlog&limit=20&type=issues" | \ jq -r '.[] | "#\(.number) \(.title)"' 2>/dev/null || echo "(could not fetch)") PUSH_INSTRUCTIONS=$(build_phase_protocol_prompt "$BRANCH" "$FORGE_REMOTE") # Load lessons from .profile repo if available (pre-session) profile_load_lessons || true LESSONS_INJECTION="${LESSONS_CONTEXT:-}" if [ "$RECOVERY_MODE" = true ]; then GIT_DIFF_STAT=$(git -C "$WORKTREE" diff "${FORGE_REMOTE}/${PRIMARY_BRANCH}..HEAD" --stat 2>/dev/null \ | head -20 || echo "(no diff)") INITIAL_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}. This is issue #${ISSUE} for the ${FORGE_REPO} project. ## Issue: ${ISSUE_TITLE} ${ISSUE_BODY} ## CRASH RECOVERY Your previous session for this issue was interrupted. Resume from where you left off. Git is the checkpoint — your code changes survived. ### Work completed before crash: \`\`\` ${GIT_DIFF_STAT} \`\`\` ### PR: #${PR_NUMBER} (${BRANCH}) **IMPORTANT: PR #${PR_NUMBER} already exists — do NOT create a new PR.** ### Next steps 1. Run \`git log --oneline -5\` and \`git status\` to understand current state. 2. Read AGENTS.md for project conventions. 3. Address any pending review comments or CI failures. 4. Commit and push to \`${BRANCH}\`. ${LESSONS_INJECTION:+## Lessons learned ${LESSONS_INJECTION} } ${PUSH_INSTRUCTIONS}" else INITIAL_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}. You have been assigned issue #${ISSUE} for the ${FORGE_REPO} project. ## Issue: ${ISSUE_TITLE} ${ISSUE_BODY} ## Other open issues labeled 'backlog' (for context): ${OPEN_ISSUES_SUMMARY} $(if [ -n "$PRIOR_ART_DIFF" ]; then printf '## Prior Art (closed PR — DO NOT start from scratch)\n\nA previous PR attempted this issue but was closed without merging. Reuse as much as possible.\n\n```diff\n%s\n```\n' "$PRIOR_ART_DIFF" fi) ${LESSONS_INJECTION:+## Lessons learned ${LESSONS_INJECTION} } ## Instructions 1. Read AGENTS.md in this repo for project context and coding conventions. 2. Implement the changes described in the issue. 3. Run lint and tests before you're done (see AGENTS.md for commands). 4. Commit your changes with message: fix: ${ISSUE_TITLE} (#${ISSUE}) 5. Push your branch. If you cannot implement this issue, write ONLY a JSON object to ${IMPL_SUMMARY_FILE}: - Unmet dependency: {\"status\":\"unmet_dependency\",\"blocked_by\":\"what's missing\",\"suggestion\":} - Too large: {\"status\":\"too_large\",\"reason\":\"explanation\"} - Already done: {\"status\":\"already_done\",\"reason\":\"where\"} ${PUSH_INSTRUCTIONS}" fi # ============================================================================= # IMPLEMENT # ============================================================================= status "running implementation" echo '{"status":"ready"}' > "$PREFLIGHT_RESULT" if [ -n "$_AGENT_SESSION_ID" ]; then agent_run --resume "$_AGENT_SESSION_ID" --worktree "$WORKTREE" "$INITIAL_PROMPT" else agent_run --worktree "$WORKTREE" "$INITIAL_PROMPT" fi # ============================================================================= # CHECK RESULT: did Claude push? # ============================================================================= REMOTE_SHA=$(git ls-remote "$FORGE_REMOTE" "refs/heads/${BRANCH}" 2>/dev/null \ | awk '{print $1}') || true if [ -z "$REMOTE_SHA" ]; then # Check for refusal in summary file if [ -f "$IMPL_SUMMARY_FILE" ] && jq -e '.status' < "$IMPL_SUMMARY_FILE" >/dev/null 2>&1; then REFUSAL_JSON=$(cat "$IMPL_SUMMARY_FILE") REFUSAL_STATUS=$(printf '%s' "$REFUSAL_JSON" | jq -r '.status') log "claude refused: ${REFUSAL_STATUS}" printf '%s' "$REFUSAL_JSON" > "$PREFLIGHT_RESULT" case "$REFUSAL_STATUS" in unmet_dependency) BLOCKED_BY_MSG=$(printf '%s' "$REFUSAL_JSON" | jq -r '.blocked_by // "unknown"') SUGGESTION=$(printf '%s' "$REFUSAL_JSON" | jq -r '.suggestion // empty') COMMENT_BODY="### Blocked by unmet dependency ${BLOCKED_BY_MSG}" [ -n "$SUGGESTION" ] && [ "$SUGGESTION" != "null" ] && \ COMMENT_BODY="${COMMENT_BODY} **Suggestion:** Work on #${SUGGESTION} first." issue_post_refusal "$ISSUE" "🚧" "Unmet dependency" "$COMMENT_BODY" issue_release "$ISSUE" CLAIMED=false ;; too_large) REASON=$(printf '%s' "$REFUSAL_JSON" | jq -r '.reason // "unspecified"') issue_post_refusal "$ISSUE" "📏" "Too large for single session" \ "### Why this can't be implemented as-is ${REASON} ### Next steps A maintainer should split this issue or add more detail to the spec." # Add underspecified label, remove backlog + in-progress UNDERSPEC_ID=$(forge_api GET "/labels" 2>/dev/null \ | jq -r '.[] | select(.name == "underspecified") | .id' 2>/dev/null || true) if [ -n "$UNDERSPEC_ID" ]; then forge_api POST "/issues/${ISSUE}/labels" \ -d "{\"labels\":[${UNDERSPEC_ID}]}" >/dev/null 2>&1 || true fi BACKLOG_ID=$(forge_api GET "/labels" 2>/dev/null \ | jq -r '.[] | select(.name == "backlog") | .id' 2>/dev/null || true) if [ -n "$BACKLOG_ID" ]; then forge_api DELETE "/issues/${ISSUE}/labels/${BACKLOG_ID}" >/dev/null 2>&1 || true fi IP_ID=$(forge_api GET "/labels" 2>/dev/null \ | jq -r '.[] | select(.name == "in-progress") | .id' 2>/dev/null || true) if [ -n "$IP_ID" ]; then forge_api DELETE "/issues/${ISSUE}/labels/${IP_ID}" >/dev/null 2>&1 || true fi CLAIMED=false ;; already_done) REASON=$(printf '%s' "$REFUSAL_JSON" | jq -r '.reason // "unspecified"') issue_post_refusal "$ISSUE" "✅" "Already implemented" \ "### Existing implementation ${REASON} Closing as already implemented." issue_close "$ISSUE" CLAIMED=false ;; esac worktree_cleanup "$WORKTREE" rm -f "$SID_FILE" "$IMPL_SUMMARY_FILE" exit 0 fi log "ERROR: no branch pushed after agent_run" # Dump diagnostics diag_file="${DISINTO_LOG_DIR:-/tmp}/dev/agent-run-last.json" if [ -f "$diag_file" ]; then result_text=""; cost_usd=""; num_turns="" result_text=$(jq -r '.result // "no result field"' "$diag_file" 2>/dev/null | head -50) || result_text="(parse error)" cost_usd=$(jq -r '.cost_usd // "?"' "$diag_file" 2>/dev/null) || cost_usd="?" num_turns=$(jq -r '.num_turns // "?"' "$diag_file" 2>/dev/null) || num_turns="?" log "no_push diagnostics: turns=${num_turns} cost=${cost_usd}" log "no_push result: ${result_text}" # Save full output for later analysis cp "$diag_file" "${DISINTO_LOG_DIR:-/tmp}/dev/no-push-${ISSUE}-$(date +%s).json" 2>/dev/null || true fi # Save full session log for debugging # Session logs are stored in CLAUDE_CONFIG_DIR/projects/{worktree-hash}/{session-id}.jsonl _wt_hash=$(printf '%s' "$WORKTREE" | md5sum | cut -c1-12) _cl_config="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" _session_log="${_cl_config}/projects/${_wt_hash}/${_AGENT_SESSION_ID}.jsonl" if [ -f "$_session_log" ]; then cp "$_session_log" "${DISINTO_LOG_DIR}/dev/no-push-session-${ISSUE}-$(date +%s).jsonl" 2>/dev/null || true log "no_push session log saved to ${DISINTO_LOG_DIR}/dev/no-push-session-${ISSUE}-*.jsonl" fi # Log session summary for debugging if [ -f "$_session_log" ]; then _read_calls=$(grep -c '"type":"read"' "$_session_log" 2>/dev/null || echo "0") _edit_calls=$(grep -c '"type":"edit"' "$_session_log" 2>/dev/null || echo "0") _bash_calls=$(grep -c '"type":"bash"' "$_session_log" 2>/dev/null || echo "0") _text_calls=$(grep -c '"type":"text"' "$_session_log" 2>/dev/null || echo "0") _failed_calls=$(grep -c '"exit_code":null' "$_session_log" 2>/dev/null || echo "0") _total_turns=$(grep -c '"type":"turn"' "$_session_log" 2>/dev/null || echo "0") log "no_push session summary: turns=${_total_turns} reads=${_read_calls} edits=${_edit_calls} bash=${_bash_calls} text=${_text_calls} failed=${_failed_calls}" fi issue_block "$ISSUE" "no_push" "Claude did not push branch ${BRANCH}" CLAIMED=false worktree_cleanup "$WORKTREE" rm -f "$SID_FILE" "$IMPL_SUMMARY_FILE" exit 1 fi log "branch pushed: ${REMOTE_SHA:0:7}" # ============================================================================= # CREATE PR (if not in recovery mode) # ============================================================================= if [ -z "$PR_NUMBER" ]; then status "creating PR" IMPL_SUMMARY="" if [ -f "$IMPL_SUMMARY_FILE" ]; then if ! jq -e '.status' < "$IMPL_SUMMARY_FILE" >/dev/null 2>&1; then IMPL_SUMMARY=$(head -c 4000 "$IMPL_SUMMARY_FILE") fi fi PR_BODY=$(printf 'Fixes #%s\n\n## Changes\n%s' "$ISSUE" "$IMPL_SUMMARY") PR_TITLE="fix: ${ISSUE_TITLE} (#${ISSUE})" PR_NUMBER=$(pr_create "$BRANCH" "$PR_TITLE" "$PR_BODY") || true if [ -z "$PR_NUMBER" ]; then log "ERROR: failed to create PR" issue_block "$ISSUE" "pr_create_failed" CLAIMED=false exit 1 fi log "created PR #${PR_NUMBER}" fi # ============================================================================= # WALK PR TO MERGE # ============================================================================= status "walking PR #${PR_NUMBER} to merge" rc=0 pr_walk_to_merge "$PR_NUMBER" "$_AGENT_SESSION_ID" "$WORKTREE" 3 5 || rc=$? if [ "$rc" -eq 0 ]; then # Merged successfully log "PR #${PR_NUMBER} merged" issue_close "$ISSUE" # Write journal entry post-session (before cleanup) profile_write_journal "$ISSUE" "$ISSUE_TITLE" "merged" "$FILES_CHANGED" || true # Pull primary branch and push to mirrors git -C "$REPO_ROOT" fetch "$FORGE_REMOTE" "$PRIMARY_BRANCH" 2>/dev/null || true git -C "$REPO_ROOT" checkout "$PRIMARY_BRANCH" 2>/dev/null || true git -C "$REPO_ROOT" pull --ff-only "$FORGE_REMOTE" "$PRIMARY_BRANCH" 2>/dev/null || true mirror_push worktree_cleanup "$WORKTREE" rm -f "$SID_FILE" "$IMPL_SUMMARY_FILE" CLAIMED=false else # Exhausted or unrecoverable failure log "PR walk failed: ${_PR_WALK_EXIT_REASON:-unknown}" issue_block "$ISSUE" "${_PR_WALK_EXIT_REASON:-agent_failed}" # Write journal entry post-session (before cleanup) local outcome="blocked_${_PR_WALK_EXIT_REASON:-agent_failed}" profile_write_journal "$ISSUE" "$ISSUE_TITLE" "$outcome" "$FILES_CHANGED" || true CLAIMED=false fi log "dev-agent finished for issue #${ISSUE}"