Skip to content

Agent / Review

Agent / Review #2

Workflow file for this run

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