Claude Code Review (fix/claude-credentials-linux) #184
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: Claude Code Review | |
| # Dynamic run name shows PR number for easy identification in the Actions UI | |
| # Format: "Claude Code Review (PR #123)" or "Claude Code Review (branch-name)" | |
| run-name: >- | |
| ${{ github.event_name == 'workflow_dispatch' && format('Claude Code Review (PR #{0})', inputs.pr_number) || | |
| github.event_name == 'pull_request' && format('Claude Code Review (PR #{0})', github.event.pull_request.number) || | |
| format('Claude Code Review ({0})', github.event.workflow_run.head_branch) }} | |
| # SECURITY NOTE: This workflow grants Claude Code access to secrets and write permissions. | |
| # Authorization checks are implemented to prevent prompt injection and code execution from untrusted users. | |
| # - Only PRs from repository collaborators, members, and owners trigger Claude reviews | |
| # - The devcontainer is NEVER rebuilt from PR branches to prevent malicious Dockerfile injection | |
| # - External PRs (from forks) are blocked from triggering Claude | |
| on: | |
| workflow_run: | |
| workflows: ["PR Tests"] | |
| types: [completed] | |
| # Reset reviews when PR is reopened (allows re-triggering full review cycle) | |
| pull_request: | |
| types: [reopened] | |
| # Allow manual triggering after workflow_dispatch-triggered PR Tests completes | |
| # (workflow_run events don't fire for workflow_dispatch triggers) | |
| workflow_dispatch: | |
| inputs: | |
| head_ref: | |
| description: 'PR branch name' | |
| required: true | |
| type: string | |
| head_repo: | |
| description: 'PR repository (owner/repo format)' | |
| required: true | |
| type: string | |
| pr_number: | |
| description: 'PR number' | |
| required: true | |
| type: string | |
| tests_passed: | |
| description: 'Whether tests passed (true/false)' | |
| required: true | |
| type: string | |
| run_id: | |
| description: 'PR Tests workflow run ID' | |
| required: true | |
| type: string | |
| # Prevent duplicate reviews when multiple PR Tests complete around the same time | |
| # Uses branch name for concurrency group | |
| # cancel-in-progress: false ensures we complete the first review rather than restarting | |
| concurrency: | |
| group: claude-review-${{ github.event_name == 'workflow_dispatch' && inputs.head_ref || github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.event.workflow_run.head_branch }} | |
| cancel-in-progress: false | |
| env: | |
| # Note: must be lowercase for Docker compatibility | |
| DEVCONTAINER_IMAGE: ghcr.io/nickborgersprobably/hide-my-list-devcontainer | |
| jobs: | |
| # Extract PR and Issue context from workflow_run or workflow_dispatch event | |
| get-context: | |
| runs-on: ubuntu-latest | |
| # Note: We don't filter by event type here. The concurrency group prevents | |
| # duplicate runs for the same branch, and downstream jobs skip if no PR is found. | |
| # This allows reviews to run even when push happens before PR creation. | |
| outputs: | |
| pr_number: ${{ steps.get-pr.outputs.pr_number }} | |
| pr_title: ${{ steps.get-pr.outputs.pr_title }} | |
| pr_body_b64: ${{ steps.get-pr.outputs.pr_body_b64 }} | |
| head_ref: ${{ steps.get-pr.outputs.head_ref }} | |
| head_repo: ${{ steps.get-pr.outputs.head_repo }} | |
| # Draft PR status - used to skip auto-fix pipeline for TDD workflows | |
| is_draft: ${{ steps.get-pr.outputs.is_draft }} | |
| # Handle workflow_run, workflow_dispatch, and pull_request events | |
| # For pull_request (reopen), tests haven't run yet so tests_passed is false | |
| tests_passed: ${{ github.event_name == 'workflow_dispatch' && inputs.tests_passed == 'true' || github.event_name == 'pull_request' && 'false' || github.event.workflow_run.conclusion == 'success' }} | |
| run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event_name == 'pull_request' && github.run_id || github.event.workflow_run.id }} | |
| # SHA that triggered this workflow - used to detect if author pushed newer commits | |
| triggering_sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.event.workflow_run.head_sha }} | |
| issue_number: ${{ steps.get-issue.outputs.issue_number }} | |
| issue_title: ${{ steps.get-issue.outputs.issue_title }} | |
| issue_body_b64: ${{ steps.get-issue.outputs.issue_body_b64 }} | |
| fix_attempts: ${{ steps.count-attempts.outputs.count }} | |
| reviews_already_passed: ${{ steps.check-existing-reviews.outputs.already_passed }} | |
| # SECURITY: Authorization status - blocks external/untrusted PR authors | |
| author_authorized: ${{ steps.check-author-auth.outputs.authorized }} | |
| # File classification - determines which reviews to run | |
| config_only: ${{ steps.classify-files.outputs.config_only }} | |
| has_scripts: ${{ steps.classify-files.outputs.has_scripts }} | |
| docs_only: ${{ steps.classify-files.outputs.docs_only }} | |
| steps: | |
| - name: Get PR from workflow run or dispatch inputs | |
| uses: actions/github-script@v7 | |
| id: get-pr | |
| with: | |
| script: | | |
| // Check if this is a workflow_dispatch event with inputs | |
| const eventName = context.eventName; | |
| let headBranch, headRepo, prNumber; | |
| if (eventName === 'workflow_dispatch') { | |
| // Use inputs from workflow_dispatch | |
| headBranch = '${{ inputs.head_ref }}'; | |
| headRepo = '${{ inputs.head_repo }}'; | |
| prNumber = parseInt('${{ inputs.pr_number }}', 10); | |
| console.log(`Triggered via workflow_dispatch for PR #${prNumber}`); | |
| } else if (eventName === 'pull_request') { | |
| // Use data from pull_request event (triggered on reopen) | |
| const pr = context.payload.pull_request; | |
| headBranch = pr.head.ref; | |
| headRepo = pr.head.repo.full_name; | |
| prNumber = pr.number; | |
| console.log(`Triggered via pull_request event for PR #${prNumber}`); | |
| } else { | |
| // Use data from workflow_run event | |
| headBranch = context.payload.workflow_run.head_branch; | |
| headRepo = context.payload.workflow_run.head_repository.full_name; | |
| console.log(`Looking for PR with head branch: ${headBranch} from ${headRepo}`); | |
| } | |
| // Fetch PR details | |
| let pr; | |
| if (prNumber) { | |
| // Fetch specific PR by number | |
| const prResponse = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| pr = prResponse.data; | |
| } else { | |
| // Search for PR by head branch | |
| const prs = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| head: `${headRepo.split('/')[0]}:${headBranch}` | |
| }); | |
| if (prs.data.length > 0) { | |
| pr = prs.data[0]; | |
| } | |
| } | |
| if (pr) { | |
| console.log(`Found PR #${pr.number}: ${pr.title}`); | |
| core.setOutput('pr_number', pr.number); | |
| core.setOutput('pr_title', pr.title); | |
| // Base64 encode PR body to avoid issues with special characters in env vars | |
| const prBodyB64 = Buffer.from(pr.body || '').toString('base64'); | |
| core.setOutput('pr_body_b64', prBodyB64); | |
| core.setOutput('head_ref', pr.head.ref); | |
| core.setOutput('head_repo', pr.head.repo.full_name); | |
| core.setOutput('is_draft', pr.draft ? 'true' : 'false'); | |
| } else { | |
| console.log('No open PR found for this workflow run'); | |
| core.setOutput('pr_number', ''); | |
| core.setOutput('is_draft', 'false'); | |
| } | |
| - name: Extract linked issue from PR body | |
| if: steps.get-pr.outputs.pr_number != '' | |
| id: get-issue | |
| env: | |
| PR_BODY_B64: ${{ steps.get-pr.outputs.pr_body_b64 }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Decode PR body to search for linked issues | |
| PR_BODY=$(echo "$PR_BODY_B64" | base64 -d) | |
| # Parse PR body for "Resolves #X", "Fixes #X", "Closes #X" patterns | |
| ISSUE_NUM=$(echo "$PR_BODY" | grep -oP '(Resolves|Fixes|Closes|Fix)\s*#\K\d+' | head -1 || echo "") | |
| echo "issue_number=$ISSUE_NUM" >> $GITHUB_OUTPUT | |
| if [ -n "$ISSUE_NUM" ]; then | |
| echo "Found linked issue: #$ISSUE_NUM" | |
| # Fetch issue details | |
| ISSUE_JSON=$(gh api repos/${{ github.repository }}/issues/$ISSUE_NUM 2>/dev/null || echo "{}") | |
| ISSUE_TITLE=$(echo "$ISSUE_JSON" | jq -r '.title // ""') | |
| ISSUE_BODY=$(echo "$ISSUE_JSON" | jq -r '.body // ""') | |
| echo "issue_title=$ISSUE_TITLE" >> $GITHUB_OUTPUT | |
| # Base64 encode issue body to avoid issues with special characters | |
| echo "issue_body_b64=$(echo "$ISSUE_BODY" | base64 -w0)" >> $GITHUB_OUTPUT | |
| else | |
| echo "No linked issue found in PR body" | |
| echo "issue_title=" >> $GITHUB_OUTPUT | |
| echo "issue_body_b64=" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Count fix attempts from PR labels | |
| if: steps.get-pr.outputs.pr_number != '' | |
| id: count-attempts | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_NUMBER="${{ steps.get-pr.outputs.pr_number }}" | |
| # Get all labels on the PR | |
| LABELS=$(gh api repos/${{ github.repository }}/issues/$PR_NUMBER/labels --jq '.[].name' 2>/dev/null || echo "") | |
| # Count claude-fix-attempt-N labels | |
| COUNT=0 | |
| for label in $LABELS; do | |
| if [[ "$label" =~ ^claude-fix-attempt-[0-9]+$ ]]; then | |
| NUM=$(echo "$label" | grep -oP '\d+$') | |
| if [ "$NUM" -gt "$COUNT" ]; then | |
| COUNT=$NUM | |
| fi | |
| fi | |
| done | |
| echo "Current fix attempt count: $COUNT" | |
| echo "count=$COUNT" >> $GITHUB_OUTPUT | |
| - name: Check if reviews already passed (or reset on reopen) | |
| if: steps.get-pr.outputs.pr_number != '' | |
| id: check-existing-reviews | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_NUMBER="${{ steps.get-pr.outputs.pr_number }}" | |
| EVENT_NAME="${{ github.event_name }}" | |
| # If PR was reopened, remove the reviews-passed label to allow re-review | |
| if [ "$EVENT_NAME" = "pull_request" ] && [ "${{ github.event.action }}" = "reopened" ]; then | |
| echo "PR was reopened - removing agent-reviews-passed label to allow re-review" | |
| gh pr edit $PR_NUMBER --remove-label "agent-reviews-passed" --repo ${{ github.repository }} 2>/dev/null || true | |
| echo "already_passed=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # Check if agent-reviews-passed label exists | |
| LABELS=$(gh api repos/${{ github.repository }}/issues/$PR_NUMBER/labels --jq '.[].name' 2>/dev/null || echo "") | |
| if echo "$LABELS" | grep -q "^agent-reviews-passed$"; then | |
| echo "Agent reviews already passed for this PR - skipping review cycle" | |
| echo "already_passed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "No existing reviews found - will run full review cycle" | |
| echo "already_passed=false" >> $GITHUB_OUTPUT | |
| fi | |
| # SECURITY: Check if PR author is authorized to trigger Claude reviews | |
| # This prevents prompt injection and code execution from external/untrusted users | |
| - name: Check PR author authorization | |
| if: steps.get-pr.outputs.pr_number != '' | |
| id: check-author-auth | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_NUMBER="${{ steps.get-pr.outputs.pr_number }}" | |
| # Fetch PR author association | |
| PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER) | |
| AUTHOR_ASSOC=$(echo "$PR_DATA" | jq -r '.author_association') | |
| AUTHOR_LOGIN=$(echo "$PR_DATA" | jq -r '.user.login') | |
| HEAD_REPO=$(echo "$PR_DATA" | jq -r '.head.repo.full_name') | |
| BASE_REPO=$(echo "$PR_DATA" | jq -r '.base.repo.full_name') | |
| echo "PR Author: $AUTHOR_LOGIN" | |
| echo "Author association: $AUTHOR_ASSOC" | |
| echo "Head repo: $HEAD_REPO" | |
| echo "Base repo: $BASE_REPO" | |
| # Check if this is a fork PR (external contribution) | |
| IS_FORK="false" | |
| if [ "$HEAD_REPO" != "$BASE_REPO" ]; then | |
| IS_FORK="true" | |
| echo "This is a PR from a fork: $HEAD_REPO -> $BASE_REPO" | |
| fi | |
| # Allow: OWNER, MEMBER, COLLABORATOR | |
| # Deny: CONTRIBUTOR, FIRST_TIMER, FIRST_TIME_CONTRIBUTOR, MANNEQUIN, NONE | |
| case "$AUTHOR_ASSOC" in | |
| OWNER|MEMBER|COLLABORATOR) | |
| echo "Authorized: $AUTHOR_LOGIN is a $AUTHOR_ASSOC" | |
| echo "authorized=true" >> $GITHUB_OUTPUT | |
| ;; | |
| *) | |
| echo "Not authorized: $AUTHOR_LOGIN is a $AUTHOR_ASSOC" | |
| echo "External users cannot trigger Claude Code Review workflows." | |
| echo "This is a security measure to prevent prompt injection attacks." | |
| echo "authorized=false" >> $GITHUB_OUTPUT | |
| # Post a comment explaining why the review was skipped | |
| gh pr comment $PR_NUMBER --body "## Claude Code Review Skipped | |
| This PR was opened by an external contributor ($AUTHOR_LOGIN with association: $AUTHOR_ASSOC). | |
| For security reasons, Claude Code reviews are only enabled for repository collaborators, members, and owners. This prevents potential prompt injection attacks through PR descriptions or issue content. | |
| A repository maintainer will review this PR manually. | |
| --- | |
| This is an automated security notice." --repo ${{ github.repository }} 2>/dev/null || true | |
| ;; | |
| esac | |
| - name: Classify PR files | |
| if: steps.get-pr.outputs.pr_number != '' | |
| id: classify-files | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_NUMBER="${{ steps.get-pr.outputs.pr_number }}" | |
| FILES=$(gh pr diff $PR_NUMBER --name-only --repo ${{ github.repository }}) | |
| echo "Changed files:" | |
| echo "$FILES" | |
| CONFIG_ONLY="true" | |
| HAS_SCRIPTS="false" | |
| DOCS_ONLY="true" | |
| while IFS= read -r file; do | |
| [ -z "$file" ] && continue | |
| if [[ "$file" == *.sh ]]; then | |
| HAS_SCRIPTS="true" | |
| fi | |
| if [[ "$file" == scripts/* ]] || [[ "$file" == docs/* ]] || [[ "$file" == design/* ]] || [[ "$file" == *.md ]]; then | |
| CONFIG_ONLY="false" | |
| fi | |
| # docs_only: true when all changed files are *.md, docs/*, or design/* | |
| if [[ "$file" != *.md ]] && [[ "$file" != docs/* ]] && [[ "$file" != design/* ]]; then | |
| DOCS_ONLY="false" | |
| fi | |
| done <<< "$FILES" | |
| echo "Classification: config_only=$CONFIG_ONLY has_scripts=$HAS_SCRIPTS docs_only=$DOCS_ONLY" | |
| echo "config_only=$CONFIG_ONLY" >> $GITHUB_OUTPUT | |
| echo "has_scripts=$HAS_SCRIPTS" >> $GITHUB_OUTPUT | |
| echo "docs_only=$DOCS_ONLY" >> $GITHUB_OUTPUT | |
| # Build and cache devcontainer image | |
| # SECURITY: Always build from main branch, NEVER from PR branches | |
| # This prevents malicious Dockerfile modifications from being used to build the container | |
| build-devcontainer: | |
| runs-on: [self-hosted, homelab] | |
| needs: get-context | |
| if: | | |
| needs.get-context.outputs.pr_number != '' && | |
| needs.get-context.outputs.is_draft != 'true' && | |
| needs.get-context.outputs.reviews_already_passed != 'true' && | |
| needs.get-context.outputs.author_authorized == 'true' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| # SECURITY: Checkout main branch, NOT the PR branch | |
| # This ensures we never build a devcontainer from potentially malicious PR code | |
| - name: Checkout main branch (security measure) | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| - name: Create directories for devcontainer mounts | |
| run: mkdir -p ~/.config/gh ~/.claude ~/.codex | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push devcontainer | |
| uses: devcontainers/ci@v0.3 | |
| # continue-on-error: Post-action cleanup may fail on GHCR push | |
| # even when the image builds successfully. Downstream review jobs | |
| # will fail independently if the image is actually unavailable. | |
| continue-on-error: true | |
| with: | |
| imageName: ${{ env.DEVCONTAINER_IMAGE }} | |
| cacheFrom: ${{ env.DEVCONTAINER_IMAGE }} | |
| push: always | |
| # Fix test failures - runs in a retry loop until tests pass (max 3 attempts) | |
| fix-test-failures: | |
| needs: [get-context, build-devcontainer] | |
| if: | | |
| needs.get-context.outputs.pr_number != '' && | |
| needs.get-context.outputs.is_draft != 'true' && | |
| needs.get-context.outputs.reviews_already_passed != 'true' && | |
| needs.get-context.outputs.author_authorized == 'true' && | |
| needs.get-context.outputs.tests_passed == 'false' && | |
| needs.get-context.outputs.fix_attempts < 3 | |
| runs-on: [self-hosted, homelab] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| packages: read | |
| actions: write # Required to trigger PR Tests workflow via workflow_dispatch | |
| statuses: write # Required to create commit status after tests pass | |
| steps: | |
| - name: Checkout PR branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.get-context.outputs.head_ref }} | |
| repository: ${{ needs.get-context.outputs.head_repo }} | |
| fetch-depth: 0 | |
| token: ${{ secrets.WORKFLOW_PAT }} # PAT required to push workflow file changes | |
| - name: Increment fix attempt counter | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_NUMBER="${{ needs.get-context.outputs.pr_number }}" | |
| CURRENT_ATTEMPTS="${{ needs.get-context.outputs.fix_attempts }}" | |
| NEXT_ATTEMPT=$((CURRENT_ATTEMPTS + 1)) | |
| # Create the label if it doesn't exist | |
| gh label create "claude-fix-attempt-$NEXT_ATTEMPT" \ | |
| --color "ff9500" \ | |
| --description "Claude fix attempt $NEXT_ATTEMPT" \ | |
| --repo ${{ github.repository }} 2>/dev/null || true | |
| # Add the label to the PR | |
| gh pr edit $PR_NUMBER --add-label "claude-fix-attempt-$NEXT_ATTEMPT" --repo ${{ github.repository }} | |
| echo "Marked as fix attempt $NEXT_ATTEMPT" | |
| - name: Create directories for devcontainer mounts | |
| run: mkdir -p ~/.config/gh ~/.claude ~/.codex | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Run Claude to fix test failures | |
| id: claude-fix | |
| uses: devcontainers/ci@v0.3 | |
| with: | |
| imageName: ${{ env.DEVCONTAINER_IMAGE }} | |
| cacheFrom: ${{ env.DEVCONTAINER_IMAGE }} | |
| push: never | |
| env: | | |
| CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| GH_TOKEN=${{ secrets.WORKFLOW_PAT }} | |
| PR_NUMBER=${{ needs.get-context.outputs.pr_number }} | |
| PR_TITLE=${{ needs.get-context.outputs.pr_title }} | |
| PR_BODY_B64=${{ needs.get-context.outputs.pr_body_b64 }} | |
| ISSUE_NUMBER=${{ needs.get-context.outputs.issue_number }} | |
| ISSUE_TITLE=${{ needs.get-context.outputs.issue_title }} | |
| ISSUE_BODY_B64=${{ needs.get-context.outputs.issue_body_b64 }} | |
| RUN_ID=${{ needs.get-context.outputs.run_id }} | |
| ATTEMPT_NUM=${{ needs.get-context.outputs.fix_attempts }} | |
| REPO=${{ github.repository }} | |
| TRIGGERING_SHA=${{ needs.get-context.outputs.triggering_sha }} | |
| HEAD_REF=${{ needs.get-context.outputs.head_ref }} | |
| runCmd: | | |
| # Calculate actual attempt number (current + 1) | |
| ATTEMPT=$((ATTEMPT_NUM + 1)) | |
| # Decode base64-encoded bodies (handles special characters safely) | |
| PR_BODY=$(echo "$PR_BODY_B64" | base64 -d 2>/dev/null || echo "") | |
| ISSUE_BODY=$(echo "$ISSUE_BODY_B64" | base64 -d 2>/dev/null || echo "") | |
| # Run Claude Code to fix test failures | |
| # Using Sonnet: Task is debugging with clear error messages and 3 retry attempts | |
| claude --print \ | |
| --verbose \ | |
| --output-format stream-json \ | |
| --model sonnet \ | |
| --dangerously-skip-permissions \ | |
| --max-turns 200 \ | |
| "You are fixing CI test failures for PR #${PR_NUMBER} in ${REPO}. | |
| This is fix attempt ${ATTEMPT} of 3. | |
| CRITICAL: BEFORE PUSHING ANY CHANGES, you MUST check if the PR author has pushed | |
| newer commits since this workflow started. Run: | |
| git fetch origin ${HEAD_REF} | |
| CURRENT_HEAD=\$(git rev-parse origin/${HEAD_REF}) | |
| if [ \"\$CURRENT_HEAD\" != \"${TRIGGERING_SHA}\" ]; then | |
| echo 'Author has pushed newer commits - attempting to merge' | |
| # Try to rebase our changes on top of the author's commits | |
| git stash 2>/dev/null || true | |
| if git pull --rebase origin ${HEAD_REF}; then | |
| git stash pop 2>/dev/null || true | |
| echo 'Successfully rebased on top of author commits - continuing with fix' | |
| else | |
| # Rebase failed - abort and let author handle it | |
| git rebase --abort 2>/dev/null || true | |
| git stash pop 2>/dev/null || true | |
| gh pr comment ${PR_NUMBER} --body '## Fix Attempt ${ATTEMPT}/3 - Aborted | |
| Detected newer commits pushed by author. Attempted to merge but encountered conflicts. | |
| Skipping automated fix to avoid overwriting author changes. | |
| The author is likely fixing this themselves.' | |
| exit 0 | |
| fi | |
| fi | |
| Proceed with fixing: | |
| ORIGINAL ISSUE (if applicable): | |
| Issue #${ISSUE_NUMBER}: ${ISSUE_TITLE} | |
| ${ISSUE_BODY} | |
| PR DESCRIPTION: | |
| ${PR_TITLE} | |
| ${PR_BODY} | |
| The tests failed in the PR Tests workflow (run ID: ${RUN_ID}). Your task: | |
| 1. Check the workflow run logs: gh run view ${RUN_ID} --log-failed | |
| 2. Identify the root cause of failures (failing tests, lint warnings, validation issues, etc.) | |
| 3. Fix the issues in the code | |
| 4. Run \`shellcheck scripts/*.sh\` and \`yamllint .github/workflows/*.yml\` to verify your fix works | |
| 5. Commit and push your fixes | |
| IMPORTANT RULES FOR THIS REPOSITORY: | |
| - This is an OpenClaw agent project, not a compiled application | |
| - Use \`shellcheck scripts/*.sh\` for shell script linting | |
| - Use \`yamllint .github/workflows/*.yml\` for workflow validation | |
| - Check documentation links are not broken | |
| - Never use \`git push --no-verify\` | |
| - If this is attempt 2+, review what was tried before and try a different approach | |
| After pushing, post a brief status update: | |
| gh pr comment ${PR_NUMBER} --body '## Fix Attempt ${ATTEMPT}/3 | |
| [Describe what you found and fixed] | |
| Pushed fix - triggering new test run.'" < /dev/null 2>&1 | tee /tmp/fix-failures-output.jsonl | |
| - name: Trigger PR Tests and reviews after fix | |
| if: success() | |
| env: | |
| GH_TOKEN: ${{ secrets.WORKFLOW_PAT }} | |
| FIX_ATTEMPTS: ${{ needs.get-context.outputs.fix_attempts }} | |
| run: | | |
| # Check if new commits were pushed by comparing current HEAD to triggering SHA | |
| git fetch origin ${{ needs.get-context.outputs.head_ref }} | |
| CURRENT_HEAD=$(git rev-parse origin/${{ needs.get-context.outputs.head_ref }}) | |
| TRIGGERING_SHA="${{ needs.get-context.outputs.triggering_sha }}" | |
| if [ "$CURRENT_HEAD" != "$TRIGGERING_SHA" ]; then | |
| echo "Fix was pushed (HEAD changed from $TRIGGERING_SHA to $CURRENT_HEAD)" | |
| echo "Triggering PR Tests workflow..." | |
| # Trigger PR Tests and capture the run | |
| gh workflow run "PR Tests" --ref "${{ needs.get-context.outputs.head_ref }}" --repo ${{ github.repository }} | |
| echo "PR Tests workflow triggered, waiting for it to start..." | |
| sleep 10 | |
| # Find the run we just triggered | |
| RUN_ID=$(gh run list --workflow="pr-tests.yml" --branch="${{ needs.get-context.outputs.head_ref }}" --limit=1 --json databaseId --jq '.[0].databaseId') | |
| echo "Found PR Tests run: $RUN_ID" | |
| # Poll for completion (max 15 minutes) | |
| MAX_WAIT=900 | |
| ELAPSED=0 | |
| POLL_INTERVAL=30 | |
| while [ $ELAPSED -lt $MAX_WAIT ]; do | |
| STATUS=$(gh run view $RUN_ID --json status,conclusion --jq '.status') | |
| if [ "$STATUS" = "completed" ]; then | |
| CONCLUSION=$(gh run view $RUN_ID --json conclusion --jq '.conclusion') | |
| echo "PR Tests completed with conclusion: $CONCLUSION" | |
| break | |
| fi | |
| echo "PR Tests still running... (waited ${ELAPSED}s)" | |
| sleep $POLL_INTERVAL | |
| ELAPSED=$((ELAPSED + POLL_INTERVAL)) | |
| done | |
| if [ $ELAPSED -ge $MAX_WAIT ]; then | |
| echo "Timed out waiting for PR Tests - reviews will not run automatically" | |
| exit 0 | |
| fi | |
| # If tests passed, create a commit status and trigger reviews | |
| # (workflow_run events don't fire for workflow_dispatch-triggered runs) | |
| if [ "$CONCLUSION" = "success" ]; then | |
| echo "Tests passed!" | |
| # Create a commit status for the PR's head SHA so the PR check shows as passed | |
| # This is needed because workflow_dispatch runs don't automatically create PR status checks | |
| echo "Creating commit status for $CURRENT_HEAD..." | |
| gh api repos/${{ github.repository }}/statuses/$CURRENT_HEAD \ | |
| -f state="success" \ | |
| -f target_url="https://github.com/${{ github.repository }}/actions/runs/$RUN_ID" \ | |
| -f description="All required tests passed (via automated fix)" \ | |
| -f context="All Required Tests" | |
| echo "Commit status created successfully" | |
| echo "Triggering Claude Code Review..." | |
| gh workflow run "Claude Code Review" \ | |
| --ref "main" \ | |
| --repo ${{ github.repository }} \ | |
| -f head_ref="${{ needs.get-context.outputs.head_ref }}" \ | |
| -f head_repo="${{ needs.get-context.outputs.head_repo }}" \ | |
| -f pr_number="${{ needs.get-context.outputs.pr_number }}" \ | |
| -f tests_passed="true" \ | |
| -f run_id="$RUN_ID" | |
| echo "Claude Code Review workflow triggered successfully" | |
| else | |
| echo "Tests still failing after fix attempt" | |
| # Trigger another Claude Code Review run to retry the fix | |
| # This is necessary because workflow_run events don't fire for | |
| # workflow_dispatch-triggered runs, so we must manually trigger | |
| NEXT_ATTEMPT=$((FIX_ATTEMPTS + 1)) | |
| if [ "$NEXT_ATTEMPT" -lt 3 ]; then | |
| echo "Triggering fix attempt $((NEXT_ATTEMPT + 1))/3..." | |
| gh workflow run "Claude Code Review" \ | |
| --ref "main" \ | |
| --repo ${{ github.repository }} \ | |
| -f head_ref="${{ needs.get-context.outputs.head_ref }}" \ | |
| -f head_repo="${{ needs.get-context.outputs.head_repo }}" \ | |
| -f pr_number="${{ needs.get-context.outputs.pr_number }}" \ | |
| -f tests_passed="false" \ | |
| -f run_id="$RUN_ID" | |
| echo "Next fix attempt triggered" | |
| else | |
| echo "Max fix attempts (3) reached - human intervention required" | |
| COMMENT_BODY="## Automated Fix Failed" | |
| COMMENT_BODY="$COMMENT_BODY"$'\n\n'"All 3 automated fix attempts were unsuccessful. Human intervention is required." | |
| COMMENT_BODY="$COMMENT_BODY"$'\n\n'"**Recent Test Run:** https://github.com/${{ github.repository }}/actions/runs/$RUN_ID" | |
| COMMENT_BODY="$COMMENT_BODY"$'\n\n'"Generated by Claude Code Review workflow" | |
| gh pr comment ${{ needs.get-context.outputs.pr_number }} --body "$COMMENT_BODY" | |
| fi | |
| fi | |
| else | |
| echo "No new commits - Claude may have aborted or found no fix needed" | |
| fi | |
| - name: Upload fix attempt output | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| continue-on-error: true | |
| with: | |
| name: fix-failures-output-attempt-${{ needs.get-context.outputs.fix_attempts }} | |
| path: /tmp/fix-failures-output.jsonl | |
| retention-days: 7 | |
| # Design review specialist - validates PR implements issue intent and reviews design quality | |
| # Runs FIRST (before code review) because intent and design must be validated before code quality | |
| design-review: | |
| needs: [get-context, build-devcontainer] | |
| if: | | |
| needs.get-context.outputs.pr_number != '' && | |
| needs.get-context.outputs.is_draft != 'true' && | |
| needs.get-context.outputs.reviews_already_passed != 'true' && | |
| needs.get-context.outputs.author_authorized == 'true' && | |
| needs.get-context.outputs.tests_passed == 'true' | |
| runs-on: [self-hosted, homelab] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| packages: read | |
| steps: | |
| - name: Checkout for composite action | |
| uses: actions/checkout@v4 | |
| - name: Reviewer setup | |
| id: setup | |
| uses: ./.github/actions/reviewer-setup | |
| with: | |
| pr_number: ${{ needs.get-context.outputs.pr_number }} | |
| head_ref: ${{ needs.get-context.outputs.head_ref }} | |
| head_repo: ${{ needs.get-context.outputs.head_repo }} | |
| github_token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| - name: Design review in devcontainer | |
| if: steps.setup.outputs.verified == 'true' | |
| uses: devcontainers/ci@v0.3 | |
| with: | |
| imageName: ${{ env.DEVCONTAINER_IMAGE }} | |
| cacheFrom: ${{ env.DEVCONTAINER_IMAGE }} | |
| push: never | |
| env: | | |
| CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| GH_TOKEN=${{ secrets.WORKFLOW_PAT }} | |
| PR_NUMBER=${{ needs.get-context.outputs.pr_number }} | |
| PR_TITLE=${{ needs.get-context.outputs.pr_title }} | |
| PR_BODY_B64=${{ needs.get-context.outputs.pr_body_b64 }} | |
| ISSUE_NUMBER=${{ needs.get-context.outputs.issue_number }} | |
| ISSUE_TITLE=${{ needs.get-context.outputs.issue_title }} | |
| ISSUE_BODY_B64=${{ needs.get-context.outputs.issue_body_b64 }} | |
| HEAD_REF=${{ needs.get-context.outputs.head_ref }} | |
| REPO=${{ github.repository }} | |
| runCmd: | | |
| # Decode base64-encoded bodies (handles special characters safely) | |
| PR_BODY=$(echo "$PR_BODY_B64" | base64 -d 2>/dev/null || echo "") | |
| ISSUE_BODY=$(echo "$ISSUE_BODY_B64" | base64 -d 2>/dev/null || echo "") | |
| # Fetch all comments on the linked issue (may contain additional context/requirements) | |
| ISSUE_COMMENTS="" | |
| if [ -n "$ISSUE_NUMBER" ]; then | |
| ISSUE_COMMENTS=$(gh api "repos/${REPO}/issues/${ISSUE_NUMBER}/comments" --jq '.[] | "---\n**\(.user.login)** commented at \(.created_at):\n\(.body)\n"' 2>/dev/null | head -c 50000 || echo "") | |
| fi | |
| claude --print \ | |
| --verbose \ | |
| --output-format stream-json \ | |
| --model opus \ | |
| --dangerously-skip-permissions \ | |
| --max-turns 450 \ | |
| "You are a DESIGN REVIEW specialist for PR #${PR_NUMBER} in ${REPO}. | |
| Review like an experienced staff engineer. Be direct and selective. | |
| Don't praise the design or list strengths — focus on what you'd actually flag in a review. | |
| Non-blocking observations are welcome but keep each to one sentence. | |
| If the design is sound, approve in one line and move on. | |
| **CONTEXT:** | |
| PR Title: ${PR_TITLE} | |
| PR Description: | |
| ${PR_BODY} | |
| Linked Issue #${ISSUE_NUMBER}: ${ISSUE_TITLE} | |
| Issue Description: | |
| ${ISSUE_BODY} | |
| Issue Comments (additional context/requirements): | |
| ${ISSUE_COMMENTS} | |
| **YOUR TASK:** | |
| 1. Read the issue/PR description to understand the problem being solved. | |
| 2. Examine the code changes (\`git diff origin/main...HEAD\`). | |
| 3. Validate: does the PR solve the stated problem? Gaps between intent and delivery are blocking. | |
| 4. SCOPE CHECK: Compare the PR title/issue against the actual diff. | |
| - If the title suggests a narrow fix but the diff introduces new abstractions or | |
| significant code beyond what the title implies, flag as blocking scope creep. | |
| - State explicitly: \"Scope check: PASS\" or \"Scope check: FAIL — [reason]\" | |
| 5. Evaluate design decisions: could a simpler approach work? Flag over-engineering as blocking. | |
| Reference docs/architecture.md for system design context. | |
| Do NOT push any changes. This is a read-only review. | |
| **INLINE COMMENTS:** Where a concern is tied to a specific line of code, also leave | |
| an inline PR review comment on that line. Use inline comments for localized feedback | |
| and the summary comment for overall assessment. | |
| Post exactly ONE summary comment using: | |
| gh pr comment ${PR_NUMBER} --body '## Design Review | |
| ### Blocking Issues | |
| [List blocking concerns. Skip section if none.] | |
| ### Worth Considering | |
| [One-sentence non-blocking observations. Skip section if none.] | |
| ### Conclusion | |
| [Approved | Approved with reservations | Needs revision — [reason]]'" < /dev/null 2>&1 | tee /tmp/design-review-output.jsonl | |
| - name: Upload design review output | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| continue-on-error: true | |
| with: | |
| name: design-review-output | |
| path: /tmp/design-review-output.jsonl | |
| retention-days: 7 | |
| # Security and infrastructure specialist - runs in parallel with design and psych | |
| security-review: | |
| needs: [get-context, build-devcontainer] | |
| if: | | |
| needs.get-context.outputs.pr_number != '' && | |
| needs.get-context.outputs.is_draft != 'true' && | |
| needs.get-context.outputs.reviews_already_passed != 'true' && | |
| needs.get-context.outputs.author_authorized == 'true' && | |
| needs.get-context.outputs.docs_only != 'true' && | |
| needs.get-context.outputs.tests_passed == 'true' | |
| runs-on: [self-hosted, homelab] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| packages: read | |
| steps: | |
| - name: Checkout for composite action | |
| uses: actions/checkout@v4 | |
| - name: Reviewer setup | |
| id: setup | |
| uses: ./.github/actions/reviewer-setup | |
| with: | |
| pr_number: ${{ needs.get-context.outputs.pr_number }} | |
| head_ref: ${{ needs.get-context.outputs.head_ref }} | |
| head_repo: ${{ needs.get-context.outputs.head_repo }} | |
| github_token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| - name: Security review in devcontainer | |
| if: steps.setup.outputs.verified == 'true' | |
| uses: devcontainers/ci@v0.3 | |
| with: | |
| imageName: ${{ env.DEVCONTAINER_IMAGE }} | |
| cacheFrom: ${{ env.DEVCONTAINER_IMAGE }} | |
| push: never | |
| env: | | |
| CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| GH_TOKEN=${{ secrets.WORKFLOW_PAT }} | |
| PR_NUMBER=${{ needs.get-context.outputs.pr_number }} | |
| HEAD_REF=${{ needs.get-context.outputs.head_ref }} | |
| runCmd: | | |
| claude --print \ | |
| --verbose \ | |
| --output-format stream-json \ | |
| --model opus \ | |
| --dangerously-skip-permissions \ | |
| --max-turns 240 \ | |
| "You are a SECURITY and INFRASTRUCTURE specialist reviewing PR #${PR_NUMBER}. | |
| **CRITICAL: BE BRIEF. Only report issues that require action from the PR author.** | |
| Do NOT list files reviewed. Do NOT explain general security concepts. | |
| If there are no actionable issues, post a short approval and move on. | |
| Run \`git diff origin/main...HEAD\` to see changes. | |
| Read SECURITY.md for the project's security model and threat assumptions. | |
| Check for security and infrastructure issues: | |
| - Credential/secret handling in scripts (no hardcoded tokens, proper .env usage) | |
| - Input validation and injection risks (especially in webhook/API handlers) | |
| - Shell script safety (proper quoting, error handling, set -euo pipefail) | |
| - Workflow permissions (principle of least privilege) | |
| - Network security (proxy bypass attempts, unexpected outbound connections) | |
| - Notion API token handling (never logged, never committed) | |
| - Resource exhaustion risks (unbounded loops, missing timeouts) | |
| - Prompt injection vectors in CI/CD pipelines | |
| Run \`shellcheck scripts/*.sh\` to verify. | |
| Do NOT push any changes. This is a read-only review. | |
| For HIGH SEVERITY bugs, describe the fix precisely (file path, line number, exact change needed) so the merge-decision agent can apply it. | |
| **INLINE COMMENTS:** Where a finding is tied to a specific line of code, also leave | |
| an inline PR review comment on that line. Use inline comments for localized findings | |
| and the summary comment for overall assessment. | |
| Post exactly ONE summary comment using: | |
| gh pr comment ${PR_NUMBER} --body '## Security & Infrastructure Review | |
| [If no issues: \"Approved - No security or infrastructure issues\" and stop here] | |
| ### Issues Found | |
| [Only list security risks, credential leaks, injection vectors, etc. Reference file:line. Skip if none.] | |
| ### Fixes Needed | |
| [Describe fixes precisely with file paths, line numbers, and exact changes. Skip section if none.] | |
| ### Conclusion | |
| [Approved | Needs changes | Blocking issues]'" < /dev/null 2>&1 | tee /tmp/security-review-output.jsonl | |
| - name: Upload security review output | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| continue-on-error: true | |
| with: | |
| name: security-review-output | |
| path: /tmp/security-review-output.jsonl | |
| retention-days: 7 | |
| # Psychological research evidence reviewer - validates features against ADHD research literature | |
| psych-review: | |
| needs: [get-context, build-devcontainer] | |
| if: | | |
| needs.get-context.outputs.pr_number != '' && | |
| needs.get-context.outputs.is_draft != 'true' && | |
| needs.get-context.outputs.reviews_already_passed != 'true' && | |
| needs.get-context.outputs.author_authorized == 'true' && | |
| needs.get-context.outputs.config_only != 'true' && | |
| needs.get-context.outputs.tests_passed == 'true' | |
| runs-on: [self-hosted, homelab] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| packages: read | |
| steps: | |
| - name: Checkout for composite action | |
| uses: actions/checkout@v4 | |
| - name: Reviewer setup | |
| id: setup | |
| uses: ./.github/actions/reviewer-setup | |
| with: | |
| pr_number: ${{ needs.get-context.outputs.pr_number }} | |
| head_ref: ${{ needs.get-context.outputs.head_ref }} | |
| head_repo: ${{ needs.get-context.outputs.head_repo }} | |
| github_token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| - name: Psych research review in devcontainer | |
| if: steps.setup.outputs.verified == 'true' | |
| uses: devcontainers/ci@v0.3 | |
| with: | |
| imageName: ${{ env.DEVCONTAINER_IMAGE }} | |
| cacheFrom: ${{ env.DEVCONTAINER_IMAGE }} | |
| push: never | |
| env: | | |
| CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| GH_TOKEN=${{ secrets.WORKFLOW_PAT }} | |
| PR_NUMBER=${{ needs.get-context.outputs.pr_number }} | |
| PR_TITLE=${{ needs.get-context.outputs.pr_title }} | |
| PR_BODY_B64=${{ needs.get-context.outputs.pr_body_b64 }} | |
| ISSUE_NUMBER=${{ needs.get-context.outputs.issue_number }} | |
| ISSUE_TITLE=${{ needs.get-context.outputs.issue_title }} | |
| ISSUE_BODY_B64=${{ needs.get-context.outputs.issue_body_b64 }} | |
| HEAD_REF=${{ needs.get-context.outputs.head_ref }} | |
| REPO=${{ github.repository }} | |
| runCmd: | | |
| # Decode base64-encoded bodies (handles special characters safely) | |
| PR_BODY=$(echo "$PR_BODY_B64" | base64 -d 2>/dev/null || echo "") | |
| ISSUE_BODY=$(echo "$ISSUE_BODY_B64" | base64 -d 2>/dev/null || echo "") | |
| claude --print \ | |
| --verbose \ | |
| --output-format stream-json \ | |
| --model opus \ | |
| --dangerously-skip-permissions \ | |
| --max-turns 450 \ | |
| "You are a PSYCHOLOGICAL RESEARCH EVIDENCE reviewer for PR #${PR_NUMBER} in ${REPO}. | |
| Your role is to evaluate whether this PR's changes are grounded in evidence-based | |
| understanding of ADHD, executive function, motivation, and cognitive load. This project | |
| is an AI-powered task manager designed to help people with ADHD live successful lives. | |
| Do NOT modify any code or push any changes. This is a read-only review. | |
| **CRITICAL: Only report issues that require action from the PR author.** | |
| Do NOT provide general ADHD education. Do NOT praise features that are fine. | |
| If there are no actionable issues, post a short approval and move on. | |
| **QUICK CHECK:** If this PR is purely infrastructure, CI/CD, devcontainer, or workflow | |
| configuration with no user-facing behavioral changes, post: | |
| 'Approved - No user-facing behavioral changes to evaluate against ADHD research.' | |
| and stop. | |
| **CONTEXT:** | |
| PR Title: ${PR_TITLE} | |
| PR Description: | |
| ${PR_BODY} | |
| Linked Issue #${ISSUE_NUMBER}: ${ISSUE_TITLE} | |
| Issue Description: | |
| ${ISSUE_BODY} | |
| **YOUR TASK:** | |
| 1. Read the code changes: \`git diff origin/main...HEAD\` | |
| 2. Read project documentation for context: | |
| - docs/architecture.md (system design) | |
| - docs/user-interactions.md (how users interact with the system) | |
| - docs/task-lifecycle.md (task states and transitions) | |
| - docs/user-preferences.md (user configuration options) | |
| - docs/ai-prompts.md (AI prompt design) | |
| - docs/reward-system.md (motivation and reward mechanics) | |
| 3. Evaluate the changes against ADHD research domains below | |
| **EVALUATION CRITERIA (only assess domains relevant to this PR):** | |
| 1. **Executive Function Support** (Barkley model of EF deficits) | |
| - Does this help with working memory limitations? | |
| - Does it reduce demands on self-regulation? | |
| - Does it support task initiation and follow-through? | |
| - Does it externalize information that ADHD users struggle to hold internally? | |
| 2. **Emotional Regulation** (Hallowell-Ratey framework) | |
| - Does this account for rejection sensitive dysphoria? | |
| - Are failure states handled compassionately (no shame-inducing language)? | |
| - Does it support emotional momentum rather than punishing inconsistency? | |
| 3. **Time Perception** (time blindness research) | |
| - Does this help users who struggle with time estimation? | |
| - Are deadlines and durations presented in ADHD-friendly ways? | |
| - Does it avoid relying on users' internal sense of time passing? | |
| 4. **Motivation & Reward Systems** (variable ratio reinforcement, dopamine regulation) | |
| - Does the reward/feedback mechanism align with ADHD motivation patterns? | |
| - Does it leverage novelty, urgency, interest, or challenge (ADHD motivation drivers)? | |
| - Does it avoid requiring sustained motivation for low-interest tasks? | |
| 5. **Cognitive Load Management** (working memory and attention research) | |
| - Does this minimize the number of decisions required at any moment? | |
| - Are information displays designed to reduce overwhelm? | |
| - Does it chunk information appropriately? | |
| 6. **Sensory & Environmental Considerations** | |
| - Are notifications/alerts designed for ADHD attention patterns? | |
| - Does it avoid designs that enable hyperfocus traps? | |
| **SEVERITY GUIDELINES:** | |
| - **Blocking**: Only for changes that could actively harm ADHD users (e.g., shame-based | |
| language, punishment for missed tasks, designs that increase cognitive overwhelm) | |
| - **Non-blocking suggestion**: Improvements that would better align with research but | |
| aren't harmful as-is | |
| - **Note**: Interesting research connections worth considering in future iterations | |
| When citing concerns, reference the specific research framework (e.g., 'Per Barkley\\'s | |
| model of EF deficits, this design places too much demand on working memory because...') | |
| **INLINE COMMENTS:** Where a concern is tied to a specific line of code or text, also | |
| leave an inline PR review comment on that line. Use inline comments for localized | |
| feedback and the summary comment for overall assessment. | |
| Post exactly ONE summary comment using: | |
| gh pr comment ${PR_NUMBER} --body '## Psychological Research Evidence Review | |
| [If no user-facing changes: \"Approved - No user-facing behavioral changes to evaluate against ADHD research.\" and stop] | |
| [If no issues: \"Approved - Changes align with ADHD research best practices.\" and stop] | |
| ### Research Evaluation | |
| [Only list actionable concerns, organized by research domain. Cite specific frameworks. Skip domains with no issues.] | |
| ### Conclusion | |
| [Approved | Approved with suggestions | Blocking concerns found]'" < /dev/null 2>&1 | tee /tmp/psych-review-output.jsonl | |
| - name: Upload psych review output | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| continue-on-error: true | |
| with: | |
| name: psych-review-output | |
| path: /tmp/psych-review-output.jsonl | |
| retention-days: 7 | |
| # Documentation consistency reviewer - ensures docs remain coherent and non-contradictory | |
| docs-review: | |
| needs: [get-context, build-devcontainer] | |
| if: | | |
| needs.get-context.outputs.pr_number != '' && | |
| needs.get-context.outputs.is_draft != 'true' && | |
| needs.get-context.outputs.reviews_already_passed != 'true' && | |
| needs.get-context.outputs.author_authorized == 'true' && | |
| needs.get-context.outputs.tests_passed == 'true' | |
| runs-on: [self-hosted, homelab] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| packages: read | |
| steps: | |
| - name: Checkout for composite action | |
| uses: actions/checkout@v4 | |
| - name: Reviewer setup | |
| id: setup | |
| uses: ./.github/actions/reviewer-setup | |
| with: | |
| pr_number: ${{ needs.get-context.outputs.pr_number }} | |
| head_ref: ${{ needs.get-context.outputs.head_ref }} | |
| head_repo: ${{ needs.get-context.outputs.head_repo }} | |
| github_token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| - name: Docs review in devcontainer | |
| if: steps.setup.outputs.verified == 'true' | |
| uses: devcontainers/ci@v0.3 | |
| with: | |
| imageName: ${{ env.DEVCONTAINER_IMAGE }} | |
| cacheFrom: ${{ env.DEVCONTAINER_IMAGE }} | |
| push: never | |
| env: | | |
| CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| GH_TOKEN=${{ secrets.WORKFLOW_PAT }} | |
| PR_NUMBER=${{ needs.get-context.outputs.pr_number }} | |
| PR_TITLE=${{ needs.get-context.outputs.pr_title }} | |
| HEAD_REF=${{ needs.get-context.outputs.head_ref }} | |
| REPO=${{ github.repository }} | |
| runCmd: | | |
| claude --print \ | |
| --verbose \ | |
| --output-format stream-json \ | |
| --model opus \ | |
| --dangerously-skip-permissions \ | |
| --max-turns 200 \ | |
| "You are a DOCUMENTATION CONSISTENCY reviewer for PR #${PR_NUMBER} in ${REPO}. | |
| Your job is to ensure all docs in this project remain coherent, consistent, and | |
| free of contradictions after this PR's changes. Don't list docs that don't need | |
| changes. If no docs need updating, approve in one line and move on. | |
| Do NOT push any changes. This is a read-only review. Describe needed fixes precisely | |
| (file path, what to change, and why) so the merge-decision agent can apply them. | |
| **YOUR TASK:** | |
| 1. Read the code changes: \`git diff origin/main...HEAD\` | |
| 2. Read ALL docs in docs/ and design/ to understand the current spec | |
| 3. Check for: | |
| - **Contradictions**: Does any doc now contradict what the code actually does? | |
| - **Stale references**: Do docs reference behaviors, flows, or components that | |
| this PR changed or removed? | |
| - **Missing coverage**: Does this PR introduce new behavior not reflected in any doc? | |
| - **Cross-doc consistency**: Do docs that reference each other still agree? | |
| (e.g., task-lifecycle.md states should match architecture.md data flow) | |
| - **AGENTS.md / CLAUDE.md**: Do project instructions still accurately describe | |
| the system after this PR? | |
| - **Terminology consistency**: Are terms, field names, and enum values | |
| (status, work_type, energy_required) used consistently across docs? | |
| - **Numeric constant consistency**: Do shared thresholds (urgency ranges, | |
| step counts, timer values, score ranges) agree across all docs? | |
| - **docs/index.md completeness**: Does it list all docs accurately? | |
| Key doc relationships to check: | |
| - docs/architecture.md <-> docs/task-lifecycle.md (system design vs task states) | |
| - docs/ai-prompts.md <-> docs/user-interactions.md (prompt behavior vs interaction patterns) | |
| - docs/user-preferences.md <-> docs/reward-system.md (personalization vs rewards) | |
| - docs/notion-schema.md <-> docs/task-lifecycle.md (field definitions vs lifecycle states) | |
| - docs/notion-schema.md <-> docs/user-preferences.md (preferences schema vs behavior) | |
| - docs/task-lifecycle.md <-> docs/user-interactions.md (state machine vs conversation flows) | |
| - docs/reward-system.md <-> docs/task-lifecycle.md (reward triggers vs lifecycle phases) | |
| - design/adhd-priorities.md <-> docs/ai-prompts.md (ADHD constraints vs prompt implementation) | |
| - AGENTS.md <-> docs/ (project instructions vs spec files) | |
| Source of truth: When field names, status values, or database structure | |
| conflict between docs, treat docs/notion-schema.md as canonical. Other | |
| docs that reference these values must agree with it. | |
| Pay special attention to docs/ai-prompts.md — it is the core system | |
| specification. Any PR that changes agent behavior must be checked against | |
| the prompt architecture defined there. | |
| Blocking vs non-blocking: A contradiction between docs that would cause | |
| the agent to receive conflicting instructions is blocking. A missing | |
| update to an example or diagram is non-blocking. | |
| **INLINE COMMENTS:** Where a doc issue is tied to a specific line, also leave | |
| an inline PR review comment on that line. | |
| Post exactly ONE summary comment using: | |
| gh pr comment ${PR_NUMBER} --body '## Documentation Consistency Review | |
| [If no docs need updating: \"Approved - All documentation remains consistent.\" and stop] | |
| ### Updates Needed | |
| [For each issue: file path, what to change, why. Be precise enough for another agent to apply the fix.] | |
| ### Conclusion | |
| [Approved | Updates needed (non-blocking) | Blocking contradictions found]'" < /dev/null 2>&1 | tee /tmp/docs-review-output.jsonl | |
| - name: Upload docs review output | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| continue-on-error: true | |
| with: | |
| name: docs-review-output | |
| path: /tmp/docs-review-output.jsonl | |
| retention-days: 7 | |
| # Prompt engineering reviewer - validates prompt clarity, constraints, and cross-prompt consistency | |
| prompt-review: | |
| needs: [get-context, build-devcontainer] | |
| if: | | |
| needs.get-context.outputs.pr_number != '' && | |
| needs.get-context.outputs.is_draft != 'true' && | |
| needs.get-context.outputs.reviews_already_passed != 'true' && | |
| needs.get-context.outputs.author_authorized == 'true' && | |
| needs.get-context.outputs.config_only != 'true' && | |
| needs.get-context.outputs.tests_passed == 'true' | |
| runs-on: [self-hosted, homelab] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| packages: read | |
| steps: | |
| - name: Checkout for composite action | |
| uses: actions/checkout@v4 | |
| - name: Reviewer setup | |
| id: setup | |
| uses: ./.github/actions/reviewer-setup | |
| with: | |
| pr_number: ${{ needs.get-context.outputs.pr_number }} | |
| head_ref: ${{ needs.get-context.outputs.head_ref }} | |
| head_repo: ${{ needs.get-context.outputs.head_repo }} | |
| github_token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| - name: Prompt engineering review in devcontainer | |
| if: steps.setup.outputs.verified == 'true' | |
| uses: devcontainers/ci@v0.3 | |
| with: | |
| imageName: ${{ env.DEVCONTAINER_IMAGE }} | |
| cacheFrom: ${{ env.DEVCONTAINER_IMAGE }} | |
| push: never | |
| env: | | |
| CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| GH_TOKEN=${{ secrets.WORKFLOW_PAT }} | |
| PR_NUMBER=${{ needs.get-context.outputs.pr_number }} | |
| PR_TITLE=${{ needs.get-context.outputs.pr_title }} | |
| HEAD_REF=${{ needs.get-context.outputs.head_ref }} | |
| REPO=${{ github.repository }} | |
| runCmd: | | |
| claude --print \ | |
| --verbose \ | |
| --output-format stream-json \ | |
| --model opus \ | |
| --dangerously-skip-permissions \ | |
| --max-turns 450 \ | |
| "You are a PROMPT ENGINEERING reviewer for PR #${PR_NUMBER} in ${REPO}. | |
| This project is an OpenClaw agent where the prompts in docs/ ARE the application. | |
| Prompt quality directly determines application quality. Your job is to ensure all | |
| prompts and behavioral specifications remain clear, complete, and consistent. | |
| Do NOT push any changes. This is a read-only review. | |
| **CRITICAL: Only report issues that require action from the PR author.** | |
| Do NOT explain prompt engineering concepts. Do NOT praise prompts that are fine. | |
| If there are no actionable issues, post a short approval and move on. | |
| **QUICK CHECK:** If this PR does not modify any files in docs/ or design/, | |
| post: 'Approved - No prompt or behavioral specification changes to evaluate.' | |
| and stop. | |
| **YOUR TASK:** | |
| 1. Read the code changes: \`git diff origin/main...HEAD\` | |
| 2. Read ALL files in docs/ and design/ to understand the full prompt architecture | |
| 3. Evaluate changes against these criteria: | |
| **Prompt Clarity** — Could an LLM misinterpret any instruction? Look for: | |
| - Ambiguous phrasing that could be read multiple ways | |
| - Missing context that an LLM would need to execute correctly | |
| - Instructions that conflict with each other within the same prompt | |
| **Constraint Completeness** — Are behavioral boundaries explicitly stated? | |
| - Are there implicit assumptions that should be made explicit? | |
| - Could the agent do something harmful or unwanted that isn't explicitly prevented? | |
| **Cross-Prompt Consistency** — Do different prompt modules give contradictory instructions? | |
| - Check docs/ai-prompts.md against docs/user-interactions.md | |
| - Check personality constraints across all docs that reference agent behavior | |
| - Verify intent detection patterns are consistent | |
| **Personality/Tone Drift** — Does the change maintain the established personality? | |
| - Casual, brief, like texting a helpful friend | |
| - No emojis unless user uses them first | |
| - Under 50 words unless explaining something complex | |
| **Shame Prevention Compliance** — BLOCKING if violated | |
| - No language that implies user failure or blame | |
| - No guilt-inducing framing for incomplete tasks | |
| - No comparison to 'normal' productivity expectations | |
| - Task rejection must be framed as the system adapting, not the user failing | |
| **Edge Case Handling** — Are ambiguous inputs, empty states, error conditions covered? | |
| - What happens when user input doesn't match any intent pattern? | |
| - Are fallback behaviors defined for unexpected states? | |
| **Few-Shot Example Quality** — Are intent detection examples representative? | |
| - Do examples cover the range of real user inputs? | |
| - Could examples bias the model toward narrow interpretations? | |
| **Output Format Reliability** — Will structured output parse correctly downstream? | |
| - Are response formats clearly specified? | |
| - Could the model deviate from expected format? | |
| **INLINE COMMENTS:** Where a prompt issue is tied to a specific line, also leave | |
| an inline PR review comment on that line. | |
| Post exactly ONE summary comment using: | |
| gh pr comment ${PR_NUMBER} --body '## Prompt Engineering Review | |
| [If no prompt issues: \"Approved - All prompts remain clear, complete, and consistent.\" and stop] | |
| ### Issues Found | |
| [For each issue: file path, line reference, what the problem is, and how to fix it. | |
| Mark shame prevention violations as BLOCKING.] | |
| ### Conclusion | |
| [Approved | Approved with suggestions | Blocking issues found]'" < /dev/null 2>&1 | tee /tmp/prompt-review-output.jsonl | |
| - name: Upload prompt review output | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| continue-on-error: true | |
| with: | |
| name: prompt-review-output | |
| path: /tmp/prompt-review-output.jsonl | |
| retention-days: 7 | |
| # Final decision maker - synthesizes all reviews and makes go/no-go call | |
| merge-decision: | |
| needs: [get-context, build-devcontainer, design-review, security-review, psych-review, docs-review, prompt-review] | |
| if: | | |
| always() && | |
| needs.get-context.outputs.pr_number != '' && | |
| needs.get-context.outputs.is_draft != 'true' && | |
| needs.get-context.outputs.reviews_already_passed != 'true' && | |
| needs.get-context.outputs.author_authorized == 'true' && | |
| needs.get-context.outputs.tests_passed == 'true' | |
| runs-on: [self-hosted, homelab] | |
| outputs: | |
| decision: ${{ steps.parse-decision.outputs.decision }} | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| packages: read | |
| steps: | |
| - name: Checkout for composite action | |
| uses: actions/checkout@v4 | |
| - name: Reviewer setup | |
| id: setup | |
| uses: ./.github/actions/reviewer-setup | |
| with: | |
| pr_number: ${{ needs.get-context.outputs.pr_number }} | |
| head_ref: ${{ needs.get-context.outputs.head_ref }} | |
| head_repo: ${{ needs.get-context.outputs.head_repo }} | |
| github_token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| checkout_token: ${{ secrets.WORKFLOW_PAT }} | |
| - name: Make merge decision in devcontainer | |
| if: steps.setup.outputs.verified == 'true' | |
| uses: devcontainers/ci@v0.3 | |
| with: | |
| imageName: ${{ env.DEVCONTAINER_IMAGE }} | |
| cacheFrom: ${{ env.DEVCONTAINER_IMAGE }} | |
| push: never | |
| env: | | |
| CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| GH_TOKEN=${{ secrets.WORKFLOW_PAT }} | |
| PR_NUMBER=${{ needs.get-context.outputs.pr_number }} | |
| HEAD_REF=${{ needs.get-context.outputs.head_ref }} | |
| DESIGN_REVIEW_RESULT=${{ needs.design-review.result }} | |
| SECURITY_REVIEW_RESULT=${{ needs.security-review.result }} | |
| PSYCH_REVIEW_RESULT=${{ needs.psych-review.result }} | |
| DOCS_REVIEW_RESULT=${{ needs.docs-review.result }} | |
| PROMPT_REVIEW_RESULT=${{ needs.prompt-review.result }} | |
| runCmd: | | |
| # Using Sonnet: Summarization task - synthesizes existing review results | |
| claude --print \ | |
| --verbose \ | |
| --output-format stream-json \ | |
| --model sonnet \ | |
| --dangerously-skip-permissions \ | |
| --max-turns 150 \ | |
| "You are the FINAL DECISION MAKER for PR #${PR_NUMBER}. | |
| You have personal responsibility to approve or reject this PR for merge to main and deployment to production. | |
| **REVIEW STATUS:** | |
| - Design Review: ${DESIGN_REVIEW_RESULT} | |
| - Security & Infrastructure Review: ${SECURITY_REVIEW_RESULT} | |
| - Psych Research Review: ${PSYCH_REVIEW_RESULT} | |
| - Prompt Engineering Review: ${PROMPT_REVIEW_RESULT} | |
| - Documentation Consistency Review: ${DOCS_REVIEW_RESULT} | |
| **YOUR TASK:** | |
| 1. Read the review comments on this PR: \`gh pr view ${PR_NUMBER} --comments\` | |
| 2. Check for merge conflicts: \`git fetch origin main && git merge-base --is-ancestor origin/main HEAD || git merge origin/main --no-commit --no-ff\` | |
| 3. If there are merge conflicts AND you believe the PR should be merged, resolve them | |
| 4. **Apply fixes from reviewers**: Check the review comments for fixes described by reviewers: | |
| - Security Review: \"Fixes Needed\" section | |
| - Documentation Consistency Review: \"Updates Needed\" section | |
| If any reviewer described specific fixes, apply them now (edit the files, commit, and push). | |
| You are the ONLY agent with push access. | |
| 5. Make a GO/NO-GO decision | |
| **DECISION CRITERIA:** | |
| - GO: All reviews passed or had only minor non-blocking issues, no merge conflicts (or you resolved them) | |
| - NO-GO: Any review found blocking issues, unresolvable merge conflicts, or tests are failing | |
| **IF NO-GO:** Explain what must be fixed before the PR can merge. | |
| **IF GO with fixes needed:** Make the fixes (merge conflicts, reviewer-requested changes, minor issues), commit, push, then approve. | |
| BEFORE PUSHING: rebase with \`git pull --rebase origin ${HEAD_REF}\` | |
| Post exactly ONE comment: | |
| gh pr comment ${PR_NUMBER} --body '## Merge Decision | |
| **Decision: [GO | NO-GO]** | |
| [If GO: \"Ready for merge to main and production deployment.\"] | |
| [If NO-GO: List specific blockers that must be resolved.] | |
| [If you made fixes: \"Applied fixes: [brief description]\"]'" < /dev/null 2>&1 | tee /tmp/merge-decision-output.jsonl | |
| - name: Upload merge decision output | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| continue-on-error: true | |
| with: | |
| name: merge-decision-output | |
| path: /tmp/merge-decision-output.jsonl | |
| retention-days: 7 | |
| - name: Parse merge decision from PR comments | |
| id: parse-decision | |
| if: always() && steps.setup.outputs.verified == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ needs.get-context.outputs.pr_number }} | |
| run: | | |
| # Fetch the most recent "Merge Decision" comment from the PR | |
| COMMENTS=$(gh api repos/${{ github.repository }}/issues/${PR_NUMBER}/comments \ | |
| --jq '[.[] | select(.body | contains("## Merge Decision"))] | last') | |
| if [ -z "$COMMENTS" ] || [ "$COMMENTS" = "null" ]; then | |
| echo "No merge decision comment found - defaulting to NO-GO" | |
| echo "decision=NO-GO" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| COMMENT_BODY=$(echo "$COMMENTS" | jq -r '.body') | |
| # Check for GO or NO-GO in the decision line | |
| if echo "$COMMENT_BODY" | grep -qE "Decision:.*GO" && ! echo "$COMMENT_BODY" | grep -qE "Decision:.*NO-GO"; then | |
| echo "Merge decision: GO" | |
| echo "decision=GO" >> $GITHUB_OUTPUT | |
| elif echo "$COMMENT_BODY" | grep -qE "NO-GO"; then | |
| echo "Merge decision: NO-GO" | |
| echo "decision=NO-GO" >> $GITHUB_OUTPUT | |
| else | |
| echo "Could not parse decision from comment - defaulting to NO-GO" | |
| echo "Comment body:" | |
| echo "$COMMENT_BODY" | |
| echo "decision=NO-GO" >> $GITHUB_OUTPUT | |
| fi | |
| # Aggregator job - use this as the required check for branch protection | |
| all-reviews-passed: | |
| name: All Required Agent Reviews | |
| runs-on: [self-hosted, homelab] | |
| needs: [get-context, build-devcontainer, fix-test-failures, design-review, security-review, psych-review, docs-review, prompt-review, merge-decision] | |
| if: always() | |
| permissions: | |
| statuses: write # Required to create commit status on PR | |
| pull-requests: write # Required to add label | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Check review results | |
| id: check-results | |
| run: | | |
| check_result() { | |
| local result=$1 | |
| [[ "$result" == "success" || "$result" == "skipped" ]] | |
| } | |
| failed=false | |
| # PR reopen only resets the label — the real reviews come from | |
| # the workflow_run trigger once PR Tests complete. Exit with | |
| # failure so this run does NOT satisfy the branch-protection | |
| # check before actual reviews have run. | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "PR reopened — waiting for PR Tests to trigger the real review run" | |
| exit 1 | |
| fi | |
| # Check if we have a valid PR | |
| if [ -z "${{ needs.get-context.outputs.pr_number }}" ]; then | |
| echo "No PR found for this workflow run - skipping reviews" | |
| exit 0 | |
| fi | |
| # If reviews already passed, signal success but still create commit status | |
| REVIEWS_ALREADY_PASSED="${{ needs.get-context.outputs.reviews_already_passed }}" | |
| if [ "$REVIEWS_ALREADY_PASSED" = "true" ]; then | |
| echo "Reviews already passed for this PR - skipping re-review" | |
| echo "To re-run reviews: close and reopen the PR" | |
| echo "skipped_with_success=true" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # Skip reviews for draft PRs (TDD workflow support) | |
| IS_DRAFT="${{ needs.get-context.outputs.is_draft }}" | |
| if [ "$IS_DRAFT" = "true" ]; then | |
| echo "Draft PR - skipping auto-fix and reviews" | |
| echo "Draft PRs support TDD workflows where tests may intentionally fail." | |
| echo "Mark the PR as ready for review to enable auto-fix and agent reviews." | |
| echo "is_draft=true" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # SECURITY: Check if author was authorized | |
| AUTHOR_AUTHORIZED="${{ needs.get-context.outputs.author_authorized }}" | |
| if [ "$AUTHOR_AUTHORIZED" != "true" ]; then | |
| echo "PR author was not authorized to trigger Claude reviews" | |
| echo "External contributors cannot trigger Claude Code Review workflows." | |
| echo "This is a security measure to prevent prompt injection attacks." | |
| echo "A repository maintainer will need to review this PR manually." | |
| echo "unauthorized=true" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # If tests failed and we're still under max attempts, fix-test-failures ran | |
| # and pushed changes - this workflow run is complete, next run will continue | |
| TESTS_PASSED="${{ needs.get-context.outputs.tests_passed }}" | |
| FIX_ATTEMPTS="${{ needs.get-context.outputs.fix_attempts }}" | |
| if [ "$TESTS_PASSED" = "false" ] && [ "$FIX_ATTEMPTS" -lt 3 ]; then | |
| echo "Tests failed - fix attempt was made, waiting for next test run" | |
| # Check if fix-test-failures succeeded | |
| if ! check_result "${{ needs.fix-test-failures.result }}"; then | |
| echo "Fix attempt failed: ${{ needs.fix-test-failures.result }}" | |
| failed=true | |
| else | |
| echo "Fix attempt completed successfully - new test run should start" | |
| exit 0 | |
| fi | |
| fi | |
| # If tests passed, check review results | |
| if [ "$TESTS_PASSED" = "true" ]; then | |
| if ! check_result "${{ needs.build-devcontainer.result }}"; then | |
| echo "Build Devcontainer: ${{ needs.build-devcontainer.result }}" | |
| failed=true | |
| fi | |
| if ! check_result "${{ needs.design-review.result }}"; then | |
| echo "Design Review: ${{ needs.design-review.result }}" | |
| failed=true | |
| fi | |
| if ! check_result "${{ needs.security-review.result }}"; then | |
| echo "Security Review: ${{ needs.security-review.result }}" | |
| failed=true | |
| fi | |
| if ! check_result "${{ needs.psych-review.result }}"; then | |
| echo "Psych Research Review: ${{ needs.psych-review.result }}" | |
| failed=true | |
| fi | |
| if ! check_result "${{ needs.docs-review.result }}"; then | |
| echo "Docs Review: ${{ needs.docs-review.result }}" | |
| failed=true | |
| fi | |
| if ! check_result "${{ needs.prompt-review.result }}"; then | |
| echo "Prompt Engineering Review: ${{ needs.prompt-review.result }}" | |
| failed=true | |
| fi | |
| if ! check_result "${{ needs.merge-decision.result }}"; then | |
| echo "Merge Decision: ${{ needs.merge-decision.result }}" | |
| failed=true | |
| fi | |
| # Check the actual merge decision (GO vs NO-GO) from the agent's comment | |
| MERGE_DECISION="${{ needs.merge-decision.outputs.decision }}" | |
| echo "Merge Decision Agent Verdict: $MERGE_DECISION" | |
| if [ "$MERGE_DECISION" = "NO-GO" ]; then | |
| echo "Merge decision agent returned NO-GO - blocking issues found" | |
| echo "See the 'Merge Decision' comment on the PR for details" | |
| echo "merge_decision_nogo=true" >> $GITHUB_OUTPUT | |
| failed=true | |
| elif [ -z "$MERGE_DECISION" ]; then | |
| echo "Could not determine merge decision - treating as NO-GO" | |
| echo "merge_decision_nogo=true" >> $GITHUB_OUTPUT | |
| failed=true | |
| fi | |
| fi | |
| # If max attempts reached and tests still failing | |
| if [ "$TESTS_PASSED" = "false" ] && [ "$FIX_ATTEMPTS" -ge 3 ]; then | |
| echo "Max fix attempts (3) reached - tests still failing" | |
| echo "Human intervention required" | |
| failed=true | |
| fi | |
| if [ "$failed" = true ]; then | |
| echo "" | |
| echo "Some checks failed" | |
| exit 1 | |
| fi | |
| echo "All agent reviews completed and merge decision is GO - PR is ready for merge" | |
| echo "" | |
| echo "Review Results:" | |
| echo " Tests Passed: ${{ needs.get-context.outputs.tests_passed }}" | |
| echo " Fix Attempts: ${{ needs.get-context.outputs.fix_attempts }}" | |
| echo " Build Devcontainer: ${{ needs.build-devcontainer.result }}" | |
| echo " Fix Test Failures: ${{ needs.fix-test-failures.result }}" | |
| echo " Design Review: ${{ needs.design-review.result }}" | |
| echo " Security Review: ${{ needs.security-review.result }}" | |
| echo " Psych Research Review: ${{ needs.psych-review.result }}" | |
| echo " Prompt Engineering Review: ${{ needs.prompt-review.result }}" | |
| echo " Docs Review: ${{ needs.docs-review.result }}" | |
| echo " Merge Decision Job: ${{ needs.merge-decision.result }}" | |
| echo " Merge Decision Verdict: ${{ needs.merge-decision.outputs.decision }}" | |
| # Signal that we should add the label (only when merge decision is GO) | |
| echo "should_label=true" >> $GITHUB_OUTPUT | |
| - name: Create commit status for reviews | |
| if: | | |
| needs.get-context.outputs.pr_number != '' && | |
| (needs.get-context.outputs.tests_passed == 'true' || steps.check-results.outputs.skipped_with_success == 'true') | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Get the HEAD SHA of the PR branch | |
| PR_NUMBER="${{ needs.get-context.outputs.pr_number }}" | |
| HEAD_SHA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER --jq '.head.sha') | |
| # Determine description based on whether this was a skip or full review | |
| if [ "${{ steps.check-results.outputs.skipped_with_success }}" = "true" ]; then | |
| DESCRIPTION="Agent reviews already passed (skipped re-review)" | |
| else | |
| DESCRIPTION="All agent reviews completed successfully" | |
| fi | |
| echo "Creating commit status for PR #$PR_NUMBER (SHA: $HEAD_SHA)" | |
| # Create commit status | |
| gh api repos/${{ github.repository }}/statuses/$HEAD_SHA \ | |
| -f state="success" \ | |
| -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| -f description="$DESCRIPTION" \ | |
| -f context="All Required Agent Reviews" | |
| echo "Commit status created successfully" | |
| - name: Add agent-reviews-passed label | |
| if: | | |
| steps.check-results.outputs.should_label == 'true' && | |
| needs.get-context.outputs.pr_number != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_NUMBER="${{ needs.get-context.outputs.pr_number }}" | |
| # Create the label if it doesn't exist | |
| gh label create "agent-reviews-passed" \ | |
| --color "0e8a16" \ | |
| --description "All agent reviews have passed" \ | |
| --repo ${{ github.repository }} 2>/dev/null || true | |
| # Add the label to the PR | |
| gh pr edit $PR_NUMBER --add-label "agent-reviews-passed" --repo ${{ github.repository }} | |
| echo "Added 'agent-reviews-passed' label to PR #$PR_NUMBER" | |
| echo "Subsequent pushes will skip agent reviews." | |
| echo "To re-run reviews: close and reopen the PR." | |
| - name: Notify agent webhook | |
| if: always() && needs.get-context.outputs.pr_number != '' | |
| continue-on-error: true | |
| run: | | |
| # Signal the OpenClaw agent that reviews are complete | |
| # The webhook discards all request data — only the connection itself is the signal | |
| WEBHOOK_URL="${{ vars.AGENT_WEBHOOK_URL }}" | |
| if [ -n "$WEBHOOK_URL" ]; then | |
| curl -s --max-time 5 "$WEBHOOK_URL" >/dev/null 2>&1 || true | |
| fi |