diff --git a/.github/workflows/_bug-fix-agent.yml b/.github/workflows/_bug-fix-agent.yml index 57dc6977a..998d888f5 100644 --- a/.github/workflows/_bug-fix-agent.yml +++ b/.github/workflows/_bug-fix-agent.yml @@ -217,7 +217,7 @@ jobs: run: | ISSUE_NUM="${{ inputs.issue-number }}" BRANCH="fix/auto-${ISSUE_NUM}" - TITLE=$(cat fix-title.txt 2>/dev/null || echo "fix: auto-fix for #${ISSUE_NUM}") + TITLE=$(cat fix-title.txt 2>/dev/null || echo "fix: agent-fix for #${ISSUE_NUM}") git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -243,19 +243,19 @@ jobs: run: | BRANCH="${{ steps.push.outputs.branch }}" BASE="${{ github.event.repository.default_branch }}" - TITLE=$(cat fix-title.txt 2>/dev/null || echo "fix: auto-fix for #${{ inputs.issue-number }}") + TITLE=$(cat fix-title.txt 2>/dev/null || echo "fix: agent-fix for #${{ inputs.issue-number }}") if [ ! -f pr-body.md ]; then echo "Automated fix for #${{ inputs.issue-number }}." > pr-body.md fi - gh label create auto-fix --description "Automated bug fix by Bug Fix Agent" --color 0E8A16 2>/dev/null || true + gh label create agent-fix --description "Automated fix by Bug Fix Agent" --color 0E8A16 2>/dev/null || true gh pr create \ --title "$TITLE" \ --body-file pr-body.md \ --base "$BASE" \ --head "$BRANCH" \ - --label "auto-fix" + --label "agent-fix" - name: Comment on issue (all attempts failed) if: >- @@ -276,4 +276,4 @@ jobs: "$ANALYSIS" > /tmp/comment-body.md gh issue comment "${{ inputs.issue-number }}" --body-file /tmp/comment-body.md - gh issue edit "${{ inputs.issue-number }}" --remove-label "auto-fix" 2>/dev/null || true + gh issue edit "${{ inputs.issue-number }}" --remove-label "agent-fix" 2>/dev/null || true diff --git a/.github/workflows/_feat-agent.yml b/.github/workflows/_feat-agent.yml new file mode 100644 index 000000000..a9c21d512 --- /dev/null +++ b/.github/workflows/_feat-agent.yml @@ -0,0 +1,217 @@ +name: Feature Agent + +on: + workflow_call: + inputs: + issue-number: + required: true + type: number + +concurrency: + group: feat-${{ inputs.issue-number }} + cancel-in-progress: true + +jobs: + implement: + name: Feature Implementation + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3 + with: + app-id: ${{ secrets.TOOLHIVE_STUDIO_CI_APP_ID }} + private-key: ${{ secrets.TOOLHIVE_STUDIO_CI_APP_KEY }} + + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ github.event.repository.default_branch }} + token: ${{ steps.app-token.outputs.token }} + + - name: Check for existing PR or previous attempt + id: guard + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + BRANCH="feat/auto-${{ inputs.issue-number }}" + BASE="${{ github.event.repository.default_branch }}" + EXISTING_PR=$(gh pr list --head "$BRANCH" --base "$BASE" --state open --json number --jq '.[0].number // empty') + + if [ -n "$EXISTING_PR" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::notice::Open PR #${EXISTING_PR} already exists for issue #${{ inputs.issue-number }} — skipping" + exit 0 + fi + + GAVE_UP=$(gh api "repos/${{ github.repository }}/issues/${{ inputs.issue-number }}/comments" \ + --paginate --jq '[.[] | select(.body | contains("Feature Agent"))] | length' 2>/dev/null || echo "0") + if [ "$GAVE_UP" -gt "0" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::notice::Agent already attempted issue #${{ inputs.issue-number }} — skipping" + exit 0 + fi + + echo "skip=false" >> $GITHUB_OUTPUT + + - name: Fetch issue body + if: steps.guard.outputs.skip != 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + gh issue view ${{ inputs.issue-number }} --json title,body,labels \ + --template '# {{.title}}{{"\n\n"}}{{.body}}' > issue-body.md + + - name: Setup + if: steps.guard.outputs.skip != 'true' + uses: ./.github/actions/setup + + - name: 'Phase 1 — Analyze & Plan (Opus)' + id: phase1 + if: steps.guard.outputs.skip != 'true' + continue-on-error: true + uses: anthropics/claude-code-action@5d5c10a4f389689f992ea10bb14dcb6fcc83146d # v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + Read issue-body.md for the feature request. + + Your task (Phase 1 — Analysis & Plan): + 1. Understand what the issue is asking for. + 2. Search the codebase to find relevant files, components, hooks, and patterns. + 3. Identify similar existing features to follow the same patterns. + 4. Write implementation-plan.md with: + ## Summary + What needs to be implemented (1-2 sentences). + + ## Relevant Files + - List of files to modify or create, with what changes are needed. + + ## Patterns to Follow + - Existing patterns in the codebase to replicate. + + ## Test Strategy + - What tests to write and where. + + Do NOT modify any source files. Only create implementation-plan.md. + claude_args: >- + --model opus + --max-turns 30 + --allowedTools "Read,Grep,Glob,Write,Bash(cat *),Bash(ls *)" + + - name: 'Phase 2 — Implement (Sonnet)' + id: phase2 + if: >- + steps.guard.outputs.skip != 'true' + && steps.phase1.outcome == 'success' + continue-on-error: true + uses: anthropics/claude-code-action@5d5c10a4f389689f992ea10bb14dcb6fcc83146d # v1 + env: + FEAT_ISSUE_NUMBER: ${{ inputs.issue-number }} + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + Read implementation-plan.md for the plan. + Read issue-body.md for the original feature request. + + Your task (Phase 2 — Implement): + 1. Follow the implementation plan exactly. + 2. Write clean code that follows existing patterns in the codebase. + 3. Write unit tests for the new functionality. + 4. Run the tests: pnpm run test:nonInteractive + 5. Run pnpm run lint and pnpm run type-check. + 6. If any check fails, fix it (max 5 attempts). + 7. Write pr-body.md (include 'Closes #${{ inputs.issue-number }}') and feat-title.txt. + + Do NOT run git, gh, or modify .env files. + Keep changes minimal and focused on what the issue asks for. + claude_args: >- + --model sonnet + --max-turns 150 + --allowedTools "Read,Grep,Glob,Edit,Write,Bash(pnpm run test:nonInteractive *),Bash(pnpm run lint),Bash(pnpm run type-check),Bash(cat *),Bash(ls *)" + + - name: 'Hard gate — Verify all checks pass' + id: verify + if: >- + steps.guard.outputs.skip != 'true' + && steps.phase2.outcome == 'success' + run: | + if pnpm run test:nonInteractive && pnpm run lint && pnpm run type-check; then + echo "::notice::All checks pass" + echo "result=success" >> $GITHUB_OUTPUT + else + echo "::warning::Verification failed" + echo "result=failure" >> $GITHUB_OUTPUT + fi + + - name: Create branch and commit + if: steps.guard.outputs.skip != 'true' && steps.verify.outputs.result == 'success' + id: push + run: | + ISSUE_NUM="${{ inputs.issue-number }}" + BRANCH="feat/auto-${ISSUE_NUM}" + TITLE=$(cat feat-title.txt 2>/dev/null || echo "feat: implement #${ISSUE_NUM}") + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + + git add -A -- '*.ts' '*.tsx' '**/*.ts' '**/*.tsx' + + if git diff --cached --quiet; then + echo "::warning::No source changes to commit" + echo "has_changes=false" >> $GITHUB_OUTPUT + else + git commit -m "$TITLE" + git push -u origin "$BRANCH" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.guard.outputs.skip != 'true' && steps.verify.outputs.result == 'success' && steps.push.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + BRANCH="${{ steps.push.outputs.branch }}" + BASE="${{ github.event.repository.default_branch }}" + TITLE=$(cat feat-title.txt 2>/dev/null || echo "feat: implement #${{ inputs.issue-number }}") + + if [ ! -f pr-body.md ]; then + echo "Automated implementation for #${{ inputs.issue-number }}." > pr-body.md + fi + + gh label create agent-feat --description "Automated feature by Feature Agent" --color 1D76DB 2>/dev/null || true + gh pr create \ + --title "$TITLE" \ + --body-file pr-body.md \ + --base "$BASE" \ + --head "$BRANCH" \ + --label "agent-feat" + + - name: Comment on issue (failed) + if: >- + always() + && steps.guard.outputs.skip != 'true' + && steps.verify.outputs.result != 'success' + && steps.phase1.outcome != 'skipped' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ -f implementation-plan.md ]; then + ANALYSIS=$(cat implementation-plan.md) + else + ANALYSIS="The agent could not analyze this feature request. The issue may be too vague or require architectural decisions that need human input." + fi + + printf '## Feature Agent — Automated Analysis\n\n%s\n\n---\n*Automated analysis by Claude Code Feature Agent. A developer will implement this feature manually.*\n' \ + "$ANALYSIS" > /tmp/comment-body.md + + gh issue comment "${{ inputs.issue-number }}" --body-file /tmp/comment-body.md + gh issue edit "${{ inputs.issue-number }}" --remove-label "agent-feat" 2>/dev/null || true diff --git a/.github/workflows/bug-fix-on-label.yml b/.github/workflows/bug-fix-on-label.yml index 8a8465e1a..254511c7f 100644 --- a/.github/workflows/bug-fix-on-label.yml +++ b/.github/workflows/bug-fix-on-label.yml @@ -14,7 +14,7 @@ jobs: bug-fix: name: Bug Fix Agent if: >- - github.event.label.name == 'auto-fix' + github.event.label.name == 'agent-fix' && contains(github.event.issue.labels.*.name, 'Bug') uses: ./.github/workflows/_bug-fix-agent.yml with: diff --git a/.github/workflows/bug-triage-cron.yml b/.github/workflows/bug-triage-cron.yml index 046149c89..cf1ecebc3 100644 --- a/.github/workflows/bug-triage-cron.yml +++ b/.github/workflows/bug-triage-cron.yml @@ -36,14 +36,14 @@ jobs: env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | - # Get open Bug issues without auto-fix or auto-fix-skip labels + # Get open Bug issues without agent-fix or agent-fix-skip labels ISSUES=$(gh issue list \ --label "Bug" \ --state open \ --limit 50 \ --json number,title,body,labels,comments \ --jq '[.[] | select( - (.labels | map(.name) | (contains(["auto-fix"]) or contains(["auto-fix-skip"])) | not) + (.labels | map(.name) | (contains(["agent-fix"]) or contains(["agent-fix-skip"])) | not) and (.comments == 0) )]') @@ -144,7 +144,7 @@ jobs: --max-turns 20 --allowedTools "Read,Grep,Glob,Write" - - name: Apply auto-fix label (max 3 per run) + - name: Apply agent-fix label (max 3 per run) if: steps.candidates.outputs.skip != 'true' && steps.filter.outputs.skip != 'true' env: GH_TOKEN: ${{ steps.app-token.outputs.token }} @@ -156,11 +156,11 @@ jobs: SUITABLE=$(jq -r '[.[] | select(.suitable == true)] | .[:3]' triage-results.json) COUNT=$(echo "$SUITABLE" | jq 'length') - echo "::notice::Labeling $COUNT issues as auto-fix" + echo "::notice::Labeling $COUNT issues as agent-fix" echo "$SUITABLE" | jq -c '.[]' | while IFS= read -r ROW; do NUM=$(echo "$ROW" | jq -r '.issue') REASON=$(echo "$ROW" | jq -r '.reason') echo "::notice::Issue #${NUM}: ${REASON}" - gh issue edit "$NUM" --add-label "auto-fix" + gh issue edit "$NUM" --add-label "agent-fix" done diff --git a/.github/workflows/feat-on-label.yml b/.github/workflows/feat-on-label.yml new file mode 100644 index 000000000..ed29fe92d --- /dev/null +++ b/.github/workflows/feat-on-label.yml @@ -0,0 +1,23 @@ +name: Feature Agent (On Label) + +on: + issues: + types: [labeled] + +permissions: + contents: write + pull-requests: write + issues: write + id-token: write + +jobs: + feat: + name: Feature Agent + if: >- + github.event.label.name == 'agent-feat' + && (contains(github.event.issue.labels.*.name, 'Task') + || contains(github.event.issue.labels.*.name, 'enhancement')) + uses: ./.github/workflows/_feat-agent.yml + with: + issue-number: ${{ github.event.issue.number }} + secrets: inherit