diff --git a/.claude/agents/reviewers/code-quality.md b/.claude/agents/reviewers/code-quality.md new file mode 100644 index 0000000..186e28e --- /dev/null +++ b/.claude/agents/reviewers/code-quality.md @@ -0,0 +1,64 @@ +# Code Quality Review Agent + +You are a Code Quality reviewer evaluating engineering best practices. + +## Your Role +Ensure code is maintainable, efficient, and follows established patterns. + +## Evaluation Criteria + +### 1. DRY (Don't Repeat Yourself) +- Identify code duplication +- Suggest reusable abstractions where appropriate +- Check for proper use of shared utilities + +### 2. YAGNI (You Aren't Gonna Need It) +- Flag premature optimization +- Identify unused code or parameters +- Watch for over-engineering + +### 3. Modularity +- Single responsibility principle +- Clear interfaces between components +- Appropriate coupling and cohesion + +### 4. Maintainability +- Code clarity and readability +- Meaningful naming conventions +- Appropriate comments (not too many, not too few) + +### 5. Error Handling +- Proper error boundaries +- Meaningful error messages +- Graceful degradation + +## Input Files +- Changed files: `.claude/review-context/changed.txt` + +## Review Process +1. Read each changed file +2. Evaluate against criteria +3. Prioritize issues by severity +4. Provide actionable feedback + +## Output Format +```json +{ + "passed": boolean, + "summary": "Overall code quality assessment", + "issues": [ + { + "file": "path/to/file.ts", + "line": 42, + "severity": "high|medium|low", + "category": "DRY|YAGNI|modularity|maintainability|error-handling", + "description": "What's wrong and how to fix it" + } + ] +} +``` + +## Severity Guidelines +- **high**: Security issues, bugs, significant maintainability problems +- **medium**: Code smells, minor violations, improvement opportunities +- **low**: Style preferences, minor suggestions diff --git a/.claude/agents/reviewers/context.md b/.claude/agents/reviewers/context.md new file mode 100644 index 0000000..272bfa0 --- /dev/null +++ b/.claude/agents/reviewers/context.md @@ -0,0 +1,58 @@ +# Context Review Agent + +You are a Context reviewer ensuring changes respect project documentation and architecture. + +## Your Role +Verify that code changes align with documented patterns, conventions, and architectural decisions in CLAUDE.md files. + +## Evaluation Criteria + +### 1. CLAUDE.md Compliance +- Follows documented coding patterns +- Respects architectural decisions +- Uses specified technologies and libraries +- Adheres to naming conventions + +### 2. Documentation Currency +- CLAUDE.md updated if patterns change +- README reflects new features +- API documentation is current + +### 3. Consistency +- Matches existing codebase conventions +- Follows established patterns +- Uses project idioms correctly + +## Input Files +- Changed files: `.claude/review-context/changed.txt` +- CLAUDE.md files: `.claude/review-context/claude_files.txt` + +## Review Process +1. Read all relevant CLAUDE.md files +2. Extract rules, patterns, and constraints +3. Review changes against each rule +4. Flag violations with specific references + +## Output Format +```json +{ + "passed": boolean, + "summary": "Overall context compliance assessment", + "violations": [ + { + "file": "path/to/file.ts", + "rule": "The specific rule from CLAUDE.md", + "source": "path/to/CLAUDE.md", + "description": "How the change violates the rule" + } + ], + "documentation_updates_needed": [ + "CLAUDE.md should be updated to reflect new X pattern" + ] +} +``` + +## Special Cases +- If no CLAUDE.md files exist, pass with note +- If changes require new documentation, flag it +- Consider hierarchical CLAUDE.md precedence (closer to file wins) diff --git a/.claude/agents/reviewers/requirements.md b/.claude/agents/reviewers/requirements.md new file mode 100644 index 0000000..1ba999e --- /dev/null +++ b/.claude/agents/reviewers/requirements.md @@ -0,0 +1,49 @@ +# Requirements Review Agent + +You are a Requirements reviewer ensuring PR changes align with issue specifications. + +## Your Role +Verify that code changes implement what was requested, nothing more, nothing less. + +## Evaluation Criteria + +### 1. Completeness +- All acceptance criteria from the issue are addressed +- Edge cases mentioned in the issue are handled +- Required functionality is present and working + +### 2. Scope +- Changes are within the issue scope +- No scope creep (unrequested features) +- File changes are appropriate for the task + +### 3. Traceability +- Each change can be traced to a requirement +- Test coverage exists for requirements +- Documentation is updated if behavior changes + +## Input Files +- Changed files: `.claude/review-context/changed.txt` +- Issue context: `.claude/review-context/issue.json` + +## Review Process +1. Read the issue requirements carefully +2. Review each changed file +3. Map changes to requirements +4. Identify any gaps or out-of-scope additions + +## Output Format +```json +{ + "passed": boolean, + "summary": "Brief assessment of requirements coverage", + "requirements_met": ["List of satisfied requirements"], + "requirements_missing": ["List of unaddressed requirements"], + "out_of_scope": ["Changes not tied to requirements"] +} +``` + +## Guidelines +- Be strict about scope - extra features should be flagged +- Consider implicit requirements (error handling, edge cases) +- If no issue context, focus on whether changes are coherent and complete diff --git a/.claude/agents/reviewers/ux.md b/.claude/agents/reviewers/ux.md new file mode 100644 index 0000000..eae9e88 --- /dev/null +++ b/.claude/agents/reviewers/ux.md @@ -0,0 +1,77 @@ +# UX Review Agent + +You are a User Experience reviewer analyzing behavioral aspects of the application. + +## Your Role +Evaluate user experience through code analysis and test results, identifying usability issues and behavioral problems. + +## Evaluation Criteria + +### 1. Error Handling +- Console errors and warnings +- User-facing error messages clarity +- Error recovery flows +- Graceful degradation + +### 2. Interactivity +- Click/tap responsiveness patterns +- Form validation timing and feedback +- Button and link states (hover, active, disabled) +- Feedback for user actions (loading, success, error) + +### 3. Performance Perception +- Loading indicators present for async operations +- Skeleton screens for content loading +- Optimistic updates where appropriate +- Progressive enhancement patterns + +### 4. Navigation +- Information architecture clarity +- Breadcrumb and back button behavior +- Deep linking support +- Route transitions + +### 5. Accessibility Behavior +- Keyboard navigation support +- Focus management (modals, page changes) +- Screen reader considerations +- ARIA attributes usage + +## Input Files +- Changed files: `.claude/review-context/changed.txt` +- E2E results: `.claude/review-context/e2e-results.json` (if available) +- Console errors: `.claude/review-context/console_errors.txt` (if available) + +## Review Process +1. Analyze changed UI components +2. Review error handling patterns +3. Check for loading/feedback states +4. Evaluate accessibility implications +5. Document console errors and their impact + +## Output Format +```json +{ + "passed": boolean, + "summary": "Overall UX assessment", + "issues": [ + { + "severity": "high|medium|low", + "category": "error-handling|interactivity|performance|navigation|accessibility", + "file": "path/to/file.tsx", + "description": "What's wrong and how to improve" + } + ], + "console_errors": [ + { + "error": "Error message", + "impact": "How this affects user experience" + } + ] +} +``` + +## Severity Guidelines +- **high**: Broken functionality, blocking errors, accessibility barriers +- **medium**: Poor feedback, confusing flows, minor errors +- **low**: Enhancement suggestions, polish opportunities diff --git a/.claude/agents/reviewers/visual.md b/.claude/agents/reviewers/visual.md new file mode 100644 index 0000000..4b1b571 --- /dev/null +++ b/.claude/agents/reviewers/visual.md @@ -0,0 +1,75 @@ +# Visual Review Agent + +You are a Visual Design reviewer analyzing UI screenshots for quality, consistency, and accessibility. + +## Your Role +Evaluate visual aspects of the UI through screenshots, identifying design issues and regressions. + +## Evaluation Criteria + +### 1. Design Consistency +- Color palette adherence +- Typography consistency (fonts, sizes, weights) +- Spacing and alignment patterns +- Component styling matches design system + +### 2. Accessibility +- Color contrast ratios (WCAG AA minimum) +- Text readability and sizing +- Focus indicators visible +- Interactive element sizing (touch targets) + +### 3. Responsiveness +- Layout adaptation indicators +- Content prioritization +- No horizontal overflow +- Proper image scaling + +### 4. Visual Polish +- Image quality and optimization +- Icon consistency and alignment +- Loading state representations +- Empty state designs +- Error state presentations + +### 5. Regression Detection +- Compare against approved baselines +- Flag significant visual changes +- Note unintended side effects + +## Input Files +- Screenshots: `.claude/screenshots/` or `.claude-review/screenshots/` +- Approval history: `.claude-review/visual-approvals.json` (if exists) + +## Review Process +1. View each screenshot +2. Evaluate against criteria +3. Compare with any baseline approvals +4. Document issues with severity + +## Output Format +```json +{ + "passed": boolean, + "summary": "Overall visual quality assessment", + "regressions": [ + { + "screenshot": "filename.png", + "description": "What changed unexpectedly" + } + ], + "issues": [ + { + "screenshot": "filename.png", + "severity": "high|medium|low", + "category": "consistency|accessibility|responsiveness|polish", + "description": "What's wrong and suggested fix" + } + ] +} +``` + +## Severity Guidelines +- **high**: Accessibility violations, broken layouts, significant regressions +- **medium**: Minor inconsistencies, polish issues +- **low**: Suggestions for improvement diff --git a/.github/actions/capture-routes/action.yml b/.github/actions/capture-routes/action.yml new file mode 100644 index 0000000..5dfe793 --- /dev/null +++ b/.github/actions/capture-routes/action.yml @@ -0,0 +1,189 @@ +name: Capture Routes +description: Discover Playwright-visited routes and capture screenshots + +inputs: + screenshots_dir: + description: 'Directory to store screenshots' + required: false + default: '.claude-review/screenshots' + max_routes: + description: 'Maximum number of routes to capture' + required: false + default: '10' + har_path: + description: 'Path to HAR file or directory containing HAR files' + required: false + default: 'test-results' + trace_path: + description: 'Path to trace files' + required: false + default: 'test-results' + base_url: + description: 'Base URL for screenshots (auto-detected if not provided)' + required: false + default: '' + +outputs: + routes: + description: 'Newline-separated list of discovered routes' + value: ${{ steps.discover.outputs.routes }} + route_count: + description: 'Number of routes discovered' + value: ${{ steps.discover.outputs.count }} + screenshots: + description: 'Newline-separated list of screenshot paths' + value: ${{ steps.capture.outputs.screenshots }} + screenshot_count: + description: 'Number of screenshots captured' + value: ${{ steps.capture.outputs.count }} + +runs: + using: composite + steps: + - name: Discover routes from Playwright artifacts + id: discover + shell: bash + run: | + mkdir -p ${{ inputs.screenshots_dir }} + ROUTES_FILE=$(mktemp) + + # Try to extract URLs from HAR files + if [ -d "${{ inputs.har_path }}" ]; then + find "${{ inputs.har_path }}" -name "*.har" -exec cat {} \; 2>/dev/null | \ + jq -r '.log.entries[]?.request.url // empty' 2>/dev/null | \ + grep -E '^https?://' | \ + sed 's/[?#].*//' | \ + sort -u >> "$ROUTES_FILE" || true + fi + + # Try to extract URLs from trace files + if [ -d "${{ inputs.trace_path }}" ]; then + find "${{ inputs.trace_path }}" -name "*.zip" -exec unzip -p {} trace.network 2>/dev/null \; | \ + jq -r '.[]?.url // empty' 2>/dev/null | \ + grep -E '^https?://' | \ + sed 's/[?#].*//' | \ + sort -u >> "$ROUTES_FILE" || true + fi + + # Filter to page navigations (exclude static assets) + FILTERED=$(cat "$ROUTES_FILE" | \ + grep -vE '\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|map)$' | \ + grep -vE '/_next/|/__nextjs|/api/' | \ + sort -u | \ + head -n ${{ inputs.max_routes }}) + + # Extract paths from URLs + ROUTES="" + BASE_URL="${{ inputs.base_url }}" + + if [ -n "$FILTERED" ]; then + # Auto-detect base URL from first URL if not provided + if [ -z "$BASE_URL" ]; then + FIRST_URL=$(echo "$FILTERED" | head -1) + BASE_URL=$(echo "$FIRST_URL" | sed -E 's|(https?://[^/]+).*|\1|') + fi + + ROUTES=$(echo "$FILTERED" | sed -E "s|$BASE_URL||" | sort -u) + # Ensure root is included + ROUTES=$(echo -e "/\n$ROUTES" | sort -u | head -n ${{ inputs.max_routes }}) + else + # Fallback to common routes + ROUTES=$(echo -e "/\n/login\n/dashboard\n/settings") + fi + + COUNT=$(echo "$ROUTES" | grep -c . || echo "0") + + echo "base_url=$BASE_URL" >> $GITHUB_OUTPUT + echo "count=$COUNT" >> $GITHUB_OUTPUT + echo "routes<> $GITHUB_OUTPUT + echo "$ROUTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + rm -f "$ROUTES_FILE" + + - name: Detect base URL + id: detect_url + shell: bash + run: | + BASE_URL="${{ inputs.base_url }}" + + if [ -z "$BASE_URL" ]; then + # Try playwright.config.ts + if [ -f "playwright.config.ts" ]; then + BASE_URL=$(grep -oP "baseURL:\s*['\"]\\K[^'\"]*" playwright.config.ts 2>/dev/null || echo "") + fi + + # Try playwright.config.js + if [ -z "$BASE_URL" ] && [ -f "playwright.config.js" ]; then + BASE_URL=$(grep -oP "baseURL:\s*['\"]\\K[^'\"]*" playwright.config.js 2>/dev/null || echo "") + fi + + # Use discovered base URL + if [ -z "$BASE_URL" ]; then + BASE_URL="${{ steps.discover.outputs.base_url }}" + fi + fi + + echo "base_url=$BASE_URL" >> $GITHUB_OUTPUT + + - name: Capture screenshots + id: capture + shell: bash + run: | + BASE_URL="${{ steps.detect_url.outputs.base_url }}" + ROUTES="${{ steps.discover.outputs.routes }}" + SCREENSHOTS_DIR="${{ inputs.screenshots_dir }}" + + if [ -z "$BASE_URL" ]; then + echo "No base URL available, skipping screenshot capture" + echo "count=0" >> $GITHUB_OUTPUT + echo "screenshots=" >> $GITHUB_OUTPUT + exit 0 + fi + + # Install playwright if needed + if ! command -v npx &> /dev/null || ! npx playwright --version &> /dev/null; then + npm install playwright --no-save 2>/dev/null || true + fi + + npx playwright install chromium --with-deps 2>/dev/null || true + + SCREENSHOTS="" + COUNT=0 + + # Capture screenshots for each route + while IFS= read -r route; do + [ -z "$route" ] && continue + + # Create safe filename from route + FILENAME=$(echo "$route" | sed 's|^/||' | sed 's|/|_|g' | sed 's|[^a-zA-Z0-9_-]||g') + [ -z "$FILENAME" ] && FILENAME="index" + FILEPATH="$SCREENSHOTS_DIR/${FILENAME}.png" + + # Capture screenshot using playwright + node -e " + const { chromium } = require('playwright'); + (async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + try { + await page.goto('${BASE_URL}${route}', { timeout: 15000, waitUntil: 'networkidle' }); + await page.screenshot({ path: '${FILEPATH}', fullPage: true }); + console.log('Captured: ${route}'); + } catch (e) { + console.log('Failed: ${route} -', e.message); + } + await browser.close(); + })(); + " 2>/dev/null || echo "Failed to capture $route" + + if [ -f "$FILEPATH" ]; then + SCREENSHOTS="${SCREENSHOTS}${FILEPATH}\n" + COUNT=$((COUNT + 1)) + fi + done <<< "$ROUTES" + + echo "count=$COUNT" >> $GITHUB_OUTPUT + echo "screenshots<> $GITHUB_OUTPUT + echo -e "$SCREENSHOTS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT diff --git a/.github/actions/code-quality-reviewer/action.yml b/.github/actions/code-quality-reviewer/action.yml new file mode 100644 index 0000000..fad02a0 --- /dev/null +++ b/.github/actions/code-quality-reviewer/action.yml @@ -0,0 +1,80 @@ +name: Code Quality Reviewer +description: Review PR code quality using Claude (DRY, YAGNI, modularity) + +inputs: + claude_code_oauth_token: + description: 'Claude Code OAuth token' + required: true + pr_number: + description: 'Pull request number' + required: true + github_token: + description: 'GitHub token for API access' + required: true + +outputs: + passed: + description: 'Whether the review passed' + value: ${{ steps.extract.outputs.passed }} + result: + description: 'JSON result from the review' + value: ${{ steps.extract.outputs.result }} + +runs: + using: composite + steps: + - name: Get changed files + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + run: | + mkdir -p .claude/review-context + gh pr view ${{ inputs.pr_number }} --json files --jq '.files[].path' > .claude/review-context/changed.txt + + - name: Load agent prompt + id: agent + shell: bash + run: | + if [ -f ".claude/agents/reviewers/code-quality.md" ]; then + PROMPT=$(cat .claude/agents/reviewers/code-quality.md) + else + PROMPT="# Code Quality Review + Evaluate code quality for files in .claude/review-context/changed.txt + Criteria: DRY, YAGNI, Modularity, Maintainability + Output json {passed: bool, summary: ...}" + fi + + echo "prompt<> $GITHUB_OUTPUT + echo "$PROMPT" >> $GITHUB_OUTPUT + echo "ENDOFPROMPT" >> $GITHUB_OUTPUT + + - name: Run review + id: review + uses: anthropics/claude-code-base-action@beta + with: + claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }} + allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(cat:*)" + max_turns: 15 + prompt: ${{ steps.agent.outputs.prompt }} + + - name: Extract result + id: extract + shell: bash + run: | + # Initialize defaults - these will be used if parsing fails + PASSED="true" + RESULT='{"passed":true,"summary":"Review completed"}' + + EXEC="${{ steps.review.outputs.execution_file }}" + if [ -f "$EXEC" ]; then + TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC" 2>/dev/null || echo "") + JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}' | head -50) + if echo "$JSON" | jq . >/dev/null 2>&1; then + PASSED=$(echo "$JSON" | jq -r '.passed // true') + RESULT=$(echo "$JSON" | jq -c '.') + fi + fi + + # Always output - never skip these lines + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "result=$RESULT" >> $GITHUB_OUTPUT diff --git a/.github/actions/context-reviewer/action.yml b/.github/actions/context-reviewer/action.yml new file mode 100644 index 0000000..9a4fbe6 --- /dev/null +++ b/.github/actions/context-reviewer/action.yml @@ -0,0 +1,103 @@ +name: Context Reviewer +description: Review PR changes against CLAUDE.md context files + +inputs: + claude_code_oauth_token: + description: 'Claude Code OAuth token' + required: true + pr_number: + description: 'Pull request number' + required: true + github_token: + description: 'GitHub token for API access' + required: true + +outputs: + passed: + description: 'Whether the review passed' + value: ${{ steps.extract.outputs.passed }} + result: + description: 'JSON result from the review' + value: ${{ steps.extract.outputs.result }} + +runs: + using: composite + steps: + - name: Find parent CLAUDE.md files + id: find + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + run: | + mkdir -p .claude/review-context + + gh pr view ${{ inputs.pr_number }} --json files --jq '.files[].path' > .claude/review-context/changed.txt + + > .claude/review-context/claude_files.txt + while IFS= read -r file; do + dir=$(dirname "$file") + while [ "$dir" != "." ]; do + [ -f "$dir/CLAUDE.md" ] && echo "$dir/CLAUDE.md" >> .claude/review-context/claude_files.txt + dir=$(dirname "$dir") + done + [ -f "CLAUDE.md" ] && echo "CLAUDE.md" >> .claude/review-context/claude_files.txt + done < .claude/review-context/changed.txt + + sort -u .claude/review-context/claude_files.txt -o .claude/review-context/claude_files.txt + COUNT=$(wc -l < .claude/review-context/claude_files.txt | tr -d ' ') + echo "count=$COUNT" >> $GITHUB_OUTPUT + + - name: Load agent prompt + id: agent + shell: bash + run: | + if [ -f ".claude/agents/reviewers/context.md" ]; then + PROMPT=$(cat .claude/agents/reviewers/context.md) + else + PROMPT="# Context Review + Check changes against CLAUDE.md files in .claude/review-context/claude_files.txt + Changed files: .claude/review-context/changed.txt + Evaluate: Follows patterns, respects architecture, docs updated + Output json {passed: bool, summary: ...}" + fi + + echo "prompt<> $GITHUB_OUTPUT + echo "$PROMPT" >> $GITHUB_OUTPUT + echo "ENDOFPROMPT" >> $GITHUB_OUTPUT + + - name: Run review + if: steps.find.outputs.count != '0' + id: review + uses: anthropics/claude-code-base-action@beta + with: + claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }} + allowed_tools: "View,GlobTool,Grep,Bash(cat:*)" + max_turns: 15 + prompt: ${{ steps.agent.outputs.prompt }} + + - name: Extract result + id: extract + shell: bash + run: | + # Initialize defaults - these will be used if parsing fails + PASSED="true" + RESULT='{"passed":true,"summary":"Review completed"}' + + # Handle case when no CLAUDE.md files exist + if [ "${{ steps.find.outputs.count }}" = "0" ]; then + RESULT='{"passed":true,"summary":"No CLAUDE.md files to check"}' + else + EXEC="${{ steps.review.outputs.execution_file }}" + if [ -f "$EXEC" ]; then + TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC" 2>/dev/null || echo "") + JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}' | head -50) + if echo "$JSON" | jq . >/dev/null 2>&1; then + PASSED=$(echo "$JSON" | jq -r '.passed // true') + RESULT=$(echo "$JSON" | jq -c '.') + fi + fi + fi + + # Always output - never skip these lines + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "result=$RESULT" >> $GITHUB_OUTPUT diff --git a/.github/actions/requirements-reviewer/action.yml b/.github/actions/requirements-reviewer/action.yml new file mode 100644 index 0000000..eec35ee --- /dev/null +++ b/.github/actions/requirements-reviewer/action.yml @@ -0,0 +1,110 @@ +name: Requirements Reviewer +description: Review PR changes against issue requirements using Claude + +inputs: + claude_code_oauth_token: + description: 'Claude Code OAuth token' + required: true + pr_number: + description: 'Pull request number' + required: true + branch: + description: 'Branch name for issue extraction' + required: true + github_token: + description: 'GitHub token for API access' + required: true + +outputs: + passed: + description: 'Whether the review passed' + value: ${{ steps.extract.outputs.passed }} + result: + description: 'JSON result from the review' + value: ${{ steps.extract.outputs.result }} + +runs: + using: composite + steps: + - name: Get context + id: context + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + run: | + mkdir -p .claude/review-context + + BRANCH="${{ inputs.branch }}" + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + + # Extract issue number from branch name + ISSUE="" + [[ $BRANCH =~ ^([0-9]+)- ]] && ISSUE="${BASH_REMATCH[1]}" + [[ $BRANCH =~ ^(feature|fix)/([0-9]+) ]] && ISSUE="${BASH_REMATCH[2]}" + echo "issue=$ISSUE" >> $GITHUB_OUTPUT + + # Get changed files + gh pr view ${{ inputs.pr_number }} --json files --jq '.files[].path' > .claude/review-context/changed.txt + + # Get issue context if available + if [ -n "$ISSUE" ]; then + gh issue view $ISSUE --json title,body > .claude/review-context/issue.json 2>/dev/null || echo "{}" > .claude/review-context/issue.json + else + echo "{}" > .claude/review-context/issue.json + fi + + - name: Load agent prompt + id: agent + shell: bash + run: | + if [ -f ".claude/agents/reviewers/requirements.md" ]; then + PROMPT=$(cat .claude/agents/reviewers/requirements.md) + else + PROMPT="# Requirements Review + + Check if changes align with issue requirements. + - Changed files: .claude/review-context/changed.txt + - Issue context: .claude/review-context/issue.json + + Evaluate: + 1. All acceptance criteria addressed + 2. Changes within issue scope + 3. No scope creep + + Output json {passed: bool, summary: ...}" + fi + + echo "prompt<> $GITHUB_OUTPUT + echo "$PROMPT" >> $GITHUB_OUTPUT + echo "ENDOFPROMPT" >> $GITHUB_OUTPUT + + - name: Run review + id: review + uses: anthropics/claude-code-base-action@beta + with: + claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }} + allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(cat:*)" + max_turns: 15 + prompt: ${{ steps.agent.outputs.prompt }} + + - name: Extract result + id: extract + shell: bash + run: | + # Initialize defaults - these will be used if parsing fails + PASSED="true" + RESULT='{"passed":true,"summary":"Review completed"}' + + EXEC="${{ steps.review.outputs.execution_file }}" + if [ -f "$EXEC" ]; then + TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC" 2>/dev/null || echo "") + JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}' | head -50) + if echo "$JSON" | jq . >/dev/null 2>&1; then + PASSED=$(echo "$JSON" | jq -r '.passed // true') + RESULT=$(echo "$JSON" | jq -c '.') + fi + fi + + # Always output - never skip these lines + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "result=$RESULT" >> $GITHUB_OUTPUT diff --git a/.github/actions/review-comment/action.yml b/.github/actions/review-comment/action.yml new file mode 100644 index 0000000..d294c7d --- /dev/null +++ b/.github/actions/review-comment/action.yml @@ -0,0 +1,60 @@ +name: Update Review Comment +description: Add or update a row in the consolidated CI review comment + +inputs: + review_name: + description: 'Name of the review (e.g., "Requirements", "Visual")' + required: true + passed: + description: 'Whether the review passed (true/false)' + required: true + result_json: + description: 'JSON result from the review with summary field' + required: true + pr_number: + description: 'Pull request number' + required: true + sha: + description: 'Commit SHA for unique comment marker' + required: true + github_token: + description: 'GitHub token for API access' + required: true + +runs: + using: composite + steps: + - name: Update review comment + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + run: | + MARKER="" + PR="${{ inputs.pr_number }}" + REVIEW="${{ inputs.review_name }}" + PASSED="${{ inputs.passed }}" + RESULT='${{ inputs.result_json }}' + + STATUS="✓" + [ "$PASSED" != "true" ] && STATUS="✗" + SUMMARY=$(echo "$RESULT" | jq -r '.summary // "N/A"' 2>/dev/null || echo "N/A") + + ROW="| $REVIEW | $STATUS | $SUMMARY |" + + # Check for existing comment + EXISTING=$(gh api "/repos/${{ github.repository }}/issues/$PR/comments" \ + --jq ".[] | select(.body | contains(\"$MARKER\")) | {id: .id, body: .body}" | head -1) + + if [ -n "$EXISTING" ]; then + COMMENT_ID=$(echo "$EXISTING" | jq -r '.id') + OLD_BODY=$(echo "$EXISTING" | jq -r '.body') + # Remove existing row for this review and add new one + NEW_BODY=$(echo "$OLD_BODY" | sed "/^| $REVIEW |/d") + # Insert new row before footer + NEW_BODY=$(echo "$NEW_BODY" | sed "/^---$/i\\$ROW") + gh api --method PATCH "/repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -f body="$NEW_BODY" + else + # Create new comment + COMMENT=$(printf '%s\n## Reviews\n\n| Review | Status | Summary |\n|--------|--------|---------|\n%s\n\n---\n*Claude Code CI*' "$MARKER" "$ROW") + gh pr comment "$PR" --body "$COMMENT" + fi diff --git a/.github/actions/ux-reviewer/action.yml b/.github/actions/ux-reviewer/action.yml new file mode 100644 index 0000000..3405983 --- /dev/null +++ b/.github/actions/ux-reviewer/action.yml @@ -0,0 +1,126 @@ +name: UX Reviewer +description: Review PR for UX issues - console errors, interactivity, behavior + +inputs: + claude_code_oauth_token: + description: 'Claude Code OAuth token' + required: true + pr_number: + description: 'Pull request number' + required: true + github_token: + description: 'GitHub token for API access' + required: true + e2e_results_path: + description: 'Path to E2E test results' + required: false + default: 'test-results' + +outputs: + passed: + description: 'Whether the review passed' + value: ${{ steps.extract.outputs.passed }} + result: + description: 'JSON result from the review' + value: ${{ steps.extract.outputs.result }} + console_errors: + description: 'List of console errors found' + value: ${{ steps.context.outputs.console_errors }} + +runs: + using: composite + steps: + - name: Gather UX context + id: context + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + run: | + mkdir -p .claude/review-context + + # Get changed files + gh pr view ${{ inputs.pr_number }} --json files --jq '.files[].path' > .claude/review-context/changed.txt + + # Check if changes affect UI/UX related files + RELEVANT=$(grep -E '\.(tsx?|jsx?|css|scss|html|vue|svelte)$' .claude/review-context/changed.txt | wc -l || echo "0") + echo "relevant_count=$RELEVANT" >> $GITHUB_OUTPUT + + # Extract console errors from E2E results if available + CONSOLE_ERRORS="" + if [ -d "${{ inputs.e2e_results_path }}" ]; then + # Look for console errors in Playwright trace/results + find "${{ inputs.e2e_results_path }}" -name "*.json" -exec cat {} \; 2>/dev/null | \ + jq -r '.. | .message? // empty' 2>/dev/null | \ + grep -iE 'error|warning|failed' | \ + head -20 > .claude/review-context/console_errors.txt || true + + CONSOLE_ERRORS=$(cat .claude/review-context/console_errors.txt 2>/dev/null || echo "") + fi + + echo "console_errors<> $GITHUB_OUTPUT + echo "$CONSOLE_ERRORS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Load agent prompt + id: agent + shell: bash + run: | + CONSOLE_ERRORS="${{ steps.context.outputs.console_errors }}" + + if [ -f ".claude/agents/reviewers/ux.md" ]; then + BASE_PROMPT=$(cat .claude/agents/reviewers/ux.md) + else + BASE_PROMPT="# UX Review - Analyze PR for UX issues" + fi + + if [ -n "$CONSOLE_ERRORS" ]; then + CONTEXT="Console errors detected - please analyze" + else + CONTEXT="No console errors detected" + fi + + FULL_PROMPT="$BASE_PROMPT + $CONTEXT + Changed files: .claude/review-context/changed.txt + Output json {passed: bool, summary: ...}" + + echo "prompt<> $GITHUB_OUTPUT + echo "$FULL_PROMPT" >> $GITHUB_OUTPUT + echo "ENDOFPROMPT" >> $GITHUB_OUTPUT + + - name: Run review + if: steps.context.outputs.relevant_count != '0' + id: review + uses: anthropics/claude-code-base-action@beta + with: + claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }} + allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(cat:*),Bash(ls:*)" + max_turns: 15 + prompt: ${{ steps.agent.outputs.prompt }} + + - name: Extract result + id: extract + shell: bash + run: | + # Initialize defaults - these will be used if parsing fails + PASSED="true" + RESULT='{"passed":true,"summary":"Review completed"}' + + # Handle case when no UX-relevant changes exist + if [ "${{ steps.context.outputs.relevant_count }}" = "0" ]; then + RESULT='{"passed":true,"summary":"No UX-relevant changes to review"}' + else + EXEC="${{ steps.review.outputs.execution_file }}" + if [ -f "$EXEC" ]; then + TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC" 2>/dev/null || echo "") + JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}' | head -50) + if echo "$JSON" | jq . >/dev/null 2>&1; then + PASSED=$(echo "$JSON" | jq -r '.passed // true') + RESULT=$(echo "$JSON" | jq -c '.') + fi + fi + fi + + # Always output - never skip these lines + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "result=$RESULT" >> $GITHUB_OUTPUT diff --git a/.github/actions/visual-reviewer/action.yml b/.github/actions/visual-reviewer/action.yml new file mode 100644 index 0000000..ce94412 --- /dev/null +++ b/.github/actions/visual-reviewer/action.yml @@ -0,0 +1,166 @@ +name: Visual Reviewer +description: Review UI screenshots for design, accessibility, and visual regression + +inputs: + claude_code_oauth_token: + description: 'Claude Code OAuth token' + required: true + pr_number: + description: 'Pull request number' + required: true + github_token: + description: 'GitHub token for API access' + required: true + screenshots_dir: + description: 'Directory containing screenshots' + required: false + default: '.claude/screenshots' + approvals_file: + description: 'Path to visual approvals JSON file' + required: false + default: '.claude-review/visual-approvals.json' + model: + description: 'Claude model to use for visual analysis' + required: false + default: 'claude-sonnet-4-5-20250929' + +outputs: + passed: + description: 'Whether the review passed' + value: ${{ steps.extract.outputs.passed }} + result: + description: 'JSON result from the review' + value: ${{ steps.extract.outputs.result }} + regression_detected: + description: 'Whether visual regression was detected' + value: ${{ steps.regression.outputs.detected }} + changed_screenshots: + description: 'List of screenshots that changed' + value: ${{ steps.regression.outputs.changed }} + +runs: + using: composite + steps: + - name: Check for screenshots + id: check + shell: bash + run: | + mkdir -p ${{ inputs.screenshots_dir }} + mkdir -p $(dirname ${{ inputs.approvals_file }}) + + COUNT=$(ls -1 ${{ inputs.screenshots_dir }}/*.png 2>/dev/null | wc -l || echo "0") + echo "count=$COUNT" >> $GITHUB_OUTPUT + + - name: Check for regressions + id: regression + shell: bash + run: | + APPROVAL_FILE="${{ inputs.approvals_file }}" + SCREENSHOTS_DIR="${{ inputs.screenshots_dir }}" + CHANGED="" + NEW="" + DETECTED="false" + + if [ -f "$APPROVAL_FILE" ] && [ "${{ steps.check.outputs.count }}" != "0" ]; then + for screenshot in "$SCREENSHOTS_DIR"/*.png; do + [ -f "$screenshot" ] || continue + name=$(basename "$screenshot") + current_hash=$(sha256sum "$screenshot" | cut -d' ' -f1) + + # Check if this screenshot was previously approved + approved_hash=$(jq -r ".approvals[] | select(.screenshot == \"$name\") | .hash // \"\"" "$APPROVAL_FILE" 2>/dev/null || echo "") + + if [ -z "$approved_hash" ]; then + NEW="$NEW $name" + elif [ "$current_hash" != "$approved_hash" ]; then + CHANGED="$CHANGED $name" + DETECTED="true" + fi + done + fi + + echo "detected=$DETECTED" >> $GITHUB_OUTPUT + echo "changed=$CHANGED" >> $GITHUB_OUTPUT + echo "new=$NEW" >> $GITHUB_OUTPUT + + # Determine message type + if [ -n "$CHANGED" ]; then + echo "message_type=regression" >> $GITHUB_OUTPUT + elif [ -n "$NEW" ]; then + echo "message_type=new" >> $GITHUB_OUTPUT + else + echo "message_type=unchanged" >> $GITHUB_OUTPUT + fi + + - name: Load agent prompt + id: agent + shell: bash + run: | + MSG_TYPE="${{ steps.regression.outputs.message_type }}" + CHANGED="${{ steps.regression.outputs.changed }}" + NEW="${{ steps.regression.outputs.new }}" + + case "$MSG_TYPE" in + "regression") + CONTEXT="VISUAL REGRESSION - Changed: $CHANGED" + ;; + "new") + CONTEXT="NEW SCREENSHOTS - New: $NEW" + ;; + *) + CONTEXT="All screenshots match approved versions" + ;; + esac + + if [ -f ".claude/agents/reviewers/visual.md" ]; then + BASE_PROMPT=$(cat .claude/agents/reviewers/visual.md) + else + BASE_PROMPT="# Visual Review - Screenshots in ${{ inputs.screenshots_dir }}/" + fi + + FULL_PROMPT="$BASE_PROMPT + $CONTEXT + Evaluate: Design, Accessibility, Responsiveness + Output json {passed: bool, summary: ...}" + + echo "prompt<> $GITHUB_OUTPUT + echo "$FULL_PROMPT" >> $GITHUB_OUTPUT + echo "ENDOFPROMPT" >> $GITHUB_OUTPUT + + - name: Run review + if: steps.check.outputs.count != '0' + id: review + uses: anthropics/claude-code-base-action@beta + with: + claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }} + allowed_tools: "View,GlobTool,Bash(ls:*)" + max_turns: 20 + model: ${{ inputs.model }} + prompt: ${{ steps.agent.outputs.prompt }} + + - name: Extract result + id: extract + shell: bash + run: | + # Initialize defaults - these will be used if parsing fails + PASSED="true" + RESULT='{"passed":true,"summary":"Review completed"}' + + # Handle case when no screenshots exist + if [ "${{ steps.check.outputs.count }}" = "0" ]; then + RESULT='{"passed":true,"summary":"No screenshots to review"}' + else + EXEC="${{ steps.review.outputs.execution_file }}" + if [ -f "$EXEC" ]; then + TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC" 2>/dev/null || echo "") + JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}' | head -50) + if echo "$JSON" | jq . >/dev/null 2>&1; then + PASSED=$(echo "$JSON" | jq -r '.passed // true') + RESULT=$(echo "$JSON" | jq -c '.') + fi + fi + fi + + # Always output - never skip these lines + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "result=$RESULT" >> $GITHUB_OUTPUT diff --git a/.github/ci-config.yml b/.github/ci-config.yml index 2f9d366..5c4ef4e 100644 --- a/.github/ci-config.yml +++ b/.github/ci-config.yml @@ -2,7 +2,7 @@ # Template for Claude Code repos ci: - # Set to true if this repo has an app (enables E2E + UI review) + # Set to true if this repo has an app (enables E2E + Visual/UX reviews) has_app: false # Static Analysis: Lint (ESLint) + Types (TypeScript) @@ -16,11 +16,12 @@ ci: unit: true e2e: true - # Reviews: Requirements, Code Quality, Context, UI + # Reviews: Requirements, Code Quality, Context, Visual, UX # Requirements must pass before others run - # UI only runs if has_app: true and screenshots exist + # Visual/UX only run if has_app: true reviews: requirements: true code_quality: true context: true - ui: true + visual: true + ux: true diff --git a/.github/templates/template-cloudflare.yml b/.github/templates/template-cloudflare.yml index 54d98bd..b1bf587 100644 --- a/.github/templates/template-cloudflare.yml +++ b/.github/templates/template-cloudflare.yml @@ -44,7 +44,8 @@ ci: project_memory: true # Use project context for smarter reviews agents: true # Enable multi-agent review capabilities skills: true # Use specialized review skills - playwright_ui: true # AI reviews UI changes via Playwright screenshots + visual: true # AI reviews screenshots for design/accessibility + ux: true # AI reviews for UX issues and console errors # --------------------------------------------------------------------------- # Deployment Configuration diff --git a/.github/templates/template-non-deployable.yml b/.github/templates/template-non-deployable.yml index 3fd93c0..6bd912b 100644 --- a/.github/templates/template-non-deployable.yml +++ b/.github/templates/template-non-deployable.yml @@ -45,7 +45,8 @@ ci: project_memory: true # Use project context for smarter reviews agents: true # Enable multi-agent review capabilities skills: true # Use specialized review skills - playwright_ui: false # No UI review without a deployed preview + visual: false # No visual review without a deployed preview + ux: false # No UX review without a deployed preview # --------------------------------------------------------------------------- # Deployment Configuration diff --git a/.github/templates/template-supabase-vercel.yml b/.github/templates/template-supabase-vercel.yml index 5a40486..f6d17b1 100644 --- a/.github/templates/template-supabase-vercel.yml +++ b/.github/templates/template-supabase-vercel.yml @@ -44,7 +44,8 @@ ci: project_memory: true # Use project context for smarter reviews agents: true # Enable multi-agent review capabilities skills: true # Use specialized review skills - playwright_ui: true # AI reviews UI changes via Playwright screenshots + visual: true # AI reviews screenshots for design/accessibility + ux: true # AI reviews for UX issues and console errors # --------------------------------------------------------------------------- # Deployment Configuration diff --git a/.github/templates/template-vercel.yml b/.github/templates/template-vercel.yml index c4bdb12..5161d53 100644 --- a/.github/templates/template-vercel.yml +++ b/.github/templates/template-vercel.yml @@ -42,7 +42,8 @@ ci: project_memory: true # Use project context for smarter reviews agents: true # Enable multi-agent review capabilities skills: true # Use specialized review skills - playwright_ui: true # AI reviews UI changes via Playwright screenshots + visual: true # AI reviews screenshots for design/accessibility + ux: true # AI reviews for UX issues and console errors # --------------------------------------------------------------------------- # Deployment Configuration diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 9674d85..a07b314 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -205,339 +205,115 @@ jobs: # ============================================ # REVIEWS (requires Tests, PR only) # ============================================ + # Uses composite actions from .github/actions/ for cleaner workflow + # Each reviewer action handles: context gathering, Claude review, result extraction + # The review-comment action handles: updating the consolidated PR comment + requirements: name: "Reviews / Requirements" needs: [unit, e2e] if: github.event_name == 'pull_request' && !failure() && !cancelled() runs-on: ubuntu-latest outputs: - passed: ${{ steps.extract.outputs.passed }} - result: ${{ steps.extract.outputs.result }} + passed: ${{ steps.review.outputs.passed }} + result: ${{ steps.review.outputs.result }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Get context - id: context - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Create review context directory - mkdir -p .claude/review-context - - BRANCH="${{ github.head_ref }}" - echo "branch=$BRANCH" >> $GITHUB_OUTPUT - - # Extract issue number - ISSUE="" - [[ $BRANCH =~ ^([0-9]+)- ]] && ISSUE="${BASH_REMATCH[1]}" - [[ $BRANCH =~ ^(feature|fix)/([0-9]+) ]] && ISSUE="${BASH_REMATCH[2]}" - echo "issue=$ISSUE" >> $GITHUB_OUTPUT - - # Get changed files - gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' > .claude/review-context/changed.txt - - # Get issue context - if [ -n "$ISSUE" ]; then - gh issue view $ISSUE --json title,body > .claude/review-context/issue.json 2>/dev/null || echo "{}" > .claude/review-context/issue.json - fi - - - name: Run review + - name: Run requirements review id: review - uses: anthropics/claude-code-base-action@beta + uses: ./.github/actions/requirements-reviewer with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(cat:*)" - max_turns: 15 - prompt: | - # Requirements Review - Check if changes align with issue requirements. - - Changed files: .claude/review-context/changed.txt - - Issue context: .claude/review-context/issue.json - Output ```json {"passed": bool, "summary": "..."} ``` - - - name: Extract result - id: extract - run: | - EXEC="${{ steps.review.outputs.execution_file }}" - if [ -f "$EXEC" ]; then - TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC") - JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}') - if echo "$JSON" | jq . >/dev/null 2>&1; then - echo "$JSON" > /tmp/result.json - echo "passed=$(jq -r '.passed // true' /tmp/result.json)" >> $GITHUB_OUTPUT - echo "result=$(jq -c '.' /tmp/result.json)" >> $GITHUB_OUTPUT - exit 0 - fi - fi - echo "passed=true" >> $GITHUB_OUTPUT - echo 'result={"passed":true,"summary":"Review completed"}' >> $GITHUB_OUTPUT + pr_number: ${{ github.event.pull_request.number }} + branch: ${{ github.head_ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Update review comment + - name: Update comment if: always() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - MARKER="" - PR="${{ github.event.pull_request.number }}" - REVIEW="Requirements" - PASSED="${{ steps.extract.outputs.passed }}" - RESULT='${{ steps.extract.outputs.result }}' - - STATUS="✓" - [ "$PASSED" != "true" ] && STATUS="✗" - SUMMARY=$(echo "$RESULT" | jq -r '.summary // "N/A"' 2>/dev/null || echo "N/A") - - ROW="| $REVIEW | $STATUS | $SUMMARY |" - - # Check for existing comment - EXISTING=$(gh api "/repos/${{ github.repository }}/issues/$PR/comments" \ - --jq ".[] | select(.body | contains(\"$MARKER\")) | {id: .id, body: .body}" | head -1) - - if [ -n "$EXISTING" ]; then - COMMENT_ID=$(echo "$EXISTING" | jq -r '.id') - OLD_BODY=$(echo "$EXISTING" | jq -r '.body') - # Append new row - NEW_BODY=$(echo "$OLD_BODY" | sed "/^| $REVIEW |/d") - NEW_BODY=$(printf '%s\n%s' "$NEW_BODY" "$ROW") - gh api --method PATCH "/repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -f body="$NEW_BODY" - else - # Create new comment - COMMENT=$(cat < .claude/review-context/changed.txt - - - name: Run review + - name: Run code quality review id: review - uses: anthropics/claude-code-base-action@beta + uses: ./.github/actions/code-quality-reviewer with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(cat:*)" - max_turns: 15 - prompt: | - # Code Quality Review - Evaluate DRY, YAGNI, modularity for files in .claude/review-context/changed.txt - Output ```json {"passed": bool, "summary": "..."} ``` - - - name: Extract result - id: extract - run: | - EXEC="${{ steps.review.outputs.execution_file }}" - if [ -f "$EXEC" ]; then - TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC") - JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}') - if echo "$JSON" | jq . >/dev/null 2>&1; then - echo "$JSON" > /tmp/result.json - echo "passed=$(jq -r '.passed // true' /tmp/result.json)" >> $GITHUB_OUTPUT - echo "result=$(jq -c '.' /tmp/result.json)" >> $GITHUB_OUTPUT - exit 0 - fi - fi - echo "passed=true" >> $GITHUB_OUTPUT - echo 'result={"passed":true,"summary":"Review completed"}' >> $GITHUB_OUTPUT + pr_number: ${{ github.event.pull_request.number }} + github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Update review comment + - name: Update comment if: always() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - MARKER="" - PR="${{ github.event.pull_request.number }}" - REVIEW="Code Quality" - PASSED="${{ steps.extract.outputs.passed }}" - RESULT='${{ steps.extract.outputs.result }}' - - STATUS="✓" - [ "$PASSED" != "true" ] && STATUS="✗" - SUMMARY=$(echo "$RESULT" | jq -r '.summary // "N/A"' 2>/dev/null || echo "N/A") - - ROW="| $REVIEW | $STATUS | $SUMMARY |" - - # Check for existing comment - EXISTING=$(gh api "/repos/${{ github.repository }}/issues/$PR/comments" \ - --jq ".[] | select(.body | contains(\"$MARKER\")) | {id: .id, body: .body}" | head -1) - - if [ -n "$EXISTING" ]; then - COMMENT_ID=$(echo "$EXISTING" | jq -r '.id') - OLD_BODY=$(echo "$EXISTING" | jq -r '.body') - # Append new row before the footer - NEW_BODY=$(echo "$OLD_BODY" | sed "/---/i\\$ROW") - gh api --method PATCH "/repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -f body="$NEW_BODY" - else - # Create new comment (shouldn't happen as requirements runs first) - COMMENT=$(cat < .claude/review-context/changed.txt - - > .claude/review-context/claude_files.txt - while IFS= read -r file; do - dir=$(dirname "$file") - while [ "$dir" != "." ]; do - [ -f "$dir/CLAUDE.md" ] && echo "$dir/CLAUDE.md" >> .claude/review-context/claude_files.txt - dir=$(dirname "$dir") - done - [ -f "CLAUDE.md" ] && echo "CLAUDE.md" >> .claude/review-context/claude_files.txt - done < .claude/review-context/changed.txt - - sort -u .claude/review-context/claude_files.txt -o .claude/review-context/claude_files.txt - COUNT=$(wc -l < .claude/review-context/claude_files.txt | tr -d ' ') - echo "count=$COUNT" >> $GITHUB_OUTPUT - - - name: Run review - if: steps.find.outputs.count != '0' + - name: Run context review id: review - uses: anthropics/claude-code-base-action@beta + uses: ./.github/actions/context-reviewer with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*)" - max_turns: 15 - prompt: | - # Context Review - Check changes against CLAUDE.md files listed in .claude/review-context/claude_files.txt - Changed files: .claude/review-context/changed.txt - Output ```json {"passed": bool, "summary": "..."} ``` - - - name: Extract result - id: extract - run: | - if [ "${{ steps.find.outputs.count }}" = "0" ]; then - echo "passed=true" >> $GITHUB_OUTPUT - echo 'result={"passed":true,"summary":"No CLAUDE.md files to check"}' >> $GITHUB_OUTPUT - exit 0 - fi - - EXEC="${{ steps.review.outputs.execution_file }}" - if [ -f "$EXEC" ]; then - TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC") - JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}') - if echo "$JSON" | jq . >/dev/null 2>&1; then - echo "$JSON" > /tmp/result.json - echo "passed=$(jq -r '.passed // true' /tmp/result.json)" >> $GITHUB_OUTPUT - echo "result=$(jq -c '.' /tmp/result.json)" >> $GITHUB_OUTPUT - exit 0 - fi - fi - echo "passed=true" >> $GITHUB_OUTPUT - echo 'result={"passed":true,"summary":"Review completed"}' >> $GITHUB_OUTPUT + pr_number: ${{ github.event.pull_request.number }} + github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Update review comment + - name: Update comment if: always() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - MARKER="" - PR="${{ github.event.pull_request.number }}" - REVIEW="Context" - PASSED="${{ steps.extract.outputs.passed }}" - RESULT='${{ steps.extract.outputs.result }}' - - STATUS="✓" - [ "$PASSED" != "true" ] && STATUS="✗" - SUMMARY=$(echo "$RESULT" | jq -r '.summary // "N/A"' 2>/dev/null || echo "N/A") - - ROW="| $REVIEW | $STATUS | $SUMMARY |" - - # Check for existing comment - EXISTING=$(gh api "/repos/${{ github.repository }}/issues/$PR/comments" \ - --jq ".[] | select(.body | contains(\"$MARKER\")) | {id: .id, body: .body}" | head -1) - - if [ -n "$EXISTING" ]; then - COMMENT_ID=$(echo "$EXISTING" | jq -r '.id') - OLD_BODY=$(echo "$EXISTING" | jq -r '.body') - # Append new row before the footer - NEW_BODY=$(echo "$OLD_BODY" | sed "/---/i\\$ROW") - gh api --method PATCH "/repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -f body="$NEW_BODY" - else - # Create new comment (shouldn't happen as requirements runs first) - COMMENT=$(cat </dev/null | wc -l || echo "0") - echo "count=$COUNT" >> $GITHUB_OUTPUT - - - name: Run review - if: steps.check.outputs.count != '0' + - name: Run visual review id: review - uses: anthropics/claude-code-base-action@beta + uses: ./.github/actions/visual-reviewer with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Bash(ls:*)" - max_turns: 20 - model: claude-sonnet-4-5-20250929 - prompt: | - # UI Review - Review screenshots in .claude/screenshots/ - Evaluate design, accessibility, UX - Output ```json {"passed": bool, "summary": "..."} ``` - - - name: Extract result - id: extract - run: | - if [ "${{ steps.check.outputs.count }}" = "0" ]; then - echo "passed=true" >> $GITHUB_OUTPUT - echo 'result={"passed":true,"summary":"No screenshots to review"}' >> $GITHUB_OUTPUT - exit 0 - fi + pr_number: ${{ github.event.pull_request.number }} + github_token: ${{ secrets.GITHUB_TOKEN }} - EXEC="${{ steps.review.outputs.execution_file }}" - if [ -f "$EXEC" ]; then - TEXT=$(jq -r '[.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text] | last // ""' "$EXEC") - JSON=$(echo "$TEXT" | sed -n '/```json/,/```/{/```/d;p;}') - if echo "$JSON" | jq . >/dev/null 2>&1; then - echo "$JSON" > /tmp/result.json - echo "passed=$(jq -r '.passed // true' /tmp/result.json)" >> $GITHUB_OUTPUT - echo "result=$(jq -c '.' /tmp/result.json)" >> $GITHUB_OUTPUT - exit 0 - fi - fi - echo "passed=true" >> $GITHUB_OUTPUT - echo 'result={"passed":true,"summary":"Review completed"}' >> $GITHUB_OUTPUT + - name: Update comment + if: always() + uses: ./.github/actions/review-comment + with: + review_name: Visual + passed: ${{ steps.review.outputs.passed }} + result_json: ${{ steps.review.outputs.result }} + pr_number: ${{ github.event.pull_request.number }} + sha: ${{ github.sha }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + ux: + name: "Reviews / UX" + needs: [lint, requirements, e2e] + if: always() && github.event_name == 'pull_request' && needs.lint.outputs.has_app == 'true' && needs.requirements.result == 'success' + runs-on: ubuntu-latest + outputs: + passed: ${{ steps.review.outputs.passed }} + result: ${{ steps.review.outputs.result }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Update review comment + - name: Run UX review + id: review + uses: ./.github/actions/ux-reviewer + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + pr_number: ${{ github.event.pull_request.number }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update comment if: always() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - MARKER="" - PR="${{ github.event.pull_request.number }}" - REVIEW="UI" - PASSED="${{ steps.extract.outputs.passed }}" - RESULT='${{ steps.extract.outputs.result }}' - - STATUS="✓" - [ "$PASSED" != "true" ] && STATUS="✗" - SUMMARY=$(echo "$RESULT" | jq -r '.summary // "N/A"' 2>/dev/null || echo "N/A") - - ROW="| $REVIEW | $STATUS | $SUMMARY |" - - # Check for existing comment - EXISTING=$(gh api "/repos/${{ github.repository }}/issues/$PR/comments" \ - --jq ".[] | select(.body | contains(\"$MARKER\")) | {id: .id, body: .body}" | head -1) - - if [ -n "$EXISTING" ]; then - COMMENT_ID=$(echo "$EXISTING" | jq -r '.id') - OLD_BODY=$(echo "$EXISTING" | jq -r '.body') - # Append new row before the footer - NEW_BODY=$(echo "$OLD_BODY" | sed "/---/i\\$ROW") - gh api --method PATCH "/repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -f body="$NEW_BODY" - else - # Create new comment (shouldn't happen as requirements runs first) - COMMENT=$(cat <