feat: rtl support, body interaction reliability (phase 2) #1142
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: AI Risk Assessment | |
| # Runs AFTER risk-label.yml applies the file-path label. | |
| # Only triggers for critical/sensitive PRs — low-risk PRs skip AI entirely. | |
| # Fork PRs skip AI (no access to ANTHROPIC_API_KEY) — file-path label still applies. | |
| # | |
| # - Updates the risk label if AI disagrees with file-path classification | |
| # - Logs full details to workflow output (visible in Actions tab) | |
| on: | |
| pull_request: | |
| types: [opened] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to assess' | |
| required: true | |
| type: number | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: risk-assess-${{ github.event.pull_request.number || github.event.inputs.pr_number }} | |
| cancel-in-progress: true | |
| env: | |
| PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} | |
| jobs: | |
| assess: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Generate app token | |
| id: app-token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| # Layer 1: file-path classification (same as risk-label.yml) | |
| - name: Classify by file paths | |
| id: filepath | |
| run: | | |
| gh pr diff ${{ env.PR_NUMBER }} --name-only \ | |
| | node .github/scripts/risk-label.mjs > /tmp/risk.json | |
| LEVEL=$(node -e "console.log(JSON.parse(require('fs').readFileSync('/tmp/risk.json','utf-8')).level)") | |
| echo "level=$LEVEL" >> $GITHUB_OUTPUT | |
| echo "File-path risk: $LEVEL" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Skip AI when low-risk or API key unavailable (e.g. fork PRs) | |
| - name: Check AI eligibility | |
| id: ai | |
| run: | | |
| if [ "${{ steps.filepath.outputs.level }}" = "low" ]; then | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| echo "Skipping AI — low risk" | |
| elif [ -z "${{ secrets.ANTHROPIC_API_KEY }}" ]; then | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| echo "Skipping AI — no API key (fork PR)" | |
| else | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Install Agent SDK (only when AI will run) | |
| - name: Setup Node and Agent SDK | |
| if: steps.ai.outputs.skip != 'true' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Install Agent SDK | |
| if: steps.ai.outputs.skip != 'true' | |
| run: npm install --prefix .github/scripts @anthropic-ai/claude-agent-sdk | |
| env: | |
| NODE_ENV: production | |
| # Layer 2+3: AI assessment | |
| - name: Run tiered AI assessment | |
| if: steps.ai.outputs.skip != 'true' | |
| id: assess | |
| run: node .github/scripts/risk-assess.mjs ${{ env.PR_NUMBER }} | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_OUTPUT: $GITHUB_OUTPUT | |
| REPO: ${{ github.repository }} | |
| REPO_ROOT: ${{ github.workspace }} | |
| # Update risk label if AI disagrees with file-path classification | |
| - name: Update risk label from AI | |
| if: steps.ai.outputs.skip != 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| script: | | |
| const fs = require('fs'); | |
| const results = JSON.parse(fs.readFileSync('/tmp/tiered-risk-assessment.json', 'utf-8')); | |
| const r = results[0]; | |
| if (!r || r.error) return; | |
| const aiLevel = r.finalLevel; | |
| const filePathLevel = r.filePath?.level; | |
| const prNumber = ${{ env.PR_NUMBER }}; | |
| // Only update if AI assessment differs from file-path | |
| if (aiLevel === filePathLevel) { | |
| console.log(`AI agrees with file-path: ${aiLevel} — no label change`); | |
| return; | |
| } | |
| console.log(`AI reclassified: ${filePathLevel} → ${aiLevel}`); | |
| const LABELS = { | |
| critical: 'review: thorough', | |
| sensitive: 'review: careful', | |
| low: 'review: quick', | |
| }; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| // Remove old risk labels | |
| const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ | |
| owner, repo, issue_number: prNumber, | |
| }); | |
| for (const label of currentLabels.filter(l => l.name.startsWith('review: '))) { | |
| await github.rest.issues.removeLabel({ | |
| owner, repo, issue_number: prNumber, name: label.name, | |
| }); | |
| } | |
| // Add new label | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number: prNumber, labels: [LABELS[aiLevel]], | |
| }); |