Agent / Review #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Agent / Review | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: "Pull request number to review" | |
| required: true | |
| requested_by: | |
| description: "GitHub login that requested the run" | |
| required: false | |
| approval_comment_url: | |
| description: "Approval comment URL" | |
| required: false | |
| request_text: | |
| description: "Original user request text forwarded from the portal" | |
| required: false | |
| session_bundle_mode: | |
| description: "Session bundle persistence mode (defaults to repository variable AGENT_SESSION_BUNDLE_MODE or 'auto')" | |
| required: false | |
| default: "" | |
| automation_mode: | |
| description: "Post-action orchestration mode (disabled, heuristics, agent)" | |
| required: false | |
| default: "disabled" | |
| automation_current_round: | |
| description: "Current automation handoff round" | |
| required: false | |
| default: "1" | |
| automation_max_rounds: | |
| description: "Maximum automation handoff rounds" | |
| required: false | |
| default: "12" | |
| orchestration_enabled: | |
| description: "Whether this run belongs to an explicit orchestrator chain" | |
| required: false | |
| default: "false" | |
| workflow_call: | |
| inputs: | |
| pr_number: | |
| type: string | |
| required: true | |
| requested_by: | |
| type: string | |
| required: false | |
| approval_comment_url: | |
| type: string | |
| required: false | |
| request_text: | |
| type: string | |
| required: false | |
| runs_on: | |
| type: string | |
| default: "" | |
| session_bundle_mode: | |
| type: string | |
| default: "" | |
| automation_mode: | |
| type: string | |
| required: false | |
| default: "disabled" | |
| automation_current_round: | |
| type: string | |
| required: false | |
| default: "1" | |
| automation_max_rounds: | |
| type: string | |
| required: false | |
| default: "12" | |
| orchestration_enabled: | |
| type: string | |
| required: false | |
| default: "false" | |
| permissions: | |
| actions: write | |
| contents: read | |
| pull-requests: write | |
| id-token: write # required for GitHub Actions OIDC broker exchange | |
| jobs: | |
| prepare: | |
| if: vars.AGENT_ENABLED != 'false' | |
| permissions: | |
| actions: read | |
| contents: read | |
| pull-requests: read | |
| id-token: write | |
| runs-on: ${{ fromJson(inputs.runs_on || vars.AGENT_RUNS_ON || '["ubuntu-latest"]') }} | |
| outputs: | |
| reviewed_head_sha: ${{ steps.capture.outputs.head_sha }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| token: ${{ github.token }} | |
| - name: Resolve GitHub auth | |
| id: auth | |
| uses: ./.github/actions/resolve-github-auth | |
| with: | |
| app_id: ${{ secrets.AGENT_APP_ID }} | |
| app_private_key: ${{ secrets.AGENT_APP_PRIVATE_KEY }} | |
| pat: ${{ secrets.AGENT_PAT }} | |
| fallback_token: ${{ github.token }} | |
| - name: Setup agent runtime | |
| id: runtime | |
| uses: ./.github/actions/setup-agent-runtime | |
| - name: Capture reviewed head | |
| id: capture | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.auth.outputs.token }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| TARGET_NUMBER: ${{ inputs.pr_number }} | |
| run: node .agent/dist/cli/capture-pr-head.js | |
| # One job definition, two parallel runs via matrix. | |
| # Adding a third reviewer = one more matrix entry. | |
| review: | |
| # Ordering-only: capture the reviewed head before reviewer lanes when | |
| # prepare succeeds. Reviewers still run without provenance if prepare fails. | |
| needs: [prepare] | |
| if: ${{ vars.AGENT_ENABLED != 'false' && !cancelled() }} | |
| # Reviewer lanes are best-effort; synthesis fails later if no lane uploads a review. | |
| continue-on-error: true | |
| permissions: | |
| # Reviewer jobs stay read-only; memory writes happen only in synthesize | |
| # (see memory_mode_override below and per-job permissions on synthesize). | |
| actions: read | |
| contents: read | |
| pull-requests: write | |
| id-token: write | |
| runs-on: ${{ fromJson(inputs.runs_on || vars.AGENT_RUNS_ON || '["ubuntu-latest"]') }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - agent: claude | |
| permission_mode: approve-all | |
| reasoning_effort: max | |
| artifact_name: claude-review | |
| lane: claude-review | |
| - agent: codex | |
| permission_mode: approve-all | |
| reasoning_effort: xhigh | |
| artifact_name: codex-review | |
| lane: codex-review | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event.repository.default_branch }} | |
| token: ${{ github.token }} | |
| - name: Resolve GitHub auth | |
| id: auth | |
| uses: ./.github/actions/resolve-github-auth | |
| with: | |
| app_id: ${{ secrets.AGENT_APP_ID }} | |
| app_private_key: ${{ secrets.AGENT_APP_PRIVATE_KEY }} | |
| pat: ${{ secrets.AGENT_PAT }} | |
| fallback_token: ${{ github.token }} | |
| - name: Setup agent runtime | |
| id: runtime | |
| uses: ./.github/actions/setup-agent-runtime | |
| with: | |
| install_codex: ${{ matrix.agent == 'codex' && 'true' || 'false' }} | |
| install_claude: ${{ matrix.agent == 'claude' && 'true' || 'false' }} | |
| - name: Resolve task timeout | |
| id: task_timeout | |
| env: | |
| AGENT_TASK_TIMEOUT_POLICY: ${{ vars.AGENT_TASK_TIMEOUT_POLICY || '' }} | |
| ROUTE: review | |
| run: node .agent/dist/cli/resolve-task-timeout.js | |
| - name: Run ${{ matrix.agent }} review | |
| id: agent | |
| continue-on-error: true | |
| timeout-minutes: ${{ fromJson(steps.task_timeout.outputs.minutes || '30') }} | |
| uses: ./.github/actions/run-agent-task | |
| with: | |
| agent: ${{ matrix.agent }} | |
| github_token: ${{ steps.auth.outputs.token }} | |
| secondary_github_token: ${{ secrets.AGENT_SECONDARY_GITHUB_TOKEN }} | |
| openai_api_key: ${{ secrets.OPENAI_API_KEY }} | |
| claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| permission_mode: ${{ matrix.permission_mode }} | |
| prompt: review | |
| reasoning_effort: ${{ matrix.reasoning_effort }} | |
| lane: ${{ matrix.lane }} | |
| memory_mode_override: 'read-only' | |
| memory_ref: ${{ vars.AGENT_MEMORY_REF || 'agent/memory' }} | |
| memory_policy: ${{ vars.AGENT_MEMORY_POLICY || '' }} | |
| rubrics_ref: ${{ vars.AGENT_RUBRICS_REF || 'agent/rubrics' }} | |
| rubrics_policy: ${{ vars.AGENT_RUBRICS_POLICY || '' }} | |
| rubrics_limit: ${{ vars.AGENT_RUBRICS_LIMIT || '10' }} | |
| session_bundle_mode: ${{ inputs.session_bundle_mode || vars.AGENT_SESSION_BUNDLE_MODE || 'auto' }} | |
| session_policy: track-only | |
| request_text: ${{ inputs.request_text }} | |
| requested_by: ${{ inputs.requested_by || github.actor }} | |
| route: review | |
| source_kind: workflow_dispatch | |
| target_kind: pull_request | |
| target_number: ${{ inputs.pr_number }} | |
| target_url: ${{ github.server_url }}/${{ github.repository }}/pull/${{ inputs.pr_number }} | |
| workflow: agent-review.yml | |
| - name: Persist review artifacts | |
| if: ${{ steps.agent.outcome == 'success' }} | |
| run: | | |
| set -euo pipefail | |
| cp "${{ steps.agent.outputs.response_file }}" "${{ runner.temp }}/review.md" | |
| printf '%s' "${{ steps.agent.outputs.acpx_session_id }}" > "${{ runner.temp }}/session.txt" | |
| if [ -f "${{ steps.agent.outputs.session_log_file }}" ]; then | |
| cp "${{ steps.agent.outputs.session_log_file }}" "${{ runner.temp }}/events.jsonl" | |
| fi | |
| - uses: actions/upload-artifact@v4 | |
| if: ${{ steps.agent.outcome == 'success' }} | |
| with: | |
| name: ${{ matrix.artifact_name }}-${{ inputs.pr_number }} | |
| path: | | |
| ${{ runner.temp }}/review.md | |
| ${{ runner.temp }}/session.txt | |
| ${{ runner.temp }}/events.jsonl | |
| retention-days: 30 | |
| rubrics-review: | |
| if: vars.AGENT_ENABLED != 'false' | |
| uses: ./.github/workflows/agent-rubrics-review.yml | |
| with: | |
| pr_number: ${{ inputs.pr_number }} | |
| requested_by: ${{ inputs.requested_by || github.actor }} | |
| request_text: ${{ inputs.request_text }} | |
| runs_on: ${{ inputs.runs_on }} | |
| rubrics_ref: ${{ vars.AGENT_RUBRICS_REF || 'agent/rubrics' }} | |
| rubrics_limit: ${{ vars.AGENT_RUBRICS_LIMIT || '10' }} | |
| post_comment: "true" | |
| session_bundle_mode: ${{ inputs.session_bundle_mode || vars.AGENT_SESSION_BUNDLE_MODE || 'auto' }} | |
| secrets: inherit | |
| synthesize: | |
| needs: [prepare, review] | |
| if: ${{ vars.AGENT_ENABLED != 'false' && !cancelled() }} | |
| permissions: | |
| # Synthesize is the only review-path job that writes memory, so it's the | |
| # only one that needs contents:write. Running after both reviewers | |
| # also avoids the parallel-push race. | |
| actions: write | |
| contents: write | |
| pull-requests: write | |
| id-token: write | |
| runs-on: ${{ fromJson(inputs.runs_on || vars.AGENT_RUNS_ON || '["ubuntu-latest"]') }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ github.token }} | |
| - name: Resolve GitHub auth | |
| id: auth | |
| uses: ./.github/actions/resolve-github-auth | |
| with: | |
| app_id: ${{ secrets.AGENT_APP_ID }} | |
| app_private_key: ${{ secrets.AGENT_APP_PRIVATE_KEY }} | |
| pat: ${{ secrets.AGENT_PAT }} | |
| fallback_token: ${{ github.token }} | |
| - name: Download review artifacts | |
| continue-on-error: true | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: ${{ runner.temp }}/reviews | |
| - name: Resolve review inputs | |
| id: reviews | |
| run: | | |
| set -euo pipefail | |
| reviews_dir="${{ runner.temp }}/reviews" | |
| mkdir -p "$reviews_dir" | |
| review_count=$(find "$reviews_dir" -type f -name review.md | wc -l | tr -d '[:space:]') | |
| if [ "$review_count" = "0" ]; then | |
| echo "No review.md files were produced by reviewer runs." >&2 | |
| exit 1 | |
| fi | |
| echo "reviews_dir=$reviews_dir" >> "$GITHUB_OUTPUT" | |
| - name: Resolve synthesis provider | |
| id: synthesis_provider | |
| uses: ./.github/actions/resolve-agent-provider | |
| with: | |
| route: review-synthesize | |
| default_provider: ${{ vars.AGENT_DEFAULT_PROVIDER || 'auto' }} | |
| openai_api_key: ${{ secrets.OPENAI_API_KEY }} | |
| claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| model_policy: ${{ vars.AGENT_MODEL_POLICY || '' }} | |
| - name: Setup agent runtime | |
| id: runtime | |
| uses: ./.github/actions/setup-agent-runtime | |
| with: | |
| install_codex: ${{ steps.synthesis_provider.outputs.install_codex }} | |
| install_claude: ${{ steps.synthesis_provider.outputs.install_claude }} | |
| - name: Resolve task timeout | |
| id: task_timeout | |
| env: | |
| AGENT_TASK_TIMEOUT_POLICY: ${{ vars.AGENT_TASK_TIMEOUT_POLICY || '' }} | |
| ROUTE: review | |
| run: node .agent/dist/cli/resolve-task-timeout.js | |
| - name: Run synthesis | |
| id: synthesis | |
| timeout-minutes: ${{ fromJson(steps.task_timeout.outputs.minutes || '30') }} | |
| uses: ./.github/actions/run-agent-task | |
| with: | |
| agent: ${{ steps.synthesis_provider.outputs.provider }} | |
| github_token: ${{ steps.auth.outputs.token }} | |
| secondary_github_token: ${{ secrets.AGENT_SECONDARY_GITHUB_TOKEN }} | |
| openai_api_key: ${{ secrets.OPENAI_API_KEY }} | |
| claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| model: ${{ steps.synthesis_provider.outputs.model }} | |
| display_model: ${{ vars.AGENT_DISPLAY_MODEL || '' }} | |
| permission_mode: approve-all | |
| prompt: review-synthesize | |
| reasoning_effort: ${{ steps.synthesis_provider.outputs.reasoning_effort || (steps.synthesis_provider.outputs.provider == 'claude' && 'max' || 'xhigh') }} | |
| lane: synthesize | |
| memory_ref: ${{ vars.AGENT_MEMORY_REF || 'agent/memory' }} | |
| memory_policy: ${{ vars.AGENT_MEMORY_POLICY || '' }} | |
| rubrics_ref: ${{ vars.AGENT_RUBRICS_REF || 'agent/rubrics' }} | |
| rubrics_policy: ${{ vars.AGENT_RUBRICS_POLICY || '' }} | |
| rubrics_limit: ${{ vars.AGENT_RUBRICS_LIMIT || '10' }} | |
| session_bundle_mode: ${{ inputs.session_bundle_mode || vars.AGENT_SESSION_BUNDLE_MODE || 'auto' }} | |
| session_policy: track-only | |
| requested_by: ${{ inputs.requested_by || github.actor }} | |
| route: review | |
| source_kind: workflow_dispatch | |
| target_kind: pull_request | |
| target_number: ${{ inputs.pr_number }} | |
| target_url: ${{ github.server_url }}/${{ github.repository }}/pull/${{ inputs.pr_number }} | |
| workflow: agent-review.yml | |
| env: | |
| REVIEWS_DIR: ${{ steps.reviews.outputs.reviews_dir }} | |
| - name: Post review comment | |
| id: post_comment | |
| env: | |
| AGENT_COLLAPSE_OLD_REVIEWS: ${{ vars.AGENT_COLLAPSE_OLD_REVIEWS }} | |
| APPROVAL_COMMENT_URL: ${{ inputs.approval_comment_url }} | |
| COMMENT_TARGET: pr | |
| GH_TOKEN: ${{ steps.auth.outputs.token }} | |
| REQUESTED_BY: ${{ inputs.requested_by }} | |
| RESPONSE_FILE: ${{ steps.synthesis.outputs.response_file }} | |
| MODEL_DISPLAY: ${{ steps.synthesis.outputs.model_display }} | |
| REVIEWED_HEAD_SHA: ${{ needs.prepare.outputs.reviewed_head_sha }} | |
| ROUTE: review | |
| STATUS: success | |
| TARGET_NUMBER: ${{ inputs.pr_number }} | |
| run: node .agent/dist/cli/post-comment.js | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: agent-review-result-${{ inputs.pr_number }} | |
| path: | | |
| ${{ steps.synthesis.outputs.response_file }} | |
| ${{ steps.synthesis.outputs.session_log_file }} | |
| retention-days: 30 | |
| - name: Orchestrate automation handoff | |
| if: >- | |
| always() && | |
| steps.auth.outputs.token && | |
| steps.post_comment.outcome == 'success' && | |
| inputs.orchestration_enabled == 'true' | |
| env: | |
| AUTOMATION_CURRENT_ROUND: ${{ inputs.automation_current_round }} | |
| AUTOMATION_MAX_ROUNDS: ${{ inputs.automation_max_rounds }} | |
| AUTOMATION_MODE: ${{ inputs.automation_mode }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| GH_TOKEN: ${{ steps.auth.outputs.token }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| ORCHESTRATION_ENABLED: ${{ inputs.orchestration_enabled }} | |
| REQUESTED_BY: ${{ inputs.requested_by || github.actor }} | |
| REQUEST_TEXT: ${{ inputs.request_text }} | |
| # SOURCE_CONCLUSION is intentionally omitted for review; the dispatcher | |
| # derives the verdict from the synthesis response file below. | |
| RESPONSE_FILE: ${{ steps.synthesis.outputs.response_file }} | |
| SESSION_BUNDLE_MODE: ${{ inputs.session_bundle_mode || vars.AGENT_SESSION_BUNDLE_MODE || 'auto' }} | |
| SOURCE_ACTION: review | |
| TARGET_KIND: pull_request | |
| TARGET_NUMBER: ${{ inputs.pr_number }} | |
| run: node .agent/dist/cli/dispatch-agent-orchestrator.js |