diff --git a/.gitea/workflows/claude.yml b/.gitea/workflows/claude.yml new file mode 100644 index 0000000..bfa5dcc --- /dev/null +++ b/.gitea/workflows/claude.yml @@ -0,0 +1,129 @@ +name: Claude Code Assistant + +on: + issues: + types: [opened, labeled] + issue_comment: + types: [created] + +jobs: + claude-code: + if: >- + (github.event_name == 'issues' && + contains(toJSON(github.event.issue.labels), 'claude')) || + (github.event_name == 'issue_comment' && + contains(github.event.comment.body, '@claude')) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Claude on Issue + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GIT_TOKEN: ${{ secrets.GIT_TOKEN }} + run: | + set +e + + # Configure git + git config user.name "Claude Bot" + git config user.email "claude@localhost" + git remote set-url origin "http://admin:${GIT_TOKEN}@localhost:3000/${{ github.repository }}.git" + + ISSUE_NUMBER="${{ github.event.issue.number }}" + ISSUE_TITLE="${{ github.event.issue.title }}" + REPO="${{ github.repository }}" + LABELS_JSON='${{ toJSON(github.event.issue.labels) }}' + + # Determine model + cost limits from issue labels + # Default: haiku (cheap). Add claude:sonnet or claude:opus for harder tasks. + CLAUDE_MODEL="haiku" + MAX_TURNS=15 + MAX_BUDGET="0.50" + EFFORT="low" + if echo "$LABELS_JSON" | grep -q '"claude:opus"'; then + CLAUDE_MODEL="claude-opus-4-6" + MAX_TURNS=40 + MAX_BUDGET="5.00" + EFFORT="high" + elif echo "$LABELS_JSON" | grep -q '"claude:sonnet"'; then + CLAUDE_MODEL="claude-sonnet-4-6" + MAX_TURNS=25 + MAX_BUDGET="2.00" + EFFORT="medium" + fi + + ISSUE_BODY=$(curl -s "http://localhost:3000/api/v1/repos/${REPO}/issues/${ISSUE_NUMBER}" \ + -H "Authorization: token ${GIT_TOKEN}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('body',''))") + + COMMENT_BODY="" + if [ "${{ github.event_name }}" = "issue_comment" ]; then + COMMENT_ID="${{ github.event.comment.id }}" + COMMENT_BODY=$(curl -s "http://localhost:3000/api/v1/repos/${REPO}/issues/comments/${COMMENT_ID}" \ + -H "Authorization: token ${GIT_TOKEN}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('body',''))") + fi + + BRANCH="claude/issue-${ISSUE_NUMBER}" + git checkout -b "${BRANCH}" + + # Run Claude Code with cost controls + claude -p "You are working on the repository ${REPO} (Gitea instance at http://localhost:3000). + A Gitea issue needs your attention: + + Issue #${ISSUE_NUMBER}: ${ISSUE_TITLE} + Description: ${ISSUE_BODY} + Additional context: ${COMMENT_BODY} + + IMPORTANT RULES: + - Do NOT retry failed commands more than once. If something fails twice, stop and report the error. + - Do NOT loop on failing tests. Fix the obvious issue or report it. Never run the same failing command 3+ times. + - If you cannot complete the task, push what you have, create the PR as draft, and explain what is blocked. + - Be efficient: read only files you need, make targeted edits, avoid unnecessary exploration. + + Steps: + 1. Read and understand the relevant parts of the codebase + 2. Implement the requested changes + 3. Commit your changes with a descriptive message + 4. Push branch ${BRANCH} to origin + 5. Create a pull request targeting main that references issue #${ISSUE_NUMBER} + 6. Post a comment on issue #${ISSUE_NUMBER} summarizing what you did + + Git is configured. You are on branch ${BRANCH}. Work in the current directory. + Use git commands to push, and curl to the Gitea API for PR creation and comments. + Gitea API token is available as env var GIT_TOKEN." \ + --allowedTools "Bash,Read,Edit,Write,Glob,Grep" \ + --model "${CLAUDE_MODEL}" \ + --max-turns "${MAX_TURNS}" \ + --max-budget-usd "${MAX_BUDGET}" \ + --effort "${EFFORT}" \ + --permission-mode bypassPermissions \ + --output-format json 2>&1 > /tmp/claude-result.json + + CLAUDE_EXIT=$? + + # Extract cost from JSON output + COST=$(python3 -c " + import json + with open('/tmp/claude-result.json') as f: + data = json.load(f) + cost = data.get('total_cost_usd', 0) + print(f'\${cost:.4f}') + " 2>/dev/null || echo "unknown") + + # Amend the last commit to include cost and model + if git log --oneline main..HEAD 2>/dev/null | head -1 | grep -q .; then + LAST_MSG=$(git log -1 --format=%B) + git commit --amend -m "${LAST_MSG} + + Claude model: ${CLAUDE_MODEL} | API cost: ${COST}" --no-verify + git push origin "${BRANCH}" --force + fi + + # Post cost as comment + curl -s -X POST "http://localhost:3000/api/v1/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \ + -H "Authorization: token ${GIT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"body\": \"Done (model: **${CLAUDE_MODEL}**, effort: ${EFFORT}, budget cap: \$${MAX_BUDGET}). API cost: **${COST}**\"}" > /dev/null + + exit ${CLAUDE_EXIT}