From 1e60c9dd5e3360c9404d492cf735d913ff3911d5 Mon Sep 17 00:00:00 2001 From: sonesuke Date: Sun, 22 Feb 2026 14:49:04 +0900 Subject: [PATCH] feat: port pr-healer autonomous agent from arxiv-cli --- AGENTS.md | 11 +++ agents/pr-healer/healer.sh | 91 +++++++++++++++++++++++ agents/pr-healer/prompt.txt | 44 +++++++++++ agents/pr-healer/tools/load-progress.sh | 20 +++++ agents/pr-healer/tools/record-progress.sh | 24 ++++++ 5 files changed, 190 insertions(+) create mode 100755 agents/pr-healer/healer.sh create mode 100644 agents/pr-healer/prompt.txt create mode 100755 agents/pr-healer/tools/load-progress.sh create mode 100755 agents/pr-healer/tools/record-progress.sh diff --git a/AGENTS.md b/AGENTS.md index 85cec5f..c5ea573 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,3 +21,14 @@ This repository (`patent-kit`) is a **Claude Plugin Marketplace** containing adv - Format all files (`.md`, `.json`) using Prettier: `npx prettier --write .` (or via `mise run fmt`). - Before committing structural changes to the plugin, validate the integrity by running `claude plugin validate .` in the project root. + +## Autonomous Agents (Host Loop) + +This repository includes autonomous agent scripts under `agents/` that can be run on the host machine to perform background tasks. + +### PR-Healer (`agents/pr-healer/healer.sh`) + +An autonomous daemon that checks for failing GitHub Actions CI checks on open Pull Requests. + +- **Workflow**: Finds failing PRs → Runs `claude` inside the Dev Container (`devcontainer exec`) → Analyzes the failure (typically using `mise run pre-commit`) → Commits the fix and replies to the PR. +- **Requirements**: Requires Docker, GitHub CLI (`gh`), `devcontainer` CLI, and `jq` installed on the host machine. diff --git a/agents/pr-healer/healer.sh b/agents/pr-healer/healer.sh new file mode 100755 index 0000000..57d90c8 --- /dev/null +++ b/agents/pr-healer/healer.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# agents/pr-healer/healer.sh (Host side) +# The simplified "Host Loop" daemon script. + +set -e +set -o pipefail + +# --- Pre-flight Checks --- +check_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "[Error] Required command '$1' not found. Please install it." >&2 + return 1 + fi +} + +check_command "gh" || exit 1 +check_command "devcontainer" || exit 1 +check_command "jq" || exit 1 + +# Check if Docker is running +if ! docker info >/dev/null 2>&1; then + echo "[Error] Docker is not running or accessible. Please start Docker Desktop." >&2 + exit 1 +fi + +GITHUB_TOKEN=$(gh auth token) +WORKSPACE_FOLDER="${WORKSPACE_FOLDER:-$(pwd)}" + +echo "[Host] Ensuring dev container is up for $WORKSPACE_FOLDER..." +devcontainer up --workspace-folder "$WORKSPACE_FOLDER" + +# Trap Ctrl+C to exit gracefully +trap 'echo "[Host] Caught SIGINT. Cleaning up..."; kill $CURRENT_PID 2>/dev/null; exit 0' SIGINT + +# Variable to hold the current child process ID for the trap +CURRENT_PID="" + +# --- Orchestration Loop --- +while :; do + echo "==================================================" + echo "[Host] Starting True Agentic PR-Healer Loop..." + echo "[Host] Triggering Claude inside Dev Container..." + + # Remove the ALL_CLEAR flag before each run + rm -f agents/pr-healer/ALL_CLEAR + + # Run Claude inside the container. + # Use a temporary file for stderr to avoid swallowing it in jq pipe while keeping jq for stdout. + TEMP_ERR=$(mktemp) + + # We run 'devcontainer exec' in the foreground so we can catch its exit code. + # We still use jq to format the JSON stream from Claude. + if ! devcontainer exec \ + --workspace-folder "$WORKSPACE_FOLDER" \ + --remote-env "GITHUB_TOKEN=$GITHUB_TOKEN" \ + claude -p \ + --dangerously-skip-permissions \ + --verbose \ + --output-format stream-json \ + "$(cat agents/pr-healer/prompt.txt)" < /dev/null 2>"$TEMP_ERR" | jq . ; then + + EXIT_CODE=$? + echo "[Host] Error: Claude agent or devcontainer failed with exit code $EXIT_CODE." >&2 + if [ -s "$TEMP_ERR" ]; then + echo "[Host] Detailed error log:" >&2 + cat "$TEMP_ERR" >&2 + fi + rm -f "$TEMP_ERR" + + # Determine if we should retry or stop. + # For now, stop on errors to avoid infinite loops of failure. + echo "[Host] Terminating loop due to error." >&2 + exit $EXIT_CODE + fi + rm -f "$TEMP_ERR" + + # If Claude determines there's nothing left to do, it will touch this flag file. + if [ -f "agents/pr-healer/ALL_CLEAR" ]; then + echo "[Host] Claude reported all PRs are clean. Sleeping for 5 minutes before checking again..." + rm -f agents/pr-healer/ALL_CLEAR + sleep 300 & + CURRENT_PID=$! + wait $CURRENT_PID + continue + fi + + echo "[Host] Healer agent finished a turn. Restarting loop..." + sleep 2 & + CURRENT_PID=$! + wait $CURRENT_PID +done diff --git a/agents/pr-healer/prompt.txt b/agents/pr-healer/prompt.txt new file mode 100644 index 0000000..dece9fd --- /dev/null +++ b/agents/pr-healer/prompt.txt @@ -0,0 +1,44 @@ +You are Ralph the PR-Healer, an autonomous agent that hunts and fixes failing pull requests. +DO NOT SUMMARIZE. DO NOT EXPLAIN WHAT YOU WOULD DO. EXECUTE EVERY STEP BELOW IMMEDIATELY. + +1. READ PAST CONTEXT: + - Run: ./agents/pr-healer/tools/load-progress.sh + - This shows past decisions, blockers, and files changed by previous iterations. + - Use this context to avoid repeating the same mistakes. + +2. DISCOVER "PREY" (PRs needing attention): + - Use `gh pr list --json number,headRefName` to get all open pull requests. + - For each PR, run: `gh pr view --json statusCheckRollup,mergeStateStatus` + - A PR needs healing ONLY if: + a) All status checks are COMPLETED and at least one has conclusion FAILURE, OR + b) mergeStateStatus is "BEHIND" + - SKIP any PR where status checks are still PENDING or IN_PROGRESS. Do NOT touch them. + - If there are NO PRs needing attention, create the flag file: + touch agents/pr-healer/ALL_CLEAR + Then terminate immediately. + +3. PREPARE & PRIORITIZE (Identify a single failing PR): + - Prioritize PRs with the smallest/simplest changes first. + - Older PRs before newer ones. + - Checkout the branch associated with your chosen PR. + - Merge the latest `main` branch into it to ensure you are up-to-date. + - Run `mise run pre-commit` locally to see what formatting, linting, or test errors occur. + +4. REASON AND HEAL: + - Explore the codebase to understand why the tests/lints are failing. + - Make small, focused changes. Leave the codebase better than you found it. + - Iterate by running `mise run pre-commit` until the checks pass cleanly. + +5. COMMIT, PUSH, COMMENT, AND LOG: + - Once `mise run pre-commit` passes: + git commit -a -m "fix: resolve CI failures for PR #" + - Push your branch to origin: + git push origin + - Leave a summary comment on the PR: + gh pr comment --body "🤖 PR-Healer: Fixed CI failures. Changes: " + - Log your decisions: + ./agents/pr-healer/tools/record-progress.sh "" "" "" "" + +6. FINISH TURN: + - Once a PR is successfully pushed and logged, terminate. + - Do NOT try to fix multiple PRs in a single execution. Fix one, push, log, and exit. diff --git a/agents/pr-healer/tools/load-progress.sh b/agents/pr-healer/tools/load-progress.sh new file mode 100755 index 0000000..75bee9f --- /dev/null +++ b/agents/pr-healer/tools/load-progress.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# agents/pr-healer/tools/load-progress.sh +# Reads and displays the most recent progress entries from progress.jsonl + +PROGRESS_FILE="agents/pr-healer/progress.jsonl" + +if [ ! -f "$PROGRESS_FILE" ]; then + echo "[load-progress] No progress file found. This is a fresh start." + exit 0 +fi + +LINES=$(wc -l < "$PROGRESS_FILE" | tr -d ' ') + +if [ "$LINES" -eq 0 ]; then + echo "[load-progress] Progress file is empty. This is a fresh start." + exit 0 +fi + +echo "[load-progress] Showing last 5 entries (of $LINES total):" +tail -n 5 "$PROGRESS_FILE" | jq . diff --git a/agents/pr-healer/tools/record-progress.sh b/agents/pr-healer/tools/record-progress.sh new file mode 100755 index 0000000..abb99ea --- /dev/null +++ b/agents/pr-healer/tools/record-progress.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# agents/pr-healer/tools/record-progress.sh +# Appends a structured JSONL entry to progress.jsonl + +PROGRESS_FILE="agents/pr-healer/progress.jsonl" + +TASK="${1:-No task specified}" +FILES="${2:-}" +DECISIONS="${3:-}" +BLOCKERS="${4:-}" + +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +# Use jq to safely create JSON +ENTRY=$(jq -n -c \ + --arg ts "$TIMESTAMP" \ + --arg task "$TASK" \ + --arg files "$FILES" \ + --arg decisions "$DECISIONS" \ + --arg blockers "$BLOCKERS" \ + '{timestamp: $ts, task: $task, files: $files, decisions: $decisions, blockers: $blockers}') + +echo "$ENTRY" >> "$PROGRESS_FILE" +echo "[record-progress] Logged: $TASK"