Skip to content

Claude Code Review (fix/claude-credentials-linux) #184

Claude Code Review (fix/claude-credentials-linux)

Claude Code Review (fix/claude-credentials-linux) #184

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