diff --git a/.github/seidroid/ai-review/README.md b/.github/seidroid/ai-review/README.md index b2bde18..d9120c3 100644 --- a/.github/seidroid/ai-review/README.md +++ b/.github/seidroid/ai-review/README.md @@ -5,7 +5,7 @@ use. Two workflows live in `.github/workflows/`: | Workflow | Trigger (in the caller) | What it does | |----------|-------------------------|--------------| -| `ai-review.yml` | `pull_request` and PR comment events | Three-pass review (OpenAI Codex ∥ Cursor → Claude synthesizes), posting **one** PR review + an `AI Review` check run. By default it reviews automatically once; callers can enable re-review on every push, and an active allowed-team member can request another review with an exact `@seidroid review` comment. Re-reviews resolve previous seidroid inline threads whose findings were addressed or superseded by a new inline comment. | +| `ai-review.yml` | `pull_request` and PR comment events | Three-pass review (OpenAI Codex ∥ Cursor → Claude synthesizes), posting **one** PR review + an `AI Review` check run. By default it reviews automatically once; callers can enable re-review on every push, and an active allowed-team member can request another review with an exact `@seidroid review` comment. Explicit requests receive a best-effort 👀 reaction while the review runs and 👍 when it completes successfully. Re-reviews resolve previous seidroid inline threads whose findings were addressed or superseded by a new inline comment. | | `ai-assistant.yml` | `issue_comment`, `pull_request_review_comment`, `pull_request_review` | Conversational responder: mention `@seidroid` on a PR and the bot answers in-thread. | ## Base prompts (edit these) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 6d9d44f..8ec3dd9 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -10,6 +10,8 @@ run-name: UCI / AI Review # never see the app token or any write scope. # * claude_review -> consumes BOTH, does its own review, MERGES all three, returns TYPED # output, then a github-script step posts one PR review + a check run. +# * complete_review_reaction -> clears the in-progress reaction and, when the review +# succeeds, marks an explicit request complete. # # Skip switch: label a PR with the `skip-review-label` input value (default `ai: skip-review`) # and the whole pipeline is skipped -- no scouts, no Claude review, no PR review, no @@ -134,9 +136,10 @@ jobs: }} permissions: contents: read - pull-requests: read + pull-requests: write outputs: should_run: ${{ steps.resolve.outputs.should_run }} + reaction_subject_id: ${{ steps.resolve.outputs.reaction_subject_id }} pr_number: ${{ steps.resolve.outputs.pr_number }} head_sha: ${{ steps.resolve.outputs.head_sha }} base_sha: ${{ steps.resolve.outputs.base_sha }} @@ -299,8 +302,34 @@ jobs: } else { core.notice("allowed-team is empty or invalid; denying request."); } + core.setOutput("should_run", String(authorized)); - if (!authorized) core.notice(`${actor} is not authorized to request a seidroid review.`); + if (!authorized) { + core.notice(`${actor} is not authorized to request a seidroid review.`); + return; + } + + const subjectId = eventName === "pull_request_review" + ? context.payload.review?.node_id + : context.payload.comment?.node_id; + if (!subjectId) { + core.warning("The review request has no reactable GitHub node ID."); + return; + } + + try { + await github.graphql( + `mutation($subjectId: ID!) { + addReaction(input: {subjectId: $subjectId, content: EYES}) { + reaction { content } + } + }`, + { subjectId }, + ); + core.setOutput("reaction_subject_id", subjectId); + } catch (error) { + core.warning(`Could not add the in-progress reaction: ${error.message}`); + } - name: Fetch seidroid prompt files if: steps.resolve.outputs.should_run == 'true' @@ -1019,4 +1048,66 @@ jobs: title: `Claude + Codex + Cursor Review: ${verdict}`, summary: summary || "No summary provided.", }, - }); \ No newline at end of file + }); + + complete_review_reaction: + name: Complete review reaction + needs: [preflight, codex_review, cursor_review, claude_review] + if: ${{ always() && needs.preflight.outputs.reaction_subject_id != '' }} + runs-on: ${{ inputs.runs-on }} + permissions: + pull-requests: write + steps: + - name: Detect app credentials + id: creds + env: + APP_ID: ${{ secrets.PLATFORM_CODE_AGENT_APP_ID }} + run: | + if [ -n "$APP_ID" ]; then echo "present=true" >> "$GITHUB_OUTPUT"; else echo "present=false" >> "$GITHUB_OUTPUT"; fi + + - name: Generate GitHub App token + id: app-token + if: steps.creds.outputs.present == 'true' + continue-on-error: true + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.PLATFORM_CODE_AGENT_APP_ID }} + private-key: ${{ secrets.PLATFORM_CODE_AGENT_APP_PK }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + + - name: Mark review request complete + uses: actions/github-script@v9 + env: + SUBJECT_ID: ${{ needs.preflight.outputs.reaction_subject_id }} + REVIEW_SUCCEEDED: ${{ needs.claude_review.result == 'success' }} + with: + github-token: ${{ steps.app-token.outputs.token || github.token }} + script: | + const subjectId = process.env.SUBJECT_ID; + if (process.env.REVIEW_SUCCEEDED === "true") { + try { + await github.graphql( + `mutation($subjectId: ID!) { + addReaction(input: {subjectId: $subjectId, content: THUMBS_UP}) { + reaction { content } + } + }`, + { subjectId }, + ); + } catch (error) { + core.warning(`Could not add the completion reaction: ${error.message}`); + } + } + try { + await github.graphql( + `mutation($subjectId: ID!) { + removeReaction(input: {subjectId: $subjectId, content: EYES}) { + subject { id } + } + }`, + { subjectId }, + ); + } catch (error) { + core.warning(`Could not clear the in-progress reaction: ${error.message}`); + }